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¶
- What Was Set Up (the Test Case)
- Step 1 — Run the Supply Pipeline
- Step 2 — Observe the Calendar in the Database
- Step 3 — Find the Pre-Position Order in the UI
- Step 4 — Read the Inventory Projection
- Step 5 — Inspect the Pegging Tree
- Step 6 — Read the Process Log
- How to Add Your Own Calendar Closure
- 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:
Select pipeline High Performance 2026 (pipeline_id = 2), then click Run.
Alternatively from the command line:
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¶
In the Settings UI (recommended)¶
- Open Settings → Master Data → Calendars
- The calendar "Brussels Hub Closure Aug-2026" appears in the list
- Click it to open the detail panel
- Switch to Year View — the closed weeks (Oct 19 – Nov 15) are highlighted in red
- 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¶
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:
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¶
- Open Supply Planning from the left menu.
- Filter by item Chain Lube and site France Country Warehouse.
- 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¶
- Click into the Chain Lube / France Country Warehouse row.
- Open the Planning Grid panel.
- 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¶
- Open Settings → Master Data → Calendars
- Click + New Calendar
- Fill in:
- Name: e.g. "Lyon Hub Summer Shutdown 2027"
- Type:
working - Mode:
not_available← this means the dates you enter are the CLOSED ones - Description: optional
- Click Create
Step 2 — Add the closed date range¶
- In the calendar detail panel, click + Add Entry
- Enter From and To dates covering the closure window
- Optionally add a Label (e.g. "Annual maintenance")
- 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:
- No pre-positioning — zero capacity just blocks orders; it doesn't pull demand forward into the last available week before the closure.
- 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:
- Create the calendar (e.g., "Supplier Alpha August Shutdown 2027")
- Add the closed date range (August 1-31)
- 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.