Files
vermietungs-tool/app/templates/invoice_print.html
T
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

175 lines
10 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="de">
{% set accent = '#26588c' if inv.unit.theme == 'blue' else '#2f6d5b' %}
{% set units_de = {'week':'Wochen','day':'Tage','night':'Nächte','month':'Monate','total':''} %}
<head>
<meta charset="utf-8">
<title>Rechnung {{ inv.number }}</title>
<style>
/* Full-bleed A4: the sheet maps 1:1 to the page so the recipient block hits the DIN window and the totals
block anchors at a constant Y. Up to 10 line rows fit one page. */
@page { size: A4; margin: 0; }
* { box-sizing: border-box; }
html, body { margin: 0; }
body { font-family: Arial, "Segoe UI", sans-serif; color:#1b1b1b; font-size:13px; background:#e9e9e9; }
.toolbar { display:flex; gap:10px; width:210mm; max-width:100%; margin:14px auto 10px; }
.toolbar a, .toolbar button { font-size:14px; padding:8px 16px; border-radius:8px; border:1px solid #ccc;
background:#f3f3f3; color:#222; cursor:pointer; text-decoration:none; }
.toolbar .primary { background:{{ accent }}; color:#fff; border-color:{{ accent }}; }
.flash { width:210mm; max-width:100%; margin:0 auto 10px; box-sizing:border-box; padding:8px 14px; font-size:14px;
background:#fff6e6; border:1px solid #e6c98a; color:#8a5a12; border-radius:8px; }
.sheet { position:relative; width:210mm; min-height:296mm; margin:0 auto; background:#fff;
display:flex; flex-direction:column; box-shadow:0 1px 8px rgba(0,0,0,.18); }
/* Recipient — DIN 5008 form B address field (20mm left, 45mm top) → shows in a window envelope. */
.recipient { position:absolute; left:20mm; top:45mm; width:85mm; height:45mm; }
.recipient .return { font-size:8px; color:#555; border-bottom:1px solid #999;
padding-bottom:2px; margin-bottom:9px; white-space:nowrap; }
.recipient .to { line-height:1.4; }
.recipient .to b { font-size:15px; }
.recipient .uid { font-size:10px; color:#333; margin-top:3px; }
/* .main grows to push the bottom block (totals+notice+footer) to a fixed position → totals at a constant Y. */
.main { flex:1 0 auto; display:flex; flex-direction:column; padding:9mm 14mm 0; }
/* 82mm clears both the address window (bottom 90mm) and the branding stack for every unit. */
.masthead { position:relative; min-height:82mm; }
.masthead .brand .name { color:{{ accent }}; font-size:25px; font-weight:700; letter-spacing:1px; line-height:1.1; }
.masthead .brand .sub { color:{{ accent }}; font-style:italic; margin:2px 0 0; }
/* Branding column, top-right, spanning the masthead. Each image gets an explicit size from
masthead_art(): all images of a unit share ONE width, solved so the stack fills the box height
(capped at the box width) — logo and house always render equally wide, with no dead letterbox
space. Box is 64 x 80 mm, gap 4mm — keep in sync with ART_BOX_* in web.py. */
.masthead .art { position:absolute; top:0; right:0; bottom:2mm; width:64mm;
display:flex; flex-direction:column; align-items:flex-end; justify-content:center; gap:4mm; }
.masthead .art img { border-radius:2px; }
.title { font-weight:700; font-size:16px; margin:0 0 3px; }
.date { font-size:12px; margin-bottom:8px; }
table.lines { width:100%; border-collapse:collapse; font-size:10.5px; }
table.lines th, table.lines td { border:1px solid #9a9a9a; } /* full gridlines */
table.lines th { background:{{ accent }}; color:#fff; font-weight:600; text-align:left; padding:4px 6px; font-size:9.5px; }
table.lines td { padding:2px 6px; vertical-align:bottom; line-height:1.25; }
table.lines td.nm { vertical-align:top; } table.lines td.nm .wn { font-weight:700; }
table.lines td.n, table.lines th.n { text-align:right; font-variant-numeric:tabular-nums; white-space:nowrap; }
/* Anchored bottom block. */
.bottom { flex-shrink:0; padding:0 14mm; }
table.totals { margin-left:auto; border-collapse:collapse; font-size:13px; margin-top:5mm; }
table.totals td { padding:3px 10px; }
table.totals td.l { text-align:right; font-weight:700; color:#333; white-space:nowrap; }
table.totals td.v { text-align:right; font-variant-numeric:tabular-nums; min-width:120px; }
table.totals tr.grand td { font-size:15px; font-weight:700; border-top:2px solid {{ accent }}; color:{{ accent }}; padding-top:5px; }
.note { text-align:center; white-space:pre-line; min-height:12mm; margin:6mm 0 0; font-size:12px; }
.thanks { text-align:center; font-weight:600; margin:4mm 0 0; }
footer { flex-shrink:0; display:flex; justify-content:center; gap:40px; margin:5mm 14mm 10mm;
border-top:1px solid #ddd; padding-top:8px; font-size:11px; color:#444; }
footer .col b { color:{{ accent }}; display:block; margin-bottom:3px; }
@media print { body { background:#fff; } .toolbar, .flash { display:none; } .sheet { box-shadow:none; margin:0; } }
</style>
</head>
<body>
<div class="toolbar">
<button class="primary" onclick="window.print()">{{ t('btn_print') }}</button>
<a href="{{ url_for('invoice_pdf', invoice_id=inv.id) }}">{{ t('btn_pdf') }}</a>
<a href="{{ url_for('invoice_edit', invoice_id=inv.id) }}">{{ t('btn_edit') }}</a>
<a href="{{ url_for('invoice_payment', invoice_id=inv.id) }}">{{ t('btn_payment') }}{% if inv.paid_date %} ✓{% endif %}</a>
<a href="{{ url_for('invoices') }}">{{ t('btn_back') }}</a>
{% if can_regen %}
<form method="post" action="{{ url_for('invoice_regenerate', invoice_id=inv.id) }}" style="display:inline"
onsubmit="return confirm('{{ t('confirm_regenerate') }}')">
<button type="submit" class="primary">{{ t('btn_regenerate') }}</button>
</form>
{% endif %}
<form method="post" action="{{ url_for('invoice_delete', invoice_id=inv.id) }}" style="display:inline;margin-left:auto"
onsubmit="return confirm('{{ t('confirm_delete') }}')">
<button type="submit" style="background:#fff;color:#b23a3a;border:1px solid #e3b7b7;padding:8px 16px;border-radius:8px;cursor:pointer">{{ t('btn_delete') }}</button>
</form>
</div>
{% with msgs = get_flashed_messages() %}
{% if msgs %}<div class="flash">{% for m in msgs %}<div>{{ m }}</div>{% endfor %}</div>{% endif %}
{% endwith %}
<div class="sheet">
<div class="recipient">
<div class="return">{{ inv.unit.name }}, {{ (inv.unit.address or '')|replace('\n', ', ') }}</div>
<div class="to">
{% if inv.client and inv.client.name %}
{% if inv.client.kind == 'private' %}Herrn{% else %}Firma{% endif %}<br>
<b>{{ inv.client.name }}</b><br>
{{ inv.client.address or '' }}{% if inv.client.postcode or inv.client.city %}<br>{{ inv.client.postcode or '' }} {{ inv.client.city or '' }}{% endif %}
{% if inv.gross and inv.gross > 10000 and inv.client.uid %}<div class="uid">UID: {{ inv.client.uid }}</div>{% endif %}
{% endif %}
</div>
</div>
<div class="main">
<div class="masthead">
<div class="brand">
<div class="name">{{ inv.unit.name }}</div>
<div class="sub">{{ inv.unit.subtitle }}</div>
</div>
<div class="art">
{% for a in masthead_art(inv.unit) %}
<img class="{{ a.cls }}" src="{{ asset_uri(a.src) }}" alt="" style="width:{{ a.w }}mm; height:{{ a.h }}mm">
{% endfor %}
</div>
</div>
<div class="title">RECHNUNG für Unterkunft&nbsp;&nbsp;&nbsp;{{ inv.number }}</div>
<div class="date">{{ inv.unit.place ~ ', am' if inv.unit.place else 'Datum:' }} {{ inv.date|date }}</div>
<table class="lines">
<thead><tr>
<th>Name / Zeitraum</th><th class="n">Anz</th><th>Einheit</th><th class="n">Brutto/Einh.</th>
<th class="n">Netto/Einh.</th><th class="n">Summe Netto</th><th class="n">USt</th>
<th class="n">Summe Brutto</th><th class="n">Ortstaxe</th>
</tr></thead>
<tbody>
{% for ln in inv.lines %}
<tr>
{% if ln.kind == 'storno' %}
<td class="nm"><div class="wn">{{ ln.worker }}</div>Stornogebühr – nicht steuerbarer Schadenersatz{% if ln.period_from %} · {{ ln.period_from|date }}{% endif %}</td>
<td class="n">{{ ln.quantity|num(0) if (ln.quantity or 0) % 1 == 0 else ln.quantity|num(1) }}</td><td>{{ units_de.get(ln.unit_type, ln.unit_type) }}</td>
<td class="n">{{ ln.gross_per_unit|euro }}</td><td class="n">—</td>
<td class="n">—</td><td class="n">—</td>
<td class="n">{{ ln.line_gross|euro }}</td><td class="n"></td>
{% else %}
<td class="nm"><div class="wn">{{ ln.worker }}</div>{{ ln.period_from|date }}{% if ln.period_to %} – {{ ln.period_to|date }}{% endif %}</td>
<td class="n">{{ ln.quantity|num(0) if (ln.quantity or 0) % 1 == 0 else ln.quantity|num(1) }}</td><td>{{ units_de.get(ln.unit_type, ln.unit_type) }}</td>
<td class="n">{{ ln.gross_per_unit|euro }}</td><td class="n">{{ ln.net_per_unit|num }}</td>
<td class="n">{{ ln.line_net|num }}</td><td class="n">{{ ln.line_vat|num }}</td>
<td class="n">{{ ln.line_gross|euro }}</td>
<td class="n">{% if ln.tourism_tax %}{{ ln.tourism_tax|euro }}{% endif %}</td>
{% endif %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="bottom">
<table class="totals">
<tr><td class="l">{% if inv.storno %}Steuerbarer Nettobetrag{% else %}Nettobetrag{% endif %}</td><td class="v">{{ inv.net|euro }}</td></tr>
{% for b in inv.vat_breakdown %}<tr><td class="l">USt {{ b.pct }} %</td><td class="v">{{ b.vat|euro }}</td></tr>{% endfor %}
{% if inv.storno %}<tr><td class="l">Stornogebühr (nicht steuerbar)</td><td class="v">{{ inv.storno|euro }}</td></tr>{% endif %}
{% if inv.tourism_tax %}<tr><td class="l">Tourismusabgabe</td><td class="v">{{ inv.tourism_tax|euro }}</td></tr>{% endif %}
<tr class="grand"><td class="l">Rechnungsbetrag</td><td class="v">{{ inv.gross|euro }}</td></tr>
</table>
<div class="note">{{ inv.notes or t('invoice_note_default') }}</div>
<div class="thanks">Vielen Dank!</div>
</div>
<footer>
<div class="col"><b>Anschrift:</b>{{ inv.unit.name }}<br>{{ inv.unit.subtitle }}<br>{{ (inv.unit.address or '')|e|replace('\n', '<br>'|safe) }}</div>
<div class="col"><b>Bankverbindung:</b>{{ inv.unit.bank }}<br>IBAN: {{ inv.unit.iban }}<br><br>UID: {{ inv.unit.uid }}</div>
{% if inv.unit.contact %}<div class="col"><b>Kontakt:</b>{{ inv.unit.contact|e|replace('\n', '<br>'|safe) }}</div>{% endif %}
</footer>
</div>
</body>
</html>