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:
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.
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 Sale | free | Freely available, no allotment limit applied. |
| Open Sale | open | Open for reservations (a no-op override that matches the default open state). |
| On Request | request | Available on approval only. |
| Stop Sale | stop | Closed — no new reservations for this calendar. |
| Blocked | block | Hard 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:
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:
| Marking | Meaning |
|---|---|
| No fill | Open / available |
| Cyan | Free Sale |
| Orange | On Request |
| Red | Stop Sale / Closed |
| Dark slate | Blocked |
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:
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:
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.
/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):
| Filter | Example | Effect |
|---|---|---|
| Date window | ?from=2026-08-01&to=2026-08-31 | Restrict the range (default: next 365 days). |
| Granularity | ?granularity=spans | Collapse identical consecutive days into ranges — far smaller for large portfolios. Default daily. |
| Country | ?country=DE,ES | Filter properties by ISO country code. |
| Property | ?property_id=42,43 | Restrict to specific properties. |
| Master pull | ?calendar=master | Force the Master feed (operators need an active partnership with stop-sale sync). |
| Reasons | ?include=reasons | Include day-status reason codes (operators: only when the contract term allows it). |
| By kind / id (acc. only) | ?kind=connected · ?calendar_id=25 | Narrow 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.
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.
| Legacy | Allotment Calendar |
|---|---|
One state per room/day (color 0–4) | Quantities (allotment / closed / confirmed) + optional status override |
| Single calendar per property | Master + one sub-calendar per operator/channel |
/api/v1/stop-sale/{token}/ | /api/v1/allotment-stop-sale/{token}/ |
| No pool arithmetic | Shared 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.