Skip to content

Calendar Closure — Observer Guide

How to set up a calendar closure, run the supply pipeline, and read everything ForecastAI shows you about what happened.


Table of Contents

  1. What Was Set Up (the Test Case)
  2. Step 1 — Run the Supply Pipeline
  3. Step 2 — Observe the Calendar in the Database
  4. Step 3 — Find the Pre-Position Order in the UI
  5. Step 4 — Read the Inventory Projection
  6. Step 5 — Inspect the Pegging Tree
  7. Step 6 — Read the Process Log
  8. How to Add Your Own Calendar Closure
  9. Rolling Back the Test

1. What Was Set Up

Item: Chain Lube Finish Line 500ml (item_id = 1)
Route: Central Warehouse Brussels (site 100) → France Country Warehouse (site 201)
Route id: 29, lead time = 3 weeks
Calendar: Brussels Hub Closure Aug-2026 (calendar_id = 11)
Closed weeks: 20, 21, 22, 23 (0-indexed from the planning horizon start)

Week 0 = current week. Week 20 = approximately 5 months out. The closure simulates a 4-week annual maintenance shutdown at the Brussels hub.

What the algorithm does:

Normal plan (4 weeks × 20 units = 80 units/week steady supply):

  Week:     ...  18   19  [20  21  22  23]  24  ...
  Brussels:      OPEN OPEN SHUT SHUT SHUT SHUT OPEN

Without closure awareness:  shortages in weeks 20-23.

With pull-forward:
  Week-19 demand = 20 (own week) + 80 (weeks 20-23 accumulated)
                 = 100 units pre-positioned in ONE delivery.
  Weeks 20-23: demand = 0 (already covered), no orders generated.
  France WH draws down from pre-positioned stock each week — no shortage.

2. Run the Supply Pipeline

In the ForecastAI UI, go to Processes and run:

Supply Planning

Select pipeline High Performance 2026 (pipeline_id = 2), then click Run.

Alternatively from the command line:

cd C:\allDev\ForecastAI2026.01\files
python run_pipeline.py --only supply --pipeline 2

Watch for these log lines:

[supply] _apply_calendar_pull_forward: pre-positioned demand in N plans (K prepos anchor keys)
[supply] Tagged N TRANSFER orders as TRANSFER_PREPOS

3. Observe the Calendar in the Database and UI

  1. Open Settings → Master Data → Calendars
  2. The calendar "Brussels Hub Closure Aug-2026" appears in the list
  3. Click it to open the detail panel
  4. Switch to Year View — the closed weeks (Oct 19 – Nov 15) are highlighted in red
  5. The mode shown is not_available — meaning the listed dates are the closed ones

This is the same CalendarManager component Mirabelle uses for all working calendars. The supply engine reads from exactly these entries at run time.

Via SQL — see which calendars exist

SELECT id, name, type, mode, description, created_at
FROM   master.calendar
ORDER  BY id;

Via SQL — see the closed date ranges

SELECT c.id, c.name, c.mode, ce.date_from, ce.date_to, ce.label
FROM   master.calendar c
JOIN   master.calendar_entry ce ON ce.calendar_id = c.id
WHERE  c.name = 'Brussels Hub Closure Aug-2026';

Via SQL — see which routes have a calendar assigned

SELECT
    r.id       AS route_id,
    i.name     AS item,
    ls.name    AS source_site,
    ld.name    AS dest_site,
    r.lead_time,
    c.name     AS calendar,
    c.mode     AS mode,
    c.id       AS calendar_id
FROM  master.route r
JOIN  master.item          i  ON i.id  = r.item_id
JOIN  master.location      ld ON ld.id = r.site_id
LEFT  JOIN master.location ls ON ls.id = r.source_site_id
LEFT  JOIN master.calendar  c ON c.id  = r.ship_calendar_id
WHERE r.ship_calendar_id IS NOT NULL
ORDER BY r.id;

How the supply engine converts dates to weeks

The engine reads the date ranges from master.calendar_entry and converts them to week indices at runtime using this logic:

week_index = floor((date - this_weeks_monday) / 7)

A week is considered closed if the closure date range overlaps ANY day in that week. No manual bitmask calculation needed — just enter human-readable dates in the CalendarManager.


4. Find the Pre-Position Order in the UI

In the Supply Planning screen

  1. Open Supply Planning from the left menu.
  2. Filter by item Chain Lube and site France Country Warehouse.
  3. In the Orders tab, look for an order with type Pre-Position (orange badge).

The pre-position order will have: - order_type = TRANSFER_PREPOS - arrival_week = 19 (one week before the closure starts) - qty ≈ 5× the normal weekly quantity (covers weeks 19-23)

Badge colours reminder

Badge colour Order type Meaning
Teal Replenish (TRANSFER) Normal inter-site transfer
Orange Pre-Position (TRANSFER_PREPOS) Pre-positioned ahead of a closure
Blue Procurement (BUY) Factory purchase
Amber Repair Returned unit repaired
Red Returns Customer return arriving
Yellow Expedite (BUY_EXPEDITE) Emergency purchase for shortage

Hover over the Pre-Position badge to see the tooltip:

"Pre-positioned ahead of a calendar closure window — sized to cover demand during the closed weeks"


5. Read the Inventory Projection

  1. Click into the Chain Lube / France Country Warehouse row.
  2. Open the Planning Grid panel.
  3. Scroll to weeks 18-25.

You should see:

Week | Opening | Incoming       | Demand | Closing
  18 |   40    | 20 (Replenish) |  20    |   40
  19 |   40    |100 (Pre-Pos)   |  20    |  120   ← large pre-position delivery
  20 |  120    |  0             |  20    |  100   ┐
  21 |  100    |  0             |  20    |   80   │  closure weeks —
  22 |   80    |  0             |  20    |   60   │  drawing down stock
  23 |   60    |  0             |  20    |   40   ┘
  24 |   40    | 20 (Replenish) |  20    |   40   ← normal resumes

No red cells in weeks 20-23. If you see shortages, check that the pipeline was re-run after the calendar was assigned.

Querying the projection directly

-- ClickHouse
SELECT
    arrival_week,
    order_type,
    qty,
    status
FROM PIPE_supply_orders
WHERE pipeline_id = 2
  AND item_id     = 1
  AND site_id     = 201
  AND arrival_week BETWEEN 17 AND 26
ORDER BY arrival_week;
-- Inventory projection
SELECT
    week,
    projected_inventory,
    demand,
    supply_received,
    shortage
FROM PIPE_supply_inventory_projection
WHERE pipeline_id = 2
  AND item_id     = 1
  AND site_id     = 201
  AND week BETWEEN 17 AND 26
ORDER BY week;

6. Inspect the Pegging Tree

The pre-position order covers multiple demand weeks. Click on it in the Orders panel to open the pegging tree.

You will see:

Pre-Position Order #XYZ  (100 units, arrives week 19)
├── Demand: France WH, week 19,  20 units
├── Demand: France WH, week 20,  20 units   ┐
├── Demand: France WH, week 21,  20 units   │ closure weeks
├── Demand: France WH, week 22,  20 units   │ all covered by
└── Demand: France WH, week 23,  20 units   ┘ this one order

This tells you exactly why the order was so large: it was planned to cover 5 weeks of demand in one delivery.


7. Read the Process Log

In the UI: Processes → Process Log, filter by the supply run.

Or directly:

SELECT step_name, status, rows_processed, log_tail, started_at
FROM   scenario.sys_process_log
WHERE  step_name LIKE '%supply%'
ORDER  BY started_at DESC
LIMIT  5;

Look for lines in log_tail containing:

_apply_calendar_pull_forward: pre-positioned demand in N plans
Tagged N TRANSFER orders as TRANSFER_PREPOS

8. How to Add Your Own Calendar Closure

Step 1 — Create the calendar in the UI

  1. Open Settings → Master Data → Calendars
  2. Click + New Calendar
  3. Fill in:
  4. Name: e.g. "Lyon Hub Summer Shutdown 2027"
  5. Type: working
  6. Mode: not_available ← this means the dates you enter are the CLOSED ones
  7. Description: optional
  8. Click Create

Step 2 — Add the closed date range

  1. In the calendar detail panel, click + Add Entry
  2. Enter From and To dates covering the closure window
  3. Optionally add a Label (e.g. "Annual maintenance")
  4. Click Save entries

The year view immediately shows the closed period in red.

Step 3 — Assign the calendar to a route

Currently route calendar assignment requires a direct SQL update (a route management UI is planned):

-- Find the calendar id
SELECT id FROM master.calendar WHERE name = 'Lyon Hub Summer Shutdown 2027';

-- Find the route id
SELECT r.id, i.name, ls.name AS source, ld.name AS dest, r.lead_time
FROM   master.route r
JOIN   master.route_type rt ON rt.id = r.type_id
JOIN   master.item         i  ON i.id  = r.item_id
JOIN   master.location     ld ON ld.id = r.site_id
LEFT JOIN master.location  ls ON ls.id = r.source_site_id
WHERE  rt.planning_type = 'TRANSFER'
ORDER  BY r.id;

-- Assign
UPDATE master.route
SET    ship_calendar_id = <calendar_id>
WHERE  id = <route_id>;

Step 4 — Re-run supply planning

The pull-forward and TRANSFER_PREPOS tagging happen automatically every time the supply pipeline runs. No bitmask computation or code changes needed.


9. Rolling Back the Test

To remove the test calendar and restore normal behaviour:

Via the UI: Settings → Master Data → Calendars → select "Brussels Hub Closure Aug-2026" → Delete.

Via SQL:

UPDATE master.route SET ship_calendar_id = NULL WHERE id = 29;
DELETE FROM master.calendar WHERE name = 'Brussels Hub Closure Aug-2026';
-- calendar_entry rows cascade automatically via FK ON DELETE CASCADE

Then re-run the supply pipeline. Week-19 will return to normal quantity. Pre-Position orders for this item will disappear.


10. Modelling a Supplier Closing for a Month Every Year

The calendar closure system works for any route that has a ship_calendar_id — including BUY routes that source from external suppliers. This means you can model a supplier's annual shutdown (e.g., a factory that closes every August for maintenance) in exactly the same way as an internal warehouse closure.

Why Use Calendar Closure Instead of Zero Capacity

You might think: "just set the supplier's capacity to 0 for those weeks." That works for partial reductions, but has two drawbacks for a full closure:

  1. No pre-positioning — zero capacity just blocks orders; it doesn't pull demand forward into the last available week before the closure.
  2. No visibility — orders are simply blocked; planners can't see a clear Pre-Position order sized to cover the entire closure window.

The calendar closure approach gives you both: a single, clearly-labelled pre-position order and automatic demand pull-forward.

Setup for a Supplier Closure

Follow the same steps as section 8, but target the BUY route instead of a TRANSFER route:

  1. Create the calendar (e.g., "Supplier Alpha August Shutdown 2027")
  2. Add the closed date range (August 1-31)
  3. Assign to the BUY route:
-- Find BUY routes sourcing from the supplier
SELECT r.id, i.name AS item, ls.name AS supplier, ld.name AS dest_site, r.lead_time
FROM   master.route r
JOIN   master.route_type rt ON rt.id = r.type_id
JOIN   master.item         i  ON i.id  = r.item_id
JOIN   master.location     ld ON ld.id = r.site_id
LEFT   JOIN master.location ls ON ls.id = r.source_site_id
WHERE  rt.planning_type = 'BUY'
  AND  ls.name = 'Supplier Alpha'
ORDER  BY r.id;

-- Assign the calendar to each affected BUY route
UPDATE master.route
SET    ship_calendar_id = <calendar_id>
WHERE  id IN (<route_id_1>, <route_id_2>, ...);

What Happens After Re-running Supply

The pull-forward logic creates a BUY_PREPOS order (or BUY with _PREPOS suffix) in the last available week before the supplier closes. The order quantity equals the total demand across all closed weeks:

Normal plan (4 weeks × 80 units/week):

  Week:     ...  18   19  [20  21  22  23]  24  ...
  Supplier:      OPEN OPEN SHUT SHUT SHUT SHUT OPEN

Without closure awareness:  shortages in weeks 20-23 (supplier can't ship).

With pull-forward:
  Week-19 demand = 80 (own week) + 320 (weeks 20-23 accumulated)
                  = 400 units pre-positioned in ONE delivery.
  Weeks 20-23: demand = 0 (covered), no BUY orders generated.
  Destination draws down from pre-positioned stock each week — no shortage.

Recurring Annual Closures

If the supplier closes every year at the same time, create a new calendar for each planning horizon year. The calendar entries are date-based (not week-index-based), so the same closure pattern shifts naturally as the planning horizon moves forward each year.

Partial capacity reduction

If the supplier doesn't fully close but reduces output (e.g., runs at 30% during August), use master.supply_capacity with reduced capacity_qty for those weeks instead of a calendar closure. The scheduling engine will redistribute orders across available weeks, but won't generate pre-position orders.