Skip to content

D-05

Plan vs Actual

The planner's working instrument: a cascade wireframe on top of a lag-locked forecast snapshot fact. Portfolio error on top, planner cuts in the middle, the item worklist at the bottom — every level visible at once, because a planner does not want to click to find their own list. This page carries the wireframe, the dimensional design, and the source tables in SAP, JD Edwards, Dynamics 365, Infor M3, Oracle EBS, and Oracle Fusion.

There is a reference build of this pattern: the Plan vs Actual dashboard in the showcase renders it over frozen, simulated data — not a live client system. Its tiles are this wireframe’s measures over a frozen, simulated year at item × location × lag — scored against the plan as frozen, with the actual-versus-plan value gap decomposed into volume, mix, price and timing legs that sum to it exactly.

Last verified September 2026.

What it answers

Where actuals broke from plan, by how much, and which planner needs to act on it. That last clause is what makes this a different instrument from a forecast-accuracy report: the error has to land on a named worklist, at the grain the planner actually owns, or it is trivia.

The boundary with the pattern above it is worth stating plainly. The executive cycle review — is the agreed plan still the plan, where are demand and supply diverging — is the S&OP / Demand Review pattern, at family grain, on a monthly cadence. This is the planner instrument underneath it: item × location, worked continuously, ranked by who has to fix what. Same discipline, different grain and different audience — build one dashboard for both and it will serve neither.

One rule governs everything on the board: score the lag-locked snapshot, never the last-edited forecast. A forecast that keeps being edited until the month closes will always look accurate, because it was walked toward the answer. The fact stores the forecast as it stood at a declared number of months out, and the scored lag is the horizon the business actually commits at.

The wireframe

A cascade, the same geometry the inventory pattern uses: every level of granularity visible at once, no clicks required. That fits a worklist, where the planner arrives already knowing which cut is theirs.

Plan versus actual dashboard wireframe: plan month and lag filters, five error KPI tiles, plan against actual by month at the scored lag, cuts by planner, family and site, and the item by location worklist ranked by absolute error

Plan Month | Lag | Planner | Family
WMAPE %
Bias %
Attainment %
Abs Error
Items in Exception
Plan vs actual by month, at the scored lag
By Planner
By Family
By Site
Item × Location Worklist, ranked by abs error
Filters
KPI / Metric
Primary Chart
Secondary Viz
Detail / Table

Layout basis

  • L-02 Cascade / Drill-Down Hierarchy every level visible at once — portfolio error on top, planner cuts in the middle, the item worklist at the bottom
  • L-01 Top-Down Hierarchy the alternative when this is an executive readout instead of a planner worklist — the S&OP pattern's territory

What each region reads from the schema below:

  1. Filters dim_lag sits in the filter bar beside the plan month, not buried in a settings panel — the lag is the single most consequential choice on the board, and it belongs where the room can see which one is selected.
  2. KPI row five tiles at the filtered grain: WMAPE and bias as the diagnostic pair, attainment as the plan-denominator view the supply side asks for, absolute error summed at the selected lag as the size, and the exception count. Each tile states its variant on its face — a bare "forecast accuracy" label is the number nobody can reproduce.
  3. Plan vs actual two series over dim_date's plan_month, filtered to the scored lag from dim_lag — and the axis label says which lag, because the same fact drawn at a different lag is a different chart with the same title.
  4. Cuts the same components grouped by dim_item planner and family and dim_location site, ranked by summed absolute error rather than by percentage — ranking on a ratio floats tiny-volume items to the top of every list.
  5. Worklist the fact's own grain — item × location at the scored lag, sorted by absolute error, with the ratios shown beside each row as diagnosis rather than as the sort key. This is the row a planner actually acts on.

The star schema underneath

The fact is a periodic snapshot with an unusual second time axis: every plan month is stored once per lag, so a single month carries the forecast as it stood one month out, two months out, three, and so on. That is what makes forecast accuracy and forecast improvement answerable from the same table — and it is why the grain has four parts rather than three.

fct_forecast_vs_actualPeriodic snapshot

Grain one row per item × location × plan month × lag

Stored components

  • plan_qty
  • actual_qty
  • plan_value
  • actual_value
  • snapshot_date

Foreign keys

  • dim_date
  • dim_lag
  • dim_item
  • dim_location
dim_date
  • plan_month
  • fiscal_period

The month being scored — actuals attach here as months close; the snapshot axis is the lag, on dim_lag.

dim_item
  • planner
  • family
  • abc_class

The planner attribute is the accountability cut — every error lands on someone's worklist.

dim_lag
  • lag_months
  • is_scored_lag

Which snapshot age is the scored plan is a convention set once — score the lag the business commits at, show the others as context.

dim_location
  • site
  • region

Forecast where you fulfill — item × location is the grain planners actually own.

fct_forecast_vs_actual — plan and actual stored as quantities and values at every lag; WMAPE, bias, and attainment all derived from them at query time, never stored.

No error column anywhere. Store an error rate on the row and you have frozen one variant, one denominator, and one aggregation rule at build time — and every rollup above it becomes an average of ratios. Store plan_qty and actual_qty, and every grain the board rolls up to gets an honest number from the same two columns.

The second time axis carries one rule with it: the quantities sum within a lag, never across lags. Each lag re-states the same plan month, so a total that spans two lags counts that month's error twice. The lag is the first filter on this board for exactly that reason.

Measures & additivity

Five measures — two summed quantities and the three ratios over the same components:

Plan vs actual measures, their formulas over the forecast snapshot fact, and their additivity
MeasureFormulaAdditivityNote
wmape_pctsum(abs(plan_qty − actual_qty)) / nullif(sum(actual_qty), 0)Non-additiveWeighted MAPE — absolute errors summed before dividing. The average of per-item MAPEs is a different, worse number; the variants get named, not mixed.
bias_pctsum(plan_qty − actual_qty) / nullif(sum(actual_qty), 0)Non-additiveSigned — the persistent lean. A planner can have terrible WMAPE and zero bias, or the reverse; the pair is the diagnosis.
attainment_pctsum(actual_qty) / nullif(sum(plan_qty), 0)Non-additiveThe plan-denominator view the supply side asks for — same components, different denominator, both labeled. No published definition of attainment has been located; this uncapped actuals-over-plan form is the house construction, named on the chart rather than assumed.
abs_error_qtysum(abs(plan_qty − actual_qty)) — at one lagSemi-additiveThe additive workhorse within a lag — rank planners, families, and items by summed absolute error before showing anyone a percentage. Never sum across lags; each lag re-states the same plan month's error.
error_qtysum(plan_qty − actual_qty) — at one lagSemi-additiveSigned error — nets out by design, which is exactly what makes it the bias numerator and nothing else. Never sum across lags; each lag re-states the same plan month's error.

Name the MAPE variant, then publish exactly one. There are at least three numbers commonly called "forecast accuracy" and they disagree materially: WMAPE, a ratio of summed absolute error to summed actual — the one this pattern publishes; the average of per-item MAPEs, which lets a single low-volume item with a 900% error dominate a portfolio number; and the symmetric variants, which put plan and actual both in the denominator to bound the result. Each is defensible; mixing them across two reports is not. Pick one, label it on the tile, and write the choice down where the business can find it.

WMAPE and bias are a pair, not alternatives. A planner can have terrible WMAPE and zero bias — noisy but unbiased, a demand-variability problem — or excellent WMAPE and a persistent lean, which is a process problem someone can fix tomorrow. Showing only the first hides exactly the one that is actionable. And bias nets out by design: at portfolio level a chronically over-planned family and a chronically under-planned one cancel to a comforting zero, which is why the bias tile is always read at the cuts below it, never alone.

The exception tile counts items breaching an error threshold the business set for itself, labeled as an internal goal on the tile. There is no credible external benchmark for forecast error — it is a function of your demand variability, not of your competence — so nothing on this pattern draws one.

On Databricks AI/BI

Rendering notes for building this wireframe on Databricks AI/BI dashboards. Where a capability below comes from hands-on checking rather than documentation, it was verified in one workspace on 2026-08-24 — a point-in-time observation, not a vendor commitment; re-check in your own workspace before committing a design to it.

What this wireframe needs from Databricks AI/BI, whether the platform provides it, and how to build it
NeedStatusNote
The scored lag as the first control on the pageNativeThere is a rule about which filter mechanism to use. Make the lag a parameter, not a field filter: parameters are substituted into the dataset SQL before joins and aggregation and always re-run the query server-side, so every tile re-reads the plan at the selected vintage instead of re-slicing rows that were fetched at another one.
WMAPE and bias recomputed from summed error and volume componentsNativeA dataset rule the documentation states outright, for the three measures it names: count distinct, median and percentiles cannot be re-aggregated from partial results. The documentation does not mention WMAPE or MAPE at all — extending the same rule to them is the house rule here, and it follows from the same arithmetic, since a weighted error rate is a quotient of two sums rather than a number that survives averaging. Store quantities and absolute errors and never a stored error rate, so each tile divides a summed numerator by a summed denominator. The filter mechanism matters for the same reason: parameters recompute from rows in DBSQL, while field filters on datasets of 100K rows or fewer combine results already returned.
The lag × plan-month surface, and the same view panelled by familyNativeHeatmap is one of the built-in visualization types. The panelled version comes from the per-widget Facet encoding, verified rendering side-by-side panels on a bar chart in one workspace on 2026-08-24.
The plan drawn as the modelled line beside the measured actualNativePer-series Pattern styling sets dashed or dotted per Y-series, verified in one workspace on 2026-08-24. Where the dataset also carries quantile columns, the forecast-line widget takes arbitrary upper and lower columns — no forecasting function required — and renders a true shaded band with the modelled series dashed by construction, also verified in one workspace on 2026-08-24. One caveat: that widget is named and labeled for forecasting, so check the legend and tooltips read honestly before shipping a band that is not a forecast.

Sourcing it from SAP

Table roles.Same honest starting point as the S&OP pattern: the plan side does not live in ECC or S/4 — it lives in APO or IBP. What the reference gives you is the mapping layer, /SAPAPO/MATKEY for the APO product master and /SAPAPO/MATLOC for the location-product record, which tie planning output back to the material and plant keys the gold layer already uses. The actuals side is a choice with three defensible answers: VBAK / VBAP on an orders basis, LIPS delivery items on a shipments basis, or VBRP billing items on an invoiced basis.

The structural insight. The actuals basis changes the number, so it gets named on the axis and in the definition, not assumed. Orders basis measures demand as the customer expressed it; shipments basis measures what you could fulfill — which means a supply failure shows up as a forecast miss, attributing to the planner something they did not do. For scoring demand planners, orders basis is usually the right call, and the reason should be written down.

Sourcing it from JD Edwards

Table roles. F3460 holds forecast quantities keyed by item, branch, and forecast type — and the forecast type is the variant axis that matters here: the statistically generated forecast and the planner-adjusted forecast are separate types, which is what makes forecast value added answerable at all. Score the adjusted type, but keep the generated one, because the difference between them is the measure of whether the adjustment helped. Actuals come from F4211 plus F42119 — the union is mandatory, since lines move to history at sales update.

The traps. F3460 carries no lag history: each planning run overwrites the rows for its own forecast type, so the snapshot-per-lag grain is entirely a lakehouse construction — capture the file on a schedule and stamp each capture with its months-out. Miss the captures and no amount of later work reconstructs them. Then the standing encoding: quantities carry implied decimals, the branch plant is space-padded (TRIM before joining), and dates are Julian CYYDDD integers.

Sourcing it from Dynamics 365

Table roles. ForecastSalesholds the demand forecast per forecast model, item, dimension set, and period start — and the forecast model plays the same version-axis role JDE's forecast type does, letting a baseline model and an adjusted model coexist. Actuals come from SalesTable / SalesLine on an orders basis, CustPackingSlipJour on a shipments basis, or CustInvoiceTrans on an invoiced basis.

The traps. Forecast models get recycled — copied over and reused under the same name — so the model alone will not give you lag history without a dated capture. Forecast lines hang off a dimension set (InventDim), so resolving them to a site or warehouse for the item × location grain is a join, not a column. And every table is company-partitioned (the DataAreaId quirk), which will otherwise fan a portfolio WMAPE across legal entities.

Sourcing it from Infor M3

Table roles. Stated plainly again: the M3 reference carries no forecast table. MITPLOis the material plan — one row per planned demand or supply transaction per item and warehouse — and it is regenerated by the planning run, so it describes today's plan and nothing about last quarter's. Actuals come from OOLINE on an orders basis, MHDISL delivery lines on a shipments basis, or ODLINE invoiced lines; MITTRA is the movement ledger behind the shipment view.

The traps.Every lag snapshot is a lakehouse capture — the fact's lag dimension exists only downstream, and it is only ever as complete as the extract schedule was. Then the usual mechanics: pin CONO in every join, read column names as a two-char prefix plus a four-char alias (the prefixes quirk), normalize quantities out of the item's basic unit of measure before summing an error, and convert the numeric YYYYMMDD dates before bucketing by plan month.

Sourcing it from Oracle EBS

Table roles. The plan side is MRP_FORECAST_DESIGNATORS, which names the forecast and its set, over MRP_FORECAST_DATES, the fact itself — a row per item, org, forecast name, and bucket date, except that a row with RATE_END_DATE set spans every bucket through to that date and has to be expanded before it is summed. Actuals come from OE_ORDER_LINES_ALL on an orders basis, WSH_DELIVERY_DETAILS on a shipments basis, or RA_CUSTOMER_TRX_LINES_ALL on an invoiced basis — whichever the actuals-basis decision picked.

The traps. The designator is the identity of a forecast, so it belongs in every join or two forecast versions merge into one plan quantity. The quantities are consumed as sales orders arrive — CURRENT_FORECAST_QUANTITY is post-consumption, which is not the plan the lag was locked at, so capture the vintage rather than reading it back later. Every lag snapshot is therefore a lakehouse capture, and it is only as complete as the extract schedule was. Pin operating unit on the _ALL tables (MOAC in practice), and on the invoiced basis filter LINE_TYPE = 'LINE' or tax and freight rows inflate the actual.

Sourcing it from Oracle Fusion

Table roles. Actuals are DOO_FULFILL_LINES_ALL at the fulfillment-line grain on an orders basis, with WSH_DELIVERY_DETAILS for a shipments basis and RA_CUSTOMER_TRX_LINES_ALL for an invoiced one. The plan side is not in this reference: forecasts and plans live in Demand and Supply Planning and arrive through their own BICC extracts.

The traps. Fusion Cloud has no direct SQL path; the physical names describe the source model and the extract must use the verified BICC/PVO surfaces available for those tables (no SQL path), and those extract definitions drift with each release (PVO drift). So the lag-locked fact is assembled from two extract paths — the planning one and the order-management one — and the lag key is stamped in the lakehouse, never inherited from either. Fulfillment lines split as they process, so sum knowing that; keep business-unit scope explicit (_ALL means BU here); and on the invoiced basis filter LINE_TYPE = 'LINE' before summing.

Running the workshop

Three decisions, all of them upstream of any build:

  • Pick the error variant and write it down. WMAPE, the average of per-item MAPEs, or a symmetric variant. The choice belongs to the business, the label belongs on the tile, and the number belongs in exactly one place so two reports cannot quietly disagree.
  • Agree the scored lag. Which horizon the business actually commits at — usually the lead time it has to buy or build against. That lag is the headline; the others stay on the board as context, never blended into it.
  • Agree the actuals basis. Orders, shipments, or invoices. Shipments basis charges the planner for supply failures they did not cause, so it needs a deliberate reason if it is chosen.

And the anti-patterns to name out loud: averaging MAPEs (one low-volume item with a 900% error hijacks the portfolio number), scoring the current forecast (edited toward actual all month, so accuracy is manufactured), and bias hidden by netting across the portfolio — a portfolio bias of zero can be two large, opposite, entirely fixable process errors cancelling each other out.

Maintained by Summit Analytics, a supply chain analytics practice. The tools and references are free — the consulting is selective.

Part of the Summit Analytics reference library.

Work with the practice

Not affiliated with or endorsed by SAP, Oracle, Microsoft, or Infor. Product names are trademarks of their respective owners.