MEIO Exclusions — SKUs Not Participating in Inventory Optimisation¶
Who this guide is for
Planners and data owners who wonder why a SKU shows no inventory optimisation results (no safety stock, no fill rate), or who received a MEIO run warning about excluded SKUs. It explains the two data-quality defects that make a SKU un-optimizable, the report that lists every excluded SKU with its reason, and how to fix the underlying master data.
Status: Implemented (Sep 2026). Source:
files/meio_runner.py,files/db/meio_exclusion_predicates.py
Why SKUs get excluded¶
MEIO sizes safety stock only for SKUs that can actually be replenished and valued. Two master-data defects make a SKU un-optimizable, and instead of producing meaningless numbers, MEIO now excludes those SKUs entirely and lists them in a dedicated report.
1. No replenishment route (no_replenishment_route)¶
The replenishment lead time of an (item, site) pair is the coverage-weighted blend over its active BUY / TRANSFER / MAKE routes. When no such route exists, the lead time resolves to 0 — and with a lead time of zero, the optimizer mathematics degenerates:
With LT = 0, demand-over-lead-time is zero, so any positive buffer — even a single unit — achieves a fill rate of 100 %. A one-unit safety stock would report a perfect service level for a SKU that actually sells 1,600 units per week.
Before Sep 2026, such SKUs produced noise rows (rop = 1, fill_rate = 1.0)
in the results and — worse — contributed their demand weight to segment
fill-rate targets as free 100 % achievements, making group targets easier
to reach than reality. A genuine same-day (0-day) replenishment route is
treated identically (agreed semantics: lead time 0 = no route).
2. No unit cost (no_unit_cost)¶
A SKU whose unit-cost chain resolves to nothing cannot be valued:
item_location.unit_cost → item.unit_cost → primary-route unit_cost
→ item.cost → item.price → nothing (0)
Safety stock cannot be optimized for an item with no monetary value: the budget logic, inventory value, and marginal-value ranking all break. These SKUs stay in the optimizer as topology anchors (a zero-cost component of a costed assembly still determines the assembly's wait time) but receive zero investment and produce no result rows — they are reported instead.
The MEIO Exclusions report¶
Settings → Master Data → MEIO Exclusions (/settings?tab=masterdata&subtab=meio_exclusions)
The grid lists every demand-bearing (item, site) pair that the next MEIO run will exclude, sorted by total demand — the highest-volume gaps appear first.
| Column | Meaning |
|---|---|
| Exclusion Reason(s) | no_replenishment_route, no_unit_cost, or both |
| Demand / wk | Average weekly demand over the observed history span |
| Total Demand | Sum of all demand quantities for the pair |
| Last 52w Demand | Demand in the trailing 52 weeks — distinguishes active gaps from dead SKUs |
| First / Last Demand | Observed history span |
| Lead Time (wk) | The resolved replenishment lead time (0 for route exclusions) |
| Unit Cost | The resolved unit cost (0 for cost exclusions) |
The report is live-computed
The grid is a database view over master.demand_actuals, master.route
and master.item — it always mirrors what the next MEIO run will
exclude. Fix the master data and the row disappears on the next refresh;
no pipeline run is needed to update the report.
Reading the report for extract-quality triage¶
The two most common data-source gaps show up clearly in the report:
| Pattern | Typical cause | Fix |
|---|---|---|
| Item has demand at several sites, routes exist at some of them | Site-pair extract gap — the source system did not export the route for every stocking site | Add the missing route rows (e.g. BUY at the site with demand) via the import schema |
| Item has demand but no route at any site | Item-level extract gap — routes were never extracted for the item | Seed the item's replenishment routes via the import schema |
| Route exists (lead time > 0) but cost = 0 | Item never costed | Populate master.item.unit_cost / item_location.unit_cost via the import schema |
The Demand / wk and Last 52w Demand columns prioritise the work: a 2,000-unit/week SKU without a route at a major site is a business-critical extract gap; a 0.1-unit/week SKU from a discontinued product is not.
What exclusion means for results¶
An excluded SKU does not participate in the MEIO game at all:
- No results rows — the SKU produces no
PIPE_meio_resultsrow (no safety stock, no fill rate, no inventory value). - No group weight — its demand neither helps nor drags segment / network fill-rate targets; remaining SKUs are sized to carry the targets alone.
- No group targets — the SKU does not demand a target-group assignment and cannot trigger the missing-targets validation.
- Supply cannot replenish it either — a pair with no BUY/TRANSFER/MAKE
route is the same
no_inbound_routecondition the supply engine reports; no orders were planned for these SKUs before the exclusion.
Every MEIO run logs a WARNING with the excluded count:
MEIO exclusions: dropped 10964 of 61509 SKU(s) with lead time 0
(no active BUY/TRANSFER/MAKE route) — they are NOT optimized and produce
no PIPE_meio_results rows. Full list with reasons:
Settings → Master Data → MEIO Exclusions.
Existing results are not cleaned retroactively
Exclusion takes effect from the next MEIO run. Old noise rows remain in the results table until a new run replaces them. If every single SKU in a tenant is excluded (all pairs without routes), the run aborts loudly with a migration hint instead of producing empty results.
Zone scenarios keep repair-loop depots¶
The exclusion predicate respects the collaborative-zone dual mode: a depot replenished only via REPAIR / BALANCING lanes has no BUY/TRANSFER/MAKE route, but zone scenarios deliberately size it on the all-route lead-time blend. Such depots therefore remain in the optimizer for zone-pass scenarios and are excluded only from standard scenarios. The report lists the base (no-route) reason, which applies to standard runs.
Report scope¶
The report covers (item, site) pairs with direct demand history. Two smaller groups are excluded at run time and appear only in the run log:
- Warehouse nodes whose only demand signal is the indirect rollup
(
PIPE_indirect_demand), notmaster.demand_actuals. - Forecast-only pairs (items planned on the forecast basis with no sales history) without routes.
How to fix excluded SKUs¶
Both defects are master-data problems — fix them at the source, never in the report (it is read-only by design).
- Check whether the item has routes at other sites (Items → Routes, or
master.route) — a site-pair gap needs only the missing site's row. - Seed the route through the import schema
(see Data Onboarding): stage the route in
import.routewith the rightitem,site,lead_time(days) and route type, then runPOST /api/import/runwith theroutestep. - Refresh the MEIO Exclusions grid — the row disappears immediately.
- Stage
import.itemrows carryingunit_cost(orcost/price), orimport.item_locationfor site-specific costs. - Run the import; the report updates on refresh.
- Re-run MEIO — the SKU is now sized (routes permitting).
After fixing, re-run MEIO from the pipeline; the run log's excluded count should drop accordingly.
How it works (internals)¶
flowchart LR
A[_load_sku_records<br/>leg_lead_time from route_lt_blend<br/>unit_cost from cost chain] --> B{_exclude_unroutable_skus<br/>per run + per scenario}
B -- "LT <= 0" --> X[dropped — never enters<br/>the Rust optimizer]
B -- "LT > 0" --> C[_neutralize_zero_cost_skus]
C -- "cost <= 0" --> Y[topology anchor:<br/>zero investment, marked<br/>no result row]
C -- "cost > 0" --> D[optimized normally]
- Route exclusion is mathematically neutral for every other SKU: with
LT = 0 a SKU contributes
prob × LT = 0to kit wait times andLT × (1 − fill rate) = 0to every replenishment dependant — dropping it changes no one else's sizing. Group fill-rate denominators do change by design: the free 100 % weight disappears. - Cost exclusion keeps the SKU in the optimizer because costed assemblies resolve their kit components from the same store — dropping a zero-cost component would understate the parent's wait time.
- The report view (
master.meio_exclusions) and the runner share the same SQL fragments (files/db/meio_exclusion_predicates.py), so the grid can never drift from what the run actually excludes.
| File | Role |
|---|---|
files/db/meio_exclusion_predicates.py |
Single source of truth: replenishment-LT / cost-chain SQL fragments + the report view definition |
files/meio_runner.py |
_exclude_unroutable_skus (run + per-scenario filter), _neutralize_zero_cost_skus (marker), result-persist skip |
files/db/db.py |
Creates/refreshes the master.meio_exclusions view on every schema init |
files/api/master_data_router.py |
Read-only registry entry serving the grid at /api/master-data/meio_exclusions |
files/frontend/src/components/MeioExclusionsGrid.jsx |
The grid component (Settings → Master Data → MEIO Exclusions) |
files/tests/test_meio_exclusions.py |
Unit tests: filter semantics, zone dual mode, markers, SQL-fragment sharing |
Related pages¶
- MEIO — Inventory Optimisation — the optimizer itself
- Master Data / Settings — where the report lives
- Data Onboarding — how to seed routes and costs