Skip to content

Verwendung Operations Report Templates

Alexander Elchlepp edited this page Jun 25, 2026 · 3 revisions

Operations Report Templates

This page documents operations-specific template data. General editor behavior is documented here:

Purpose

Operations report templates generate PDF output (e.g. housekeeping, frontdesk, meals, management summaries) from the selected date range, subsidiary, and occupancy filters.

Root Variables

  • filters (array)
  • rangeView (array)
  • dayViews (array, alias of rangeView.dayViews)
  • reservations (array, alias of rangeView.reservations)
  • occupancyLabels (array)
  • statusLabels (array)
  • invoiceStatusLabels (array)
  • simple (array)
  • statistics (array, optional; only added if template text contains statistics.)
  • touristTax (array, optional; only added if template text contains touristTax)

Why simple Exists

simple keeps complex logic in PHP and provides template-safe, flat data for visual editing (data-repeat, data-if).

simple Structure

simple.meta

  • simple.meta.periodLabel (string, d.m.Y or d.m.Y - d.m.Y)
  • simple.meta.generatedAt (string, d.m.Y H:i)
  • simple.meta.subsidiaryName (string|null, concrete subsidiary name when a subsidiary is selected)
  • simple.meta.subsidiaryAllLabelKey (translation key; default: housekeeping.subsidiary.all)
  • simple.meta.occupancyTypeLabelKeys (string[])
  • simple.meta.dayKey (string, Y-m-d)

simple.days

Shared day list for week-like tables.

simple.days[]:

  • day.date (DateTimeImmutable)
  • day.dateKey (string, Y-m-d)
  • day.label (string, d.m.)

simple.reservations

Normalized reservation rows reused across reports.

simple.reservations[]:

  • row.roomNumber (string)
  • row.roomLabel (string)
  • row.guest (string)
  • row.persons (int|string)
  • row.start (DateTimeImmutable)
  • row.end (DateTimeImmutable)
  • row.period (string)
  • row.frontdeskSectionKey (string|null)
  • row.invoiceStatusLabelKey (string|null)
  • row.apartmentId (int|null)

simple.views

Report-specific shaped data based on the shared structures above.

simple.views.housekeepingDay

  • simple.views.housekeepingDay.date (DateTimeImmutable)
  • simple.views.housekeepingDay.rows[] with:
  • row.date (string, Y-m-d)
  • row.room (string)
  • row.occupancyLabelKey (string)
  • row.guests (int|string)
  • row.reservationSummary (string)
  • row.statusLabelKey (string|null)
  • row.assignedTo (string)
  • row.note (string)

simple.views.housekeepingWeek

  • simple.views.housekeepingWeek.rows[]
  • row.apartmentLabel (string)
  • row.cells[] with:
  • cell.occupancyLabelKey (string)
  • cell.statusLabelKey (string)
  • cell.guests (string)

simple.views.frontdesk

  • simple.views.frontdesk.sections[]
  • section.titleKey (translation key)
  • section.rows[] with:
  • row.guest (string)
  • row.persons (int|string)
  • row.room (string)
  • row.period (string)
  • row.invoiceStatusLabelKey (string|null)

simple.views.mealsChecklist

  • simple.views.mealsChecklist.rows[]
  • row.room (string)
  • row.guest (string)
  • row.cells (string[])

Other Root Variables

filters

  • filters.start (DateTimeImmutable)
  • filters.end (DateTimeImmutable)
  • filters.subsidiary (Subsidiary|null)
  • filters.occupancyTypes (string[])
  • filters.template (Template, export only)
  • filters.subsidiaryId (string, e.g. "all")

rangeView

Built by HousekeepingViewService::buildRangeView():

  • rangeView.start (DateTimeImmutable)
  • rangeView.end (DateTimeImmutable)
  • rangeView.days (DateTimeImmutable[])
  • rangeView.apartments (Appartment[])
  • rangeView.reservations (Reservation[])
  • rangeView.dayViews (array<string, DayView>)

DayView rows include:

  • row.apartment (Appartment)
  • row.occupancyType (FREE|STAYOVER|ARRIVAL|DEPARTURE|TURNOVER)
  • row.guestCount (int|null)
  • row.reservationSummary (string|null)
  • row.status (RoomDayStatus|null)
  • row.apartmentReservations (Reservation[])

Label Maps

  • occupancyLabels: occupancy type value -> translation key
  • statusLabels: housekeeping status value -> translation key
  • invoiceStatusLabels: invoice status value -> translation key

statistics (optional)

Only present when template text contains statistics..

  • statistics.range.start (DateTimeImmutable)
  • statistics.range.end (DateTimeImmutable)
  • statistics.countryNames (array<string, string>, locale-resolved ISO-alpha-2 → country name)
  • statistics.months[]:
  • entry.year (int)
  • entry.month (int)
  • entry.metrics (array)
  • entry.warnings (string[])

entry.metrics.tourism includes (in addition to the country/state buckets):

  • metrics.tourism.overnights_by_guest_category (array<int, int>, keyed by GuestCategory.id → overnight count)
  • metrics.tourism.overnights_by_country_and_category (array<string, array<int, float>>, keyed by ISO country code → category id → overnight count; fractional because category counts are split proportionally across linked customers' addresses)

touristTax (optional)

Only present when template text contains touristTax. Live-calculated via TouristTaxService::calculateForReservation() — the same source of truth as the invoice positions and the booking journal. If the hotelier changes a tariff or report-group setting, the next report render shows the new values automatically.

  • touristTax.range.start (DateTimeImmutable)
  • touristTax.range.end (DateTimeImmutable)
  • touristTax.guestCategories[] — active guest categories for the selected subsidiary:
  • category.id (int)
  • category.name (string)
  • category.statisticalGroup (string: ADULT|CHILD|INFANT|OTHER)
  • category.isCountedInOccupancy (bool)
  • category.sortOrder (int)
  • touristTax.months[] — one entry per month inside the reporting range:
  • entry.year (int)
  • entry.month (int)
  • entry.taxes — keyed by TouristTax.id, only contains taxes that are active and valid within this month (sorted by sortOrder):
  • tax.taxId (int)
  • tax.name (string)
  • tax.sortOrder (int)
  • tax.appliesOnlyToAdult (bool) — when true, only categories with statisticalGroup === ADULT contribute
  • tax.calculationMode (string: per_night_flat|percent_per_room)
  • tax.percentageRate (float|null) — only for percent modes
  • tax.percentageBase (string|null: net|gross) — only for percent modes
  • tax.totalNights (int) — sum across all groups
  • tax.totalAmount (float) — money total
  • tax.groups[] — one row per report group (multiple TouristTaxRate entries sharing the same reportGroup text are merged here; rates without a reportGroup fall back to one row per GuestCategory):
  • group.label (string) — reportGroup if set, otherwise the category name
  • group.reportGroup (string|null) — raw report-group key, useful for debugging
  • group.pricePerNight (float)
  • group.pricePerNightConsistent (bool) — false when merged rates have differing tariffs (signals a config issue)
  • group.totalNights (int)
  • group.totalCount (int)
  • group.totalAmount (float)
  • group.categoryIds (int[]) — which GuestCategory.ids ended up in this group

Example Snippets (Easy Mode)

Shared header

<div class="header">
  <div style="text-align:center; color:#666; font-size:11px;">
    [[ simple.meta.periodLabel ]]
    <span data-repeat="simple.meta.occupancyTypeLabelKeys" data-repeat-as="type">
      | [[ occupancyLabels[type]|trans({}, 'Housekeeping') ]]
    </span>
    | [[ simple.meta.subsidiaryName ?: (simple.meta.subsidiaryAllLabelKey|trans({}, 'Housekeeping')) ]]
    | [[ simple.meta.generatedAt ]]
  </div>
</div>

Housekeeping week table

<table>
  <tr>
    <th>Room</th>
    <th data-repeat="simple.days" data-repeat-as="day">[[ day.label ]]</th>
  </tr>
  <tr data-repeat="simple.views.housekeepingWeek.rows" data-repeat-as="row">
    <td>[[ row.apartmentLabel ]]</td>
    <td data-repeat="row.cells" data-repeat-as="cell">
      [[ cell.occupancyLabelKey ? (cell.occupancyLabelKey|trans({}, 'Housekeeping')) : '' ]]
    </td>
  </tr>
</table>

Frontdesk sections

<div data-repeat="simple.views.frontdesk.sections" data-repeat-as="section">
  <h3>[[ section.titleKey|trans ]]</h3>
  <table>
    <tr data-repeat="section.rows" data-repeat-as="row">
      <td>[[ row.guest ]]</td>
      <td>[[ row.room ]]</td>
      <td>[[ row.period ]]</td>
      <td>[[ row.invoiceStatusLabelKey ? (row.invoiceStatusLabelKey|trans) : '-' ]]</td>
    </tr>
    <tr data-if="section.rows is empty"><td colspan="4">-</td></tr>
  </table>
</div>

Tourist tax — monthly report block

Pulls the taxes that are active and valid for the selected month, then renders one section per tax with its report-group rows. pricePerNightConsistent is a soft warning when multiple rates were merged into the same report group but carry different tariffs.

[% set selectedYear = filters.start|date('Y') %]
[% set selectedMonth = filters.start|date('n') %]
[% set currentTaxMonth = touristTax.months|filter(m => m.year == selectedYear and m.month == selectedMonth)|first %]
[% set currentTaxes = currentTaxMonth ? currentTaxMonth.taxes : [] %]

<div data-repeat="currentTaxes" data-repeat-as="tax">
  <h3>[[ tax.name ]]</h3>
  <table>
    <thead>
      <tr>
        <th>Nights</th>
        <th>Guests</th>
        <th>Rate per night</th>
        <th>Total</th>
      </tr>
    </thead>
    <tbody>
      <tr data-repeat="tax.groups" data-repeat-as="group">
        <td>[[ group.totalNights ]]</td>
        <td>
          [[ group.label ]]
          <span data-if="not group.pricePerNightConsistent">⚠ inconsistent rates</span>
        </td>
        <td>[[ group.pricePerNight|number_format(2, '.', '') ]]</td>
        <td>[[ group.totalAmount|number_format(2, '.', '') ]]</td>
      </tr>
      <tr data-if="tax.groups is empty"><td colspan="4">No bookings.</td></tr>
      <tr data-if="tax.groups is not empty">
        <td>[[ tax.totalNights ]]</td>
        <td colspan="2">Total</td>
        <td>[[ tax.totalAmount|number_format(2, '.', '') ]]</td>
      </tr>
    </tbody>
  </table>
</div>
<div data-if="currentTaxes is empty">No active tourist tax for the selected month.</div>

Overnights by country × guest category

Uses the snapshot bucket statistics.months[].metrics.tourism.overnights_by_country_and_category together with the locale-resolved country names. Values are fractional because category counts are split proportionally across the linked customers' countries; round in the template as needed.

[% set currentStats = statistics.months|filter(m => m.year == selectedYear and m.month == selectedMonth)|first %]
[% set countryAndCategory = currentStats.metrics.tourism.overnights_by_country_and_category ?? {} %]

<table>
  <thead>
    <tr>
      <th>Country</th>
      <th data-repeat="touristTax.guestCategories" data-repeat-as="category">[[ category.name ]]</th>
    </tr>
  </thead>
  <tbody>
    <tr data-repeat="countryAndCategory" data-repeat-key="countryCode" data-repeat-as="rowData">
      <td>[[ statistics.countryNames[countryCode|upper] ?? countryCode ]]</td>
      <td data-repeat="touristTax.guestCategories" data-repeat-as="category">
        [[ (rowData[category.id] ?? 0)|round(0) ]]
      </td>
    </tr>
  </tbody>
</table>

Clone this wiki locally