Files
BlackMarkandClaude Opus 5.5 45424e6fd6 Vermietungs-Tool
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>
2026-11-02 16:52:27 +01:00

97 lines
7.1 KiB
HTML

{% extends "base.html" %}
{% block main_class %}grid-page{% endblock %}
{% block content %}
<h1>{{ t('ledger_title') }}</h1>
<div class="toolbar">
<input class="search live-search" data-grid="led" type="text" placeholder="{{ t('search_placeholder') }}">
{# Jahr first: the list spans many years, and picking one is the most common filter #}
<select class="live-filter" data-grid="led" data-col="{{ t('col_from') }}">
<option value="">{{ t('filter_all', col=t('filter_year')) }}</option>
{% for y in years %}<option>{{ y }}</option>{% endfor %}</select>
<select class="live-filter" data-grid="led" data-col="{{ t('hd_room') }}"><option value="">{{ t('filter_all', col=t('col_room')) }}</option>
{% for r in rooms %}<option>{{ r.number }}</option>{% endfor %}</select>
{# between Zimmer and Bezahlt: who is still here? A row without 'Bis' is still running. #}
<select class="live-filter" data-grid="led" data-col="{{ t('col_to') }}">
<option value="">{{ t('filter_all', col=t('filter_persons')) }}</option>
<option>{{ t('filter_still_here') }}</option></select>
<select class="live-filter" data-grid="led" data-col="{{ t('col_payment') }}"><option value="">{{ t('filter_all', col=t('col_payment')) }}</option>
<option>{{ t('payment_paid') }}</option><option>{{ t('payment_open') }}</option><option>{{ t('payment_cash') }}</option>
<option value="{{ t('payment_unknown') }}">{{ t('payment_unknown_label') }}</option></select>
<span class="count"><span class="grid-count" data-grid="led">{{ rows|length }}</span> {{ t('showing_count', count='')|trim }}</span>
<span class="count">{{ t('sum_label') }}: <span class="grid-sum" data-grid="led" data-col="{{ t('col_amount') }}"></span> €</span>
<span class="spacer"></span>
<button class="btn secondary" type="button" onclick="invoiceFromSelection()">{{ t('btn_invoice_from_selection') }}</button>
<button class="btn secondary" type="button" onclick="gridPrint('led')">{{ t('btn_print') }}</button>
<a class="btn" href="{{ url_for('ledger_new') }}">+ {{ t('btn_add_ledger') }}</a>
</div>
<p class="printinfo" data-grid="led"></p>
<div class="tablewrap">
{# data-pin: Zimmer + Name stay on screen while scrolling to the payment columns.
data-max: tighter maximum widths, so RNr. and Re vom can be read on a small screen (1280 px at 150 %)
without scrolling — anything longer is cut off and shows in full on hover #}
<table id="led" class="grid" data-scroll="end" data-pin="1 2 6">
<thead><tr>
<th class="nosort selcol"></th><th title="{{ t('col_room') }}">{{ t('hd_room') }}</th><th>{{ t('col_from') }}</th><th>{{ t('col_to') }}</th>
<th title="{{ t('col_gb') }}" data-max="70">{{ t('hd_gb') }}</th><th data-max="135">{{ t('col_occupant') }}</th><th class="num">{{ t('col_days') }}</th>
<th class="num">{{ t('col_rate') }}</th><th class="num" title="{{ t('col_qty') }}">{{ t('hd_qty') }}</th>
<th class="num" title="{{ t('col_persons') }}">{{ t('hd_persons') }}</th><th class="num">{{ t('col_amount') }}</th>
<th class="num">{{ t('col_tourism_tax') }}</th>
<th title="{{ t('col_payer') }}" data-max="90">{{ t('hd_payer') }}</th><th data-max="40">{{ t('col_art') }}</th><th title="{{ t('col_invoice') }}" data-max="72">{{ t('hd_invoice') }}</th>
<th title="{{ t('col_invoice_date') }}">{{ t('hd_invoice_date') }}</th><th data-max="105">{{ t('col_payment') }}</th>
<th title="{{ t('col_paid_date') }}">{{ t('hd_paid_date') }}</th><th>{{ t('col_notes') }}</th><th class="nosort actions"></th>
</tr></thead>
<tbody>
{# rows ship inside an inert <template>: grids.js renders only the visible window (see freezeLayout) #}
<template>
{% for r in rows %}
<tr>
<td class="selcol"><input type="checkbox" class="rowsel" value="{{ r.charge_id }}"></td>
<td>{{ r.room or '' }}</td>
{%- set year = (r.first_date or '')[:4] %}
{#- the weekday before the date, to be seen at a glance. data-k keeps the sort:
makeKey only recognises a bare DD.MM.YYYY; with "Mo " in front it would sort alphabetically -#}
<td{% if year %} data-f="{{ year }}"{% endif %}{% if r.period_from %} data-k="{{ r.period_from|replace('-', '') }}"{% endif %}>{{ r.period_from|datewd }}</td>
{#- 'noch da' = a running occupancy: Von set, no Bis, and either a Gästeblatt number or not billed
yet. 'Bereitstellung'/'Storno' rows without Bis are settled items, not people in the house. -#}
<td{% if r.period_from and not r.period_to and (r.gb_ref or not r.line_net_override) %} data-f="{{ t('filter_still_here') }}"{% endif %}{% if r.period_to %} data-k="{{ r.period_to|replace('-', '') }}"{% endif %}>{{ r.period_to|datewd }}</td><td>{{ r.gb_ref or '' }}</td><td>{{ r.occupant or '' }}</td>
<td class="num">{{ r.days if r.days is not none else '' }}</td>
<td class="num">{{ r.gross_per_unit|num if r.gross_per_unit else '' }}</td>
<td class="num">{{ (r.quantity|num(0) if r.quantity % 1 == 0 else r.quantity|num(1)) if r.quantity is not none else '' }}</td>
<td class="num">{{ r.persons|num(0) if r.persons }}</td>
<td class="num">{{ r.amount|euro }}</td>
<td class="num">{{ r.tourism_tax|euro if r.tourism_tax else '' }}</td>
<td>{{ r.payer or '' }}</td><td>{{ r.art or '' }}</td>
<td>{% if r.invoice_id %}<a href="{{ url_for('invoice_view', invoice_id=r.invoice_id) }}">{{ r.invoice_no }}</a>
{% else %}{{ r.invoice_no or '' }}{% endif %}</td>
<td>{{ r.invoice_date|date }}</td>
{#- paid or open: repo._payment — the Umsatzsteuer list reads the same rule -#}
{%- set payment = r.payment %}
{#- data-f: the filter asks for the STATE, even when text stands beside it (the part payment)
— without it "OFFEN" would miss exactly these rows -#}
<td data-f="{{ t('payment_' + payment) }}">
{%- if payment == 'unknown' %}{{ t('payment_unknown') }}
{%- else %}<span class="pill {{ payment }}">{{ t('payment_' + payment) }}</span>{% endif %}
{%- if r.open_amount %} {{ t('payment_part', paid=r.paid_amount|euro, open=r.open_amount|euro) }}{% endif %}</td>
{#- without a date of the row's own, this shows when the INVOICE was paid -#}
<td>{{ r.paid_on|date }}</td>
<td>{{ r.notes or '' }}</td>
<td class="actions"><a class="btn secondary small" href="{{ url_for('ledger_edit', charge_id=r.charge_id) }}">{{ t('btn_edit') }}</a></td>
</tr>
{% endfor %}
</template>
{% if not rows %}
<tr data-empty="1"><td class="empty" colspan="20">{{ t('no_results') }}</td></tr>
{% endif %}
</tbody>
</table>
</div>
<script>
function invoiceFromSelection() {
var ids = window.gridChecked('led'); // from the grid model — most rows are not in the DOM
if (!ids.length) { alert('Bitte zuerst Zeilen auswählen.'); return; }
window.location = '{{ url_for("invoice_new") }}?charges=' + ids.join(',');
}
</script>
{% endblock %}