Skip to content

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_results row (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_route condition 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), not master.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).

  1. 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.
  2. Seed the route through the import schema (see Data Onboarding): stage the route in import.route with the right item, site, lead_time (days) and route type, then run POST /api/import/run with the route step.
  3. Refresh the MEIO Exclusions grid — the row disappears immediately.
  1. Stage import.item rows carrying unit_cost (or cost/price), or import.item_location for site-specific costs.
  2. Run the import; the report updates on refresh.
  3. 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 = 0 to kit wait times and LT × (1 − fill rate) = 0 to 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