Home Modules Allotment Calendar
Module

Allotment Calendar

Quantity-based, per-operator availability. Every property has a Master calendar backed by its real room inventory, plus any number of sub-calendars — one per tour operator or channel — each with its own allotment, day statuses, and a live open → on request → closed cascade.

Replaces the legacy Stop Sale module

The Allotment Calendar is the successor to the original single-state Stop Sale calendar. The legacy module and its endpoints (/api/stop-sale/…, /api/v1/stop-sale/{token}/) remain frozen and fully supported so existing integrations keep working, but all new work should target the Allotment Calendar and its /api/v1/allotment-stop-sale/{token}/ export. See Migration from legacy.

Overview

Where the legacy calendar tracked a single availability state per room per day, the Allotment Calendar tracks quantities and distributes them across operators. The model has three layers:

  • Physical pool — each room type's real unit count (from the property fact sheet, Room.room_total_units). This is the hard ceiling; the platform never lets the sum of everything held exceed it.
  • Calendars — a Master calendar plus sub-calendars. Each sub-calendar holds an allotment (how many units that operator may sell) and records daily closed and confirmed counts.
  • Day statuses — an optional manual override (Free / Open / On Request / Stop / Blocked) that sits on top of the numeric cascade for any room and day.

Calendar Types

Every calendar has a kind that determines where its allotment comes from and whether it consumes the physical pool:

Kind Allotment source Consumes pool? API access Use for
Master
master
Property fact sheet (room totals). Auto-created, one per property, not editable as a calendar. Yes Exported to operators as the general/fallback feed The hotel-wide baseline every operator sees.
Connected
connected
Manual allotment blocks (or a linked rate contract, if the operator has one). Yes Yes — the bound tour operator pulls it via their token An operator you have an accepted partnership contract with.
Contracted
contracted
Authoritative from a Pricing Engine rate contract (contract → room → allotment). Manual blocks are rejected. Yes Yes — via the operator's token An operator with a signed rate/allotment contract in the Pricing Engine.
Specific Allotment
allotment
Manual allotment blocks. Yes No — freetext counterparty, internal only An operator or channel with no contract on the platform yet.
Standalone
standalone
None — carries day statuses only, no quantities. No No A legacy-style state-only calendar. Not recommended.

The Master Calendar & Physical Pool

The Master calendar is created automatically for every property and its total comes straight from the fact sheet. It is the shared physical pool that all pool-consuming calendars draw from. On any room and date:

text
physical_remaining = room_total_units
                   - SUM(closed + confirmed) over ALL pool-consuming calendars
                   - PMS-ingested units (reserved + checked-in + out-of-order)

Both closed and confirmed units are held out of circulation, so a sub-calendar closing five units immediately reduces what the Master (and every other operator) can still sell. Standalone calendars are the one exception — they never touch the pool.

PMS & channel-manager inventory

Units booked in a connected property-management system (Mews, Apaleo, Cloudbeds, Opera, …) are ingested as day inventory and deducted from the pool alongside your in-app allotments, so the remaining figure reflects the true live picture.

Creating a Sub-Calendar

From the calendar page, pick a property and choose New Calendar. The form fields depend on the kind you select:

Field Applies to Description
name All A label for the calendar (e.g. the operator or channel name).
kind All Connected, Contracted, Specific Allotment, or Standalone (Master is auto-managed).
counterparty_contract Connected The accepted partnership contract this calendar is bound to. Required for Connected.
rate_contract Contracted The Pricing Engine rate contract that feeds allotment authoritatively.
external_name, external_details Specific Allotment Freetext identity for an operator/channel not (yet) on the platform.
mailing_group All (optional) A mailing list this calendar's PDF is sent to via Send Partner.

Uniqueness rules the backend enforces:

  • Exactly one Master calendar per property.
  • At most one Connected calendar per property per partnership contract.
  • At most one Contracted calendar per property per rate contract.
  • Free-tier accounts are capped at 5 calendars per property; the paid API/premium plan lifts this.

Allotment Blocks

A calendar's allotment is defined as blocks — a quantity of a room type over a date range (a season). Daily consumption (closed / confirmed) is tracked separately, so a block is a compact definition rather than a row per day.

text
AllotmentBlock
  room        -> which room type
  start_date  -> season start
  end_date    -> season end
  quantity    -> units allotted to this calendar for the range
  source      -> manual | contract (Pricing Engine)
  • Blocks for the same room on one calendar cannot overlap — start the next block the day after the previous one ends.
  • On Contracted calendars (and Connected calendars whose operator has an active rate contract) allotment is pulled authoritatively from the Pricing Engine and manual blocks are rejected. Re-pull with POST calendars/{id}/pull-contract/; it is idempotent (it clears and rebuilds blocks from the current contract rows).
  • Manual allotment is only permitted as a fallback when a rate contract exists but has no usable room/allotment rows. The server re-checks this on every block write, so the rule cannot be bypassed from the UI.

Day Statuses & Reasons

On top of the numbers, any room and day can carry a manual status override. When set, it dictates the displayed and synced state; when absent, the numeric cascade drives the day.

Status Code Meaning
Free SalefreeFreely available, no allotment limit applied.
Open SaleopenOpen for reservations (a no-op override that matches the default open state).
On RequestrequestAvailable on approval only.
Stop SalestopClosed — no new reservations for this calendar.
BlockedblockHard block. On the Master calendar this is hotel-wide and propagates to every operator.

On Request, Stop Sale, and Blocked can carry a reason, and any status can carry a freetext note (internal only — a note is never exposed to a tour operator). Available reasons:

renovation · group_hold · allotment_expired · hotel_event · force_majeure · peak_exclusion

The Availability Cascade

For each room and day the platform resolves one operator-facing state, applying overrides in order: hotel-wide Master status → this calendar's own status → the numeric cascade. The numeric part is:

text
own_available = max(0, allotment - closed - confirmed)

own_available > 0                          -> OPEN
own_available == 0 and pool remaining > 0   -> ON_REQUEST
physical_remaining == 0                     -> CLOSED

Operators always receive one of three stable states — open, on_request, or closed — regardless of the internal status names, so an integration can rely on a fixed enum.

Shared Inventory

Directional share rules let one room type lend spare capacity to another. When the recipient room is fully consumed, it may borrow up to max_units from a donor room's live remaining pool — so the hotel total still never exceeds fact-sheet capacity. While borrowing applies, the recipient shows On Request (or Open, if the rule opts for it) instead of Stop Sale. Rules are managed from the Shared Inventory dialog on the calendar page.

Editing: Paint, Buffer & Save

  • Click a cell to set its closed/confirmed counts, a status, a reason, and a note for that room and day.
  • Paint a status across a range by selecting it from the palette and dragging across cells.
  • Staged saves. Edits are buffered in the browser and are not sent to the server until you press Save, which submits the whole batch atomically in one request. The Save button stays disabled while there are no pending changes, and navigating away with unsaved edits warns you first.
  • Past dates are locked. Days before today cannot be edited; a past-dated day inside a batch is silently skipped.

Allocation Consumed

Opening a day cell shows an Allocation Consumed breakdown — for that room and date it lists the total units and exactly who is holding them: each operator calendar with its allotment, closed and confirmed counts, plus PMS reservations per source, ending with what remains. It answers “the hotel has 20 triples — where did they go?” and always agrees with the grid's remaining figure.

Timeline

The Timeline (Beta) page is a week-by-week, property-wide view per room type: derived allocation bars per operator, real reservation spans from the rooming ledger (positioned by night and colored by status), and PMS day inventory. Use it so a reservations desk can see, at a glance, allocation used per operator and per channel across a date window.

Export & Distribution

  • PDF — download the calendar you are currently viewing as a formatted availability chart.
  • Email — send the PDF to a mailing list from the app.
  • Send Partner — email the PDF to the mailing group associated with the calendar in one click.
  • API — operators pull live data via their token; see below.

All three always export the active calendar — the one open on screen. A chart sent to a tour operator therefore contains that operator's calendar, never the master pool, unless the master calendar is the one you are viewing. The calendar's name and kind are printed under the hotel name on every chart so the recipient can tell which one they received.

The chart is branded as your document, not ours: the header carries the property's own logo, falling back to your company logo if the property has none. If neither is set — or the stored logo file is no longer reachable — the header simply shows the property name. Logos must be a raster image (PNG or JPG); SVG logos cannot be rendered into a PDF and will be skipped.

What the chart covers

The PDF always covers 12 whole months starting from the month you are viewing, regardless of the shorter window shown on screen — partners expect a year of forward availability. Each month is drawn as its own block with the day numbers running across.

Colour marks the exceptions only. Days that are simply open are left unmarked, so a restricted period stands out at a glance:

MarkingMeaning
No fillOpen / available
CyanFree Sale
OrangeOn Request
RedStop Sale / Closed
Dark slateBlocked

The chart reads from the same resolved availability the on-screen grid uses, so a day's colour in the PDF always matches the app — including the hotel-wide master override and the shared-inventory cascade. A summary panel on the first page lists the room types, total units from the fact sheet, months covered and how many days carry a marking.

Internal Management API

The in-app calendar UI is backed by session-authenticated endpoints under /api/allotment/. These are the same calls a first-party integration would use.

Method Path Description
GET / POST/api/allotment/calendars/?property={id}List calendars (auto-creates the Master) / create a sub-calendar.
GET / PATCH / DELETE/api/allotment/calendars/{id}/Retrieve, edit, or delete a calendar (Master is immutable).
GET / POST/api/allotment/calendars/{id}/blocks/List / create allotment blocks (rejected on contract-fed calendars).
PATCH / DELETE/api/allotment/blocks/{id}/Edit or remove a block.
POST/api/allotment/calendars/{id}/pull-contract/Re-pull allotment from the Pricing Engine rate contract (idempotent).
POST/api/allotment/calendars/{id}/daily/Upsert a single day's counts/status.
POST/api/allotment/calendars/{id}/daily/batch/Atomic multi-day save (the buffered Save button).
POST/api/allotment/calendars/{id}/bulk/Apply a status/count across a date range and set of rooms.
GET/api/allotment/calendars/{id}/grid/?from=&to=Computed availability cascade per room/day (the calendar view).
GET/api/allotment/calendars/{id}/day-breakdown/?room=&date=Per-day consumption of one room, broken down by operator + PMS source.
GET/api/allotment/calendars/{id}/timeline/?from=&to=Property-wide allocation bars + rooming reservation spans + PMS bars.
GET/api/allotment/calendars/{id}/pdf/?from=&to=Download the calendar PDF. Both params optional; to defaults to 12 months after from, and the chart is capped at 12 whole months.
GET/api/allotment/calendars/{id}/pdf/send-mail/{list_id}/Email the PDF to a mailing list.
GET/api/allotment/calendars/{id}/send-partner/Email the PDF to the calendar's associated mailing group.
GET/api/allotment/property/{property_id}/pdf/?from=&to=Download the chart for a property without knowing its calendar id — resolves that property's Master (Inclusive) calendar. Used by the Export Centre.
GET/api/allotment/property/{property_id}/pdf/send-mail/{list_id}/Email the property's Master chart to a mailing list.
GET/api/allotment/partner-contracts/?property={id}Accepted partnership contracts available to bind a Connected calendar.
GET/api/allotment/rate-contracts/?property={id}Rate contracts available to bind a Contracted calendar.
GET / POST / PATCH / DELETE/api/allotment/shares/Manage directional shared-inventory rules.

Example — the computed grid for a two-week window:

json
GET /api/allotment/calendars/1/grid/?from=2026-08-01&to=2026-08-14

{
  "calendar_id": 1,
  "kind": "master",
  "from": "2026-08-01",
  "to": "2026-08-14",
  "rooms": [
    {
      "room_id": 2,
      "room_name": "Triple Room",
      "total_units": 20,
      "days": {
        "2026-08-02": {
          "state": "on_request",
          "status": null,
          "own_available": 0,
          "base": 20,
          "closed": 0,
          "confirmed": 0,
          "external": 0,
          "physical_remaining": 4
        }
      }
    }
  ]
}

Example — who is holding a room on one day:

json
GET /api/allotment/calendars/1/day-breakdown/?room=2&date=2026-08-02

{
  "room_name": "Triple Room",
  "date": "2026-08-02",
  "total_units": 20,
  "consumed_total": 16,
  "physical_remaining": 4,
  "internal": [
    { "label": "RS",  "kind": "allotment", "allotment": 5,  "closed": 2, "confirmed": 3, "held": 5 },
    { "label": "TUI", "kind": "allotment", "allotment": 5,  "closed": 2, "confirmed": 0, "held": 2 },
    { "label": "tui@operator.com", "kind": "connected", "allotment": 10, "closed": 9, "confirmed": 0, "held": 9 }
  ],
  "external": []
}

Partner Export API

Tour operators and channel managers consume the calendars over the token-based v1 export. One URL serves both sides; the response shape switches on the token owner's account type.

GET /api/v1/allotment-stop-sale/{token}/
  • Accommodation token → your own Master + every sub-calendar with full counts (allotment, closed, confirmed, available, PMS-external, physical remaining) — the PMS / channel-manager output feed.
  • Tour-operator token → your contracted portfolio: the calendar bound to you (connected, else rate-contract), falling back to the Master. Day statuses arrive as a stable enum (open / on_request / closed) plus your own mapped codes.

Common filters (compose with AND):

FilterExampleEffect
Date window?from=2026-08-01&to=2026-08-31Restrict the range (default: next 365 days).
Granularity?granularity=spansCollapse identical consecutive days into ranges — far smaller for large portfolios. Default daily.
Country?country=DE,ESFilter properties by ISO country code.
Property?property_id=42,43Restrict to specific properties.
Master pull?calendar=masterForce the Master feed (operators need an active partnership with stop-sale sync).
Reasons?include=reasonsInclude day-status reason codes (operators: only when the contract term allows it).
By kind / id (acc. only)?kind=connected · ?calendar_id=25Narrow to a kind or a single calendar.

Response format

JSON by default; send Accept: application/xml for XML. Appending ?format=json or ?format=xml forces the format regardless of the Accept header — handy because a browser address bar negotiates to XML.

The full request/response reference, per-persona payload examples, and error behaviours live in the API Reference.

Sub-User Access

Sub-users need the stop_sales permission flag on their UserPermission to reach the calendar endpoints. Account managers always have access. Sub-user visibility can be further scoped by property.

python
if not user.is_manager():
    perms = UserPermission.objects.filter(user=user).first()
    if not perms or not perms.stop_sales:
        return Response({'message': 'Not permitted.'}, status=403)

Migration from Legacy

The legacy Stop Sale module (a single Event.color state per room/day) stays frozen and keeps running so existing integrations are never broken. The Allotment Calendar uses entirely separate tables and endpoints, so the two run side by side during the transition.

LegacyAllotment Calendar
One state per room/day (color 0–4)Quantities (allotment / closed / confirmed) + optional status override
Single calendar per propertyMaster + one sub-calendar per operator/channel
/api/v1/stop-sale/{token}//api/v1/allotment-stop-sale/{token}/
No pool arithmeticShared physical pool with PMS-aware remaining

New integrations should target /api/v1/allotment-stop-sale/{token}/. The legacy export remains available until the new module is fully stabilised.