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>
76 lines
3.7 KiB
Python
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)}
|