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

76 lines
3.7 KiB
Python

"""Quarterly Umsatzsteuer-Voranmeldung list, computed from the Zimmerbelegung.
- A receipt belongs to the quarter it was PAID in (Ist-Besteuerung), not the one it was lived or invoiced
in. A row paid in several goes counts each payment on its own day; otherwise paid = the same rule the
Zimmerbelegung shows (`repo._payment`): the row's own date, else the date its invoice was paid.
- Only the Betrag counts. The Ortstaxe is a durchlaufender Posten and never appears; payments cover the
Betrag first (175,00 paid of 175,00 Betrag + 16,00 Ortstaxe: the Betrag is in, the Ortstaxe open).
- One Betrieb at a time — each unit files separately. A Betrieb includes its predecessors.
- Everything is 10 % USt: Beherbergung, and the Garçonnières' residential rent alike. A Storno is nicht
steuerbar (`repo.is_storno`): listed, because the money came in, but kept out of the base.
"""
import datetime
from app import db, repo
VAT_RATE = 0.10
def previous_quarter(today):
"""The UVA is filed for the quarter that has just ended."""
q = (today.month - 1) // 3 # 0 = we are in the 1st quarter → last year's 4th
return (today.year, q) if q else (today.year - 1, 4)
def _bounds(year, quarter):
first = datetime.date(year, 3 * quarter - 2, 1)
after = datetime.date(year + 1, 1, 1) if quarter == 4 else datetime.date(year, 3 * quarter + 1, 1)
return first.isoformat(), (after - datetime.timedelta(days=1)).isoformat()
def receipts(row):
"""When this row's money came in: [(date, part of the Betrag, part of the Ortstaxe)].
Recorded payments count one by one, each on its own day — an invoice paid half in May and half in
October belongs to two quarters. They cover the Betrag first and the Ortstaxe last. Without payments, a
paid row came in whole on its 'bezahlt am'."""
if row["payments"]:
out, left, taxe = [], row["amount"] or 0, row["tourism_tax"] or 0
for on, amount in row["payments"]:
part = round(min(amount, left), 2)
tax = round(min(amount - part, taxe), 2)
left, taxe = left - part, taxe - tax
if part > 0 or tax > 0:
out.append((on, part, tax))
return out
if row["payment"] in ("paid", "cash") and row["paid_on"]:
return [(row["paid_on"], row["amount"] or 0, row["tourism_tax"] or 0)]
return []
def units(conn):
return db.rows(conn, "SELECT code, name FROM unit WHERE is_current=1 ORDER BY id")
def quarterly_report(conn, unit_code, year, quarter):
first, last = _bounds(year, quarter)
codes = {r["code"] for r in db.rows(conn, "SELECT code FROM unit WHERE code=? OR succeeded_by_id="
"(SELECT id FROM unit WHERE code=?)", (unit_code, unit_code))}
rooms = {}
for r in repo.ledger_rows(conn):
if r["unit"] not in codes:
continue
for on, amount, _ in receipts(r):
if amount and first <= on <= last:
rooms.setdefault(r["room"] or "", []).append(
dict(r, received_on=on, received=amount, storno=repo.is_storno(r)))
sections = [{"room": room, "rows": sorted(rows, key=lambda x: (x["received_on"], x["charge_id"])),
"sum": round(sum(x["received"] for x in rows), 2)}
for room, rows in sorted(rooms.items())]
total = round(sum(s["sum"] for s in sections), 2)
storno = round(sum(x["received"] for s in sections for x in s["rows"] if x["storno"]), 2)
gross = round(total - storno, 2)
net = round(gross / (1 + VAT_RATE), 2)
return {"first": first, "last": last, "sections": sections, "total": total, "storno": storno,
"gross": gross, "net": net, "vat": round(gross - net, 2)}