What This Document Covers
This is the companion document to the SCM POC Data Dictionary & ER Diagram. Where that document describes what each table and column is, this one captures the business logic and judgment calls that shape the data — the rules applied while building it, the assumptions made where the source data or requirements were ambiguous, and who the pipeline is meant to serve.
Business Rules — standing filters, transformations, and classification logic applied to the data, organized by business domain (demand, delivery, purchasing, inventory, planning). These are current, active rules, not a change history.
Data Assumptions — specific judgment calls made where the source data, a business requirement, or a naming convention was ambiguous, and what was decided. Each entry states the assumption and its practical consequence for anyone querying the data.
Personas — the intended consumers of this pipeline and the SCM_AGENT_WITH_RECS SCM Assistant Agent it serves, including the row-level access scoping designed for each.
_V2 semantic-view generation. It does not cover the deprecated schema/view generation that still exists in Snowflake but is out of scope for the live SCM Assistant Agent.Business Rules
Standing rules applied while transforming the data, grouped by domain. Each rule states what is done and why, in plain language.
Cross-Cutting Scope & Interpretation (applies across all four areas)
R1 US-market default. This data covers the Americas region (US, Canada, Latin America), but a question…
US-market default. This data covers the Americas region (US, Canada, Latin America), but a question that doesn't name a market is scoped to the US by default, and the answer states that the default was applied.
WHERE PLANT_CODE LIKE '10US%' (or the US-plant flag where exposed)R2 Default reporting window. A question that states no time period defaults to Q1 2026 (Jan–Mar), the…
Default reporting window. A question that states no time period defaults to Q1 2026 (Jan–Mar), the standard reporting window for this data — stated explicitly whenever applied. This does not extend to other unstated parameters (minimum line counts, thresholds), which get an explicit, reasonable assumption instead of a standard default.
WHERE <period column> >= '2026-01-01' AND <period column> < '2026-04-01'R3 "Consistently" / "every month" / "throughout" requires the condition to hold in *every* period of…
"Consistently" / "every month" / "throughout" requires the condition to hold in *every* period of the stated window, not merely in the periods where the entity happens to have data. An entity present in only 2 of 3 months of a quarter does not qualify as "consistent" for that quarter unless the missing month is explicitly excluded and that's stated.
Not expressible as a single filter — requires per-period evaluation (e.g.,COUNT(DISTINCT period WHERE condition holds) = COUNT(DISTINCT all periods in window))R4 "Where" means a physical location. A "where is X concentrated" question is answered by…
"Where" means a physical location. A "where is X concentrated" question is answered by plant/distribution centre (code + site name); business unit, brand, and product family are categories, not places, and do not answer a "where" question.
Not expressible as a filter — aGROUP BY PLANT_CODErequirement, not aWHEREclause
R5 "Which products / materials / SKUs / items" is always resolved at individual material grain…
"Which products / materials / SKUs / items" is always resolved at individual material grain (12-digit material code); a brand/BU/category rollup may support the answer but never substitutes for the material-level list.
Not expressible as a filter — aGROUP BY MATERIAL_12NC requirementR6 Exclude no-activity entities from window-based counts or rankings. A material/vendor/customer/plant…
Exclude no-activity entities from window-based counts or rankings. A material/vendor/customer/plant with zero relevant activity in the window is out of scope, not a "flat" or "on-target" zero; the population actually used is stated alongside the count.
Not expressible as a single filter — requires excluding rows with no qualifying activity (e.g.,HAVING SUM(activity_qty) <> 0 or an equivalent existence check per entity)R7 Business-unit grouping/filtering/joining uses the numeric business-unit code, never the descriptive…
Business-unit grouping/filtering/joining uses the numeric business-unit code, never the descriptive business-unit label, which can repeat or be entered inconsistently. The label is display-only.
GROUP BY BU_CODE (joined to resolve the display name)R8 Stock/snapshot measures (on-hand, slow-moving, dead-stock) anchor to the latest available period,…
Stock/snapshot measures (on-hand, slow-moving, dead-stock) anchor to the latest available period, independent of any demand/sales window named elsewhere in the same question — pulling the stock leg back to an earlier month answers a different, historical question. A "held zero stock in at least one month of Q1" question is checked month-by-month, not collapsed to a single aggregate or latest-period check.
WHERE CALENDAR_MONTH_KEY = (SELECT MAX(CALENDAR_MONTH_KEY) FROM <fact>)R9 All "open / overdue / at risk" logic is anchored to a fixed snapshot date (the latest actual…
All "open / overdue / at risk" logic is anchored to a fixed snapshot date (the latest actual delivery date in this data extract) rather than the current calendar date. Because the underlying data is a static extract, using the live calendar date would produce a different, wrong answer every day; the fixed anchor is updated by hand whenever the extract itself is refreshed.
WHERE open_qty > 0 AND <requested/confirmed/scheduled date> < DATE '2026-07-07'R10 Never mix a quantity measure and a value/currency measure in the same aggregation. Wherever a fact…
Never mix a quantity measure and a value/currency measure in the same aggregation. Wherever a fact table carries both, the measure type is always filtered explicitly before summing.
WHERE MEASURE_TYPE = 'QUANTITY'*or*WHERE MEASURE_TYPE = 'VALUE_EUR'(never omitted)
Delivery & Order Fulfilment
R11 DRM reliability % (delivery on-time rate) is on-time scored lines ÷ eligible lines, computed only…
DRM reliability % (delivery on-time rate) is on-time scored lines ÷ eligible lines, computed only over lines with no populated rejection-reason code — cancelled sales-order lines are treated as not being delivery failures, and are removed from both sides of the ratio, not just the numerator. Including them understates every service metric by roughly 3.6 percentage points on this data. The rejection-reason code itself remains available for explaining *why* a line was rejected; it does not decide whether a rejected line counts toward the miss rate.
SUM(IFF(REJECTION_REASON_CODE IS NULL, DRM_SCORED_LINES, 0)) * 100.0 / NULLIF(SUM(IFF(REJECTION_REASON_CODE IS NULL, DRM_TOTAL_LINES, 0)), 0)onFACT_DELIVERY
R12 OTIF (on-time and in-full) is DRM-reliable AND delivered quantity ≥ agreed quantity, over lines…
OTIF (on-time and in-full) is DRM-reliable AND delivered quantity ≥ agreed quantity, over lines with no populated rejection-reason code. This is always read from the pre-built delivery-grain metric — never hand-reconstructed from the reliability flag plus a quantity comparison (a materially stricter, wrong reading on this data), and never read from the sales-order-line table, whose on-time/OTIF columns are a different, unsanctioned measure that disagrees with the correct figure by roughly 20 percentage points.
100.0 * SUM(IFF(REJECTION_REASON_CODE IS NULL AND DRM_FLAG = 1 AND DELIVERY_ACTUAL_QTY >= DELIVERY_AGREED_QTY, 1, 0)) / NULLIF(SUM(IFF(REJECTION_REASON_CODE IS NULL, 1, 0)), 0)onFACT_DELIVERY
R13 Delivery/order periods are scoped by the customer's requested delivery date, never the actual…
Delivery/order periods are scoped by the customer's requested delivery date, never the actual delivery date. The actual-delivery-date column is populated only on fully delivered lines, so filtering a period by it structurally removes every short, missed, and open line and makes shortfalls invisible.
WHERE CUSTOMER_REQUESTED_DATE BETWEEN <period start> AND <period end>(notACTUAL_DELIVERY_DATE)
R14 Miss-reason percentages are computed as a share of missed lines (denominator = missed lines) by…
Miss-reason percentages are computed as a share of missed lines (denominator = missed lines) by default; a second, per-opportunity variant (denominator = all scored/eligible lines) is used only when a question is about loss-per-opportunity rather than composition of misses.
Share of misses:100.0 * SUM(missed_reason_flag) / NULLIF(SUM(IS_DRM_MISS), 0). Per opportunity:100.0 * SUM(missed_reason_flag) / NULLIF(SUM(DRM_TOTAL_LINES), 0)
R15 A DRM miss is always derived from the reliability flag being 0, never from a delay-unmeasurable…
A DRM miss is always derived from the reliability flag being 0, never from a delay-unmeasurable flag or rejection status. An unmeasurable delay means the delay itself cannot be measured, not that the delivery failed; excluding these rows understates the delivery-lateness population. A related pre-built "is a miss" column is not used as a substitute — it still wrongly drops unmeasurable and rejected lines that should count as misses.
Miss rate:SUM(IFF(DRM_FLAG = 0, 1, 0)) / COUNT(DRM_FLAG). Average lateness excludes unmeasurable rows:AVG(IFF(IS_DRM_MISS = 1 AND REJECTION_REASON_CODE IS NULL AND IS_ADP_UNMEASURABLE = FALSE, DELAY_DAYS_ADP, NULL))
R16 "Late" (positive delay days) and "DRM miss" (reliability flag = 0) are different,…
"Late" (positive delay days) and "DRM miss" (reliability flag = 0) are different, non-interchangeable populations — a delivery can be a few days late without being a scored DRM miss, and vice versa. "How late were deliveries" questions use the lateness population; DRM/OTIF questions use the miss population.
Lateness:WHERE DELAY_DAYS_ADP > 0. Miss:WHERE DRM_FLAG = 0
R17 A blank/NULL miss-reason bucket means no miss occurred, not "Uncategorized" — labelling it that way…
A blank/NULL miss-reason bucket means no miss occurred, not "Uncategorized" — labelling it that way manufactures a data-quality problem that isn't there. Genuine, unexplained misses (a scored miss where none of the 17 coded reason flags fired) do roll into the "Uncategorized" display bucket, which is a different, real state from "no miss."
Miss-reason label derived asCOALESCE(NULLIF(TRIM(PRIMARY_DELAY_REASON), ''), 'Uncategorized'), sorted with Uncategorized lastR18 A delivery/fill-rate gap's root cause is checked against the actual miss-reason breakdown, never…
A delivery/fill-rate gap's root cause is checked against the actual miss-reason breakdown, never inferred from open-PO backlog or low on-hand stock alone — a plant can carry a large PO backlog or thin stock and still have a delivery gap driven by outbound/administrative issues; "No stock" is consistently under 5% of miss lines even at plants with large backlogs.
Not expressible as a single filter — requires aGROUP BY MISS_REASON check before attributing a causeR19 Open-and-overdue order lines use a pre-built flag (open quantity plus a requested date before the…
Open-and-overdue order lines use a pre-built flag (open quantity plus a requested date before the snapshot anchor, see R9), never a hand-built date comparison.
WHERE OPEN_QTY > 0 AND CUSTOMER_REQUESTED_DATE < DATE '2026-07-07'onFACT_SALES_ORDER_LINE
Purchasing & Vendor Performance
R20 Vendor on-time delivery rate is measured against the PO line's own committed delivery date — goods…
Vendor on-time delivery rate is measured against the PO line's own committed delivery date — goods receipt on or before the planned delivery date — not against the planned-lead-time benchmark in the material-plant master, which is a separate, non-interchangeable concept (the two disagree by 34 percentage points on this data: 37.6% vs 71.7%). Reserve the lead-time-benchmark measures for questions specifically about lead-time variance against plan.
WHERE ACTUAL_GR_DATE <= PLANNED_DELIVERY_DATEonFACT_PURCHASE_ORDER_LINE
R21 Lead-time "variance" has two distinct, non-interchangeable meanings, both exposed separately:…
Lead-time "variance" has two distinct, non-interchangeable meanings, both exposed separately: requested-vs-actual delay, and planned-vs-actual gap against the master-data lead time. Use the latter for any "gap between actual and planned lead time" question.
Requested-vs-actual:AVG(SUPPLY_DELAY_DAYS). Planned-vs-actual:AVG(COALESCE(ACTUAL_LT_DAYS, 0) - COALESCE(PLANNED_LT_DAYS, 0))
R22 First-goods-receipt timing is computed as the earliest posting date from the goods-movement…
First-goods-receipt timing is computed as the earliest posting date from the goods-movement records, joined by PO number and item — the goods-receipt date on the PO line itself is the *latest* receipt for a line received in multiple partial shipments, not the first, and using it directly for "time to first receipt" reads materially wrong (and even sign-flipped) results for some vendors.
MIN(POSTING_DATE)fromFACT_GOODS_MOVEMENTgrouped byPO_NUMBER, PO_ITEM, joined to the schedule date onFACT_PO_SCHEDULE(notFACT_PURCHASE_ORDER_LINE.ACTUAL_GR_DATE)
R23 Valid goods receipt on the goods-movement records prefers movement types 101 and 102.
Valid goods receipt on the goods-movement records prefers movement types 101 and 102.
WHERE MOVEMENT_TYPE IN ('101','102')onFACT_GOODS_MOVEMENT
R24 Vendor confirmation type mapping: AB = order acknowledgement, LA = shipping notification. Unless a…
Vendor confirmation type mapping: AB = order acknowledgement, LA = shipping notification. Unless a question asks about a specific type, any row in the confirmation records counts as a valid confirmation.
Acknowledgement rows only:WHERE CONFIRMATION_TYPE = 'AB'R25 Vendor-confirmation-row questions must not be pre-aggregated (e.g. collapsed to one row per PO…
Vendor-confirmation-row questions must not be pre-aggregated (e.g. collapsed to one row per PO line) before comparing confirmation dates — a PO line can carry several confirmation rows (re-confirmations, partial confirmations), and collapsing to one row per line before the date comparison has been found to understate a "how many lines/vendors slipped" count by roughly half.
CompareCONFIRMED_DELIVERY_DATEat raw confirmation-row grain — noGROUP BY PO_NUMBER, PO_ITEMwithMAX()/MIN()before the comparison
R26 "How many PO lines..." confirmation-date questions use the PO line's own resolved confirmation…
"How many PO lines..." confirmation-date questions use the PO line's own resolved confirmation date, not a join out to the raw confirmation records (a different, finer grain that answers a different question).
SELECT ... FROM FACT_PURCHASE_ORDER_LINEusingCONFIRMED_DELIVERY_DATEdirectly (no join toFACT_PO_CONFIRMATION)
R27 PO and slow-moving quantities are never summed across units of measure without segmenting by the…
PO and slow-moving quantities are never summed across units of measure without segmenting by the unit-of-measure column — close to half of PO lines are recorded as sets rather than single pieces, and no single-unit-equivalent conversion is available.
GROUP BY ..., DCPO_BUOM(purchasing) orGROUP BY ..., BASE_UOM(inventory/slow-moving)
R28 Vendor rankings require a non-null supplier name. A NULL/blank supplier name is an unresolved…
Vendor rankings require a non-null supplier name. A NULL/blank supplier name is an unresolved supplier key, not a real anonymous vendor, and can carry an extreme average that displaces a real vendor from a ranking.
WHERE DC_SUPPLIER_NAME IS NOT NULLR29 Open-and-overdue PO quantity uses a pre-built flag (open quantity plus a confirmed date before the…
Open-and-overdue PO quantity uses a pre-built flag (open quantity plus a confirmed date before the snapshot anchor, see R9), never a hand-built literal-date comparison, which drifts wrong on every reload.
WHERE OPEN_QTY > 0 AND CONFIRMED_DELIVERY_DATE < DATE '2026-07-07'onFACT_PURCHASE_ORDER_LINE
R30 Any safety-stock or reorder-point figure must state its coverage. These fields are populated on…
Any safety-stock or reorder-point figure must state its coverage. These fields are populated on only ~1.1% of material-plant rows; a total or average built from them covers a sliver of the portfolio unless that is disclosed.
SUM(IFF(COALESCE(SAFETY_STOCK,0) > 0, 1, 0))reported alongsideCOUNT(*)as the coverage denominator
Demand Planning & Forecast Accuracy
R31 Forecast error (WMAPE/MAPE) is always computed at the aggregate level over the full population, at…
Forecast error (WMAPE/MAPE) is always computed at the aggregate level over the full population, at the one-month-ahead lag by default, never as a per-row average and never excluding zero-actual rows. A per-row, actual-filtered calculation is dominated by tiny-actual outliers and reads roughly three times too high (215% vs. the correct 73%) on this data, and zero-actual rows are legitimate forecast errors, not exclusions. A separate three-month-lag figure is used specifically for the named 3-month-horizon plan-vs-actual gap metric, but is not the default for an unqualified "forecast error" question.
SUM(ABS_DEV_N1) / NULLIF(SUM(ACTUAL_DELIVERED_QTY), 0)onFACT_FORECAST_PERFORMANCE(noactual > 0filter)
R32 Forecast bias is volume-weighted at the aggregate level, not an unweighted average of per-row bias…
Forecast bias is volume-weighted at the aggregate level, not an unweighted average of per-row bias ratios (which is dominated by tiny-actual outliers). Positive = over-forecast, negative = under-forecast.
100.0 * (SUM(PLANNED_QTY_N) - SUM(ACTUAL_DELIVERED_QTY)) / NULLIF(SUM(ACTUAL_DELIVERED_QTY), 0)R33 Forecast (plan) and actuals must never be summed together — the demand-forecast records hold both…
Forecast (plan) and actuals must never be summed together — the demand-forecast records hold both in the same column, distinguished only by a source flag. Summing across both roughly triples the reported quantity on this data.
Plan:SUM(IFF(SOURCE_FILE = 'DEMAND_QXP', FORECAST_QUANTITY, 0)). Actuals:SUM(IFF(SOURCE_FILE = 'SALES_VIPP', FORECAST_QUANTITY, 0))
R34 "Actual sales/demand" and "forecast bias by material" are answered from the demand-forecast records…
"Actual sales/demand" and "forecast bias by material" are answered from the demand-forecast records (source-guarded, as in R33), not from the forecast-performance records, which are a different accuracy/lag measure with a different population and quantity definition. The forecast-performance records are reserved for accuracy/lag questions (WMAPE, "how accurate was the forecast N months out").
WHERE SOURCE_FILE IN ('DEMAND_QXP','SALES_VIPP')onFACT_DEMAND_FORECAST, notFACT_FORECAST_PERFORMANCE
R35 Demand-plan lifecycle phase and supply class are read from the demand-forecast records' own…
Demand-plan lifecycle phase and supply class are read from the demand-forecast records' own contextual columns, not the material master's global values, which disagree materially with the plan-row values (e.g. the dominant supply class on the US plan differs depending on which column is used).
SELECT PLAN_LIFECYCLE_PHASE, PLAN_SUPPLY_CLASS FROM FACT_DEMAND_FORECAST (not the equivalent columns on the material dimension)R36 Plant-level forecast accuracy cannot be produced. The forecast-performance records carry no plant…
Plant-level forecast accuracy cannot be produced. The forecast-performance records carry no plant column (grain is material × planning account × month); a cross-domain question must aggregate the other side (e.g. inventory) up to material level instead.
Not expressible — no plant column exists onFACT_FORECAST_PERFORMANCER37 The forecast-performance records are already 100% US-scoped — no additional US filter is needed or…
The forecast-performance records are already 100% US-scoped — no additional US filter is needed or possible, unlike every other fact used in this scope.
No filter required (population is already US-only)
R38 Days of Supply is answered by a published, verified query, not a hand-derived on-hand ÷…
Days of Supply is answered by a published, verified query, not a hand-derived on-hand ÷ average-daily-demand calculation from the inventory and demand records separately, which risks mismatching grain or period between the two.
Full query in Appendix A.1
Inventory & Slow-Moving / Dead Stock
R39 Dead stock = on-hand stock held against a phased-out product, not simply stock that hasn't moved.…
Dead stock = on-hand stock held against a phased-out product, not simply stock that hasn't moved. This is the confirmed definition, and it takes precedence over two other measures that are more readily available but answer different questions: a same-named ageing-based measure that is actually built on a >12-month no-movement bucket, and a "no demand this period" cut that reflects only the current month's activity, not lifecycle phase.
WHERE ON_HAND_QTY > 0 AND LIFECYCLE_PHASE IN ('Not-active','Phase out','Phase-out Initiated')onFACT_SLOW_MOVING_INVENTORY, latest period only (R8)
R40 Available finished-goods inventory requires manufactured-finished-good material type, unrestricted…
Available finished-goods inventory requires manufactured-finished-good material type, unrestricted stock status, and a positive on-hand quantity.
WHERE MATERIAL_TYPE = '10MANE' AND IS_UNRESTRICTED AND ON_HAND_QTY > 0(joinFACT_INVENTORYto the material dimension)
R41 Any days-of-supply or stock-rate KPI filters to a positive stock quantity in its denominator…
Any days-of-supply or stock-rate KPI filters to a positive stock quantity in its denominator population — zero-stock rows produce an infinite or zero cover ratio otherwise.
WHERE ON_HAND_QTY > 0(orGIT_QTY > 0where relevant)
R42 On-hand inventory is a repeating monthly snapshot (17 periods live at once) — always filtered to…
On-hand inventory is a repeating monthly snapshot (17 periods live at once) — always filtered to the latest period, or a query overstates stock roughly 17-fold by summing every snapshot together.
WHERE CALENDAR_MONTH_KEY = (SELECT MAX(CALENDAR_MONTH_KEY) FROM FACT_INVENTORY)R43 On-hand inventory carries multiple rows per material-plant-period (storage location, stock type,…
On-hand inventory carries multiple rows per material-plant-period (storage location, stock type, batch) — always summed, never a single row or maximum value; stock type is included in the grouping whenever availability matters, or available stock silently blends with blocked/quality-inspection stock.
SUM(ON_HAND_QTY) ... GROUP BY MATERIAL_12NC, PLANT_CODE, [STOCK_TYPE]R44 The pre-computed ageing buckets (0–6, 7–12, and 12+ months) already partition on-hand stock by age…
The pre-computed ageing buckets (0–6, 7–12, and 12+ months) already partition on-hand stock by age and are used as-is — never re-derived, and never added on top of total on-hand, which would double-count.
QTY_0_6M + QTY_7_12M + QTY_GT_12M = ON_HAND_QTY (identity, not a filter)R45 Healthy stock (0–6 months) and slow-moving stock (6+ months) are answered in units, from the…
Healthy stock (0–6 months) and slow-moving stock (6+ months) are answered in units, from the ageing-bucket quantities. This is not the same measure as, and must not be derived as, on-hand value minus slow-moving value in currency terms.
Healthy:SUM(QTY_0_6M). Slow-moving:SUM(QTY_7_12M + QTY_GT_12M), onFACT_SLOW_MOVING_INVENTORY, latest period
R46 SLOB% (the >12-month no-movement ratio) is a distinct value-based ratio, not the dead-stock…
SLOB% (the >12-month no-movement ratio) is a distinct value-based ratio, not the dead-stock definition (R39), and is used only when a question specifically and literally asks for the ">12-month ageing" or "SLOB" measure by that name.
SUM(VALUE_GT_12M) / NULLIF(SUM(ON_HAND_VALUE), 0) — reserved for literal SLOB/ageing questions onlyR47 "How much slow-moving stock" uses the dedicated slow-moving quantity/value columns directly, not…
"How much slow-moving stock" uses the dedicated slow-moving quantity/value columns directly, not the ageing-bucket split — the two are not interchangeable and do not sum to the same total. The ageing-bucket split is used only when a question specifically asks for the healthy-vs-aged breakdown (R45).
SUM(SLOW_MO_QTY)/SUM(SLOW_MO_VALUE)onFACT_SLOW_MOVING_INVENTORY, latest period
R48 A negative on-hand quantity on the slow-moving records is a posting-timing exception, not physical…
A negative on-hand quantity on the slow-moving records is a posting-timing exception, not physical negative stock, and is reported as its own line item — never clamped to zero and never folded into a positive total.
WHERE ON_HAND_QTY < 0reported separately, neverMAX(ON_HAND_QTY, 0)
R49 Slow-moving stock quantity does not reconcile to the on-hand inventory quantity for the same period…
Slow-moving stock quantity does not reconcile to the on-hand inventory quantity for the same period (runs at ~155% of it); only the value reconciles. The two are never stated as tying out to each other.
Not a filter — a reporting caveat: slow-movingON_HAND_QTY≠ inventoryON_HAND_QTYfor the same material-plant-period
R50 Material status is always shown decoded, never as the raw system code, which means nothing to a…
Material status is always shown decoded, never as the raw system code, which means nothing to a planner.
CASE MATERIAL_STATUS WHEN 'ZF' THEN 'Stop Supply' WHEN 'ZG' THEN 'EOL, Blocked, Scrap Stock' WHEN 'ZO' THEN 'To Be Phased Out' WHEN 'ZH' THEN 'Fully deleted for plant' WHEN 'ZI' THEN 'Phase In' ELSE MATERIAL_STATUS ENDR51 Lead-time and planning-parameter averages are segmented by MRP type before blending — half the…
Lead-time and planning-parameter averages are segmented by MRP type before blending — half the material base carries no MRP planning at all, so an unsegmented average describes no real population. The fully-planned group is the only one suited to a lead-time average; a group with no procurement type set is excluded from lead-time measures entirely.
WHERE MRP_TYPE = 'X3'for a fully-planned-only average; excludeWHERE MRP_TYPE = 'X0' AND PROCUREMENT_TYPE IS NULLfrom any lead-time measure
R52 Brand/business-unit questions (Hue, WiZ, LED Lamps & Luminaires) resolve through the…
Brand/business-unit questions (Hue, WiZ, LED Lamps & Luminaires) resolve through the business-unit dimension, never through the product-family field on the material master, which holds unrelated internal family codes and is frequently blank.
JOIN DIM_BUSINESS_UNIT_BYCODE ON BU_CODE ... SELECT BU_NAME (not the material dimension's product-family field)1. Material & Plant Identity
Material codes are standardized to the 14-digit 12NC form
Different source extracts represent the same material with different code widths and padding. Every table is normalized to Signify's canonical 14-digit 12NC by prefixing 10 and left-trimming leading zeros from the source material code. Some sources already carry a canonical 12NC and need no transform.
Why: without a single normalized code, the same physical material shows up as multiple distinct "materials" depending on which source table you're looking at, breaking every downstream join and rollup.
Plant codes are standardized to a 6-character canonical form
Some source extracts carry a 4-character SAP plant code; these are prefixed with 10 to reach the canonical 6-character form used everywhere from STAGING onward.
Why: same rationale as the material code — one canonical form per physical plant, regardless of which source extract it came from.
Only fully numeric material codes are kept
Rows whose material code is not purely numeric are excluded before standardization. Non-numeric codes are placeholder/dummy entries (statistical postings, rebates, and similar non-physical-material rows) rather than real materials — and different source extracts use different placeholder-code conventions, so a numeric-only test is used instead of trying to enumerate every placeholder pattern by name.
Why: a real 12NC is always fully numeric; testing for that directly is robust to any placeholder naming convention a given extract happens to use, rather than chasing pattern variants one at a time.
2. Demand & Sales
Every demand/sales key figure is classified as QUANTITY or VALUE_EUR before storage
The source key-figure column mixes physical units and monetary values in the same field. Each row's key figure is classified — by name pattern (units vs. QxP/NNS/GAS monetary indicators) and by whether a currency is present — into exactly one of a quantity column or a EUR-value column. The two are never populated on the same row and never summed together.
Why: summing a units figure together with a EUR figure produces a number with no real meaning. Routing every value through one of the two typed columns makes it structurally impossible to accidentally mix them downstream.
Only rows with a blank finance-posting indicator are kept
Demand and sales rows are filtered to the blank/default finance-posting indicator (represented in source as a literal placeholder character, not a NULL). Other codes represent rebates, credits, reclassifications, and other non-sales postings that carry a revenue value with no associated physical quantity.
Why: including those codes would inflate reported revenue with postings that were never actual sales, and — since they have no quantity — would silently break any quantity-based metric that assumes revenue and quantity move together.
Demand and sales are scoped to distribution channel 01 (business-confirmed)
Only the primary consumer distribution channel is kept for demand and sales. The DRM (delivery reliability) table is deliberately not filtered the same way — a channel used for internal sales is kept there because it is relevant to the delivery-reliability metric specifically.
Why: this is a scope decision for the pilot, not a universal rule — applying it uniformly across every table would have silently dropped data DRM needs to score correctly.
Only the latest revision of each demand/sales business record is kept
Source demand and sales extracts can carry multiple revisions of the same business record (same material, plant, period, key figure, category, and a handful of other genuinely-distinguishing attributes). Only the most recently updated revision survives; older revisions are dropped rather than summed.
Why: summing every revision as if each were an additive, independent row overstates volume — superseded versions of the same record are not new data, they're corrections to existing data.
3. Delivery Reliability (DRM)
DRM scoring is limited to standard consumer delivery lines
Only the standard-delivery item category is scored for DRM/OTIF; returns, free-of-charge, and other non-standard categories are excluded from the eligible population entirely (not counted as either a hit or a miss).
Why: returns and free-of-charge lines don't represent a delivery-promise commitment in the same sense as a standard sales delivery, so scoring them would distort the reliability percentage.
Lines with an unmeasurable delay figure are excluded from DRM scoring
A specific placeholder value in the delay-days field marks a line where the delay genuinely cannot be measured. These lines are excluded from the DRM miss/hit calculation (given NULL, not scored as either) rather than being counted as a failure.
Why: the source system flags these lines internally in a way that, left unhandled, causes every one of them to score as a miss by default — materially overstating the miss rate for a population where the outcome is actually unknown, not bad.
Cancelled/rejected order lines are excluded from the DRM denominator
Lines carrying a rejection reason are excluded from DRM scoring entirely rather than counted as delivered or missed.
Why: a rejected line was never going to be delivered by design — including it in either the numerator or denominator misrepresents what DRM is meant to measure (reliability of lines that were actually supposed to ship).
A line with no delivery date is scored a miss once its agreed date has passed
A line that has not yet been delivered is only excluded from scoring while its agreed delivery date is still in the future (genuinely unknown outcome). Once the agreed date has passed with no delivery date recorded, the line is scored as a miss rather than continuing to be excluded.
Why: treating every undelivered line as "unknown, so exclude it" was silently hiding the worst cases — lines that are late and still haven't shipped — from the reliability metric, which overstated performance.
Each missed line gets one primary reason, chosen by a fixed domain priority order
A missed delivery line can trip several of the source system's miss-reason flags simultaneously. A single primary_delay_reason and broader delay_reason_group are derived by testing the specific flags in each domain (Supply, Sales, Outbound) before that domain's catch-all/"other" flag, and testing domains in a fixed order: Supply first, then Sales, then Outbound, then Other.
Why: testing a domain's catch-all flag before its specific flags let "other" win even when a real, specific cause was also flagged on the same line — the fixed, specific-before-catch-all order ensures the most informative reason is always chosen.
Delivery completeness is derived from actual vs. agreed quantity, not from the source completeness flag
The source carries a delivery-completeness indicator, but it reflects a configuration/requirement setting, not proof that a line actually shipped complete — it disagrees with actual outcomes on a meaningful share of rows. A separate, genuinely-derived completeness flag is computed instead, comparing actual delivered quantity against agreed quantity directly.
Why: using a configuration flag as if it were an outcome measurement misrepresents what "perfect order" (on-time + complete) actually means for a given line.
DRM % must be computed additively (sum of scored lines ÷ sum of total lines), never averaged
At any rollup grain above the individual line (material, BU, plant, period), the correct DRM percentage is SUM(drm_scored_lines) / SUM(drm_total_lines) × 100 — never a plain AVG(drm_pct) across lines.
Why: DRM percentage is a ratio, and averaging ratios weights every line equally regardless of how much volume it represents — a rollup built on AVG(drm_pct) will diverge from the true additive rate, especially where volume is unevenly distributed across lines.
4. Purchasing (Purchase Orders)
DC vendor and EP vendor are kept as two separate, explicit fields
A purchase order can carry two distinct supplier identities: the DC (distribution-centre) vendor, who is the actual PO counterparty, and the EP (enterprise-planning) vendor, an upstream reference. Both are exposed as separate columns rather than collapsed into one.
Why: silently substituting one for the other on a meaningful share of lines means the wrong counterparty gets attributed to a purchase order — keeping both explicit lets consumers choose the correct one for their use case.
Full DC-PO-to-EP-PO allocation detail is preserved in a separate table
A single DC purchase order line can legitimately split across more than one EP-PO allocation. The main PO fact is deduplicated to one row per DC-PO line (needed for correct received/open/schedule-quantity math), so the full allocation detail — which EP-POs a DC-PO line actually maps to — is preserved separately rather than being lost to the dedup.
Why: collapsing to one row per DC-PO line at the main fact's grain would arbitrarily keep only one of several real allocations, silently misrepresenting the rest as if they didn't exist.
Goods-receipt quantity is signed by the movement's debit/credit indicator, not by a hardcoded movement-type list
Received quantity nets goods receipts and returns using the source system's debit/credit indicator (receipt = positive, return = negative) rather than a fixed allow-list of movement-type codes.
Why: a hardcoded movement-type list is fragile — it silently excludes valid movement types (such as purchase returns) that weren't anticipated, understating the true received quantity.
Schedule and confirmation dates only consider lines with remaining open quantity
The earliest scheduled/confirmed delivery date for a PO line is computed only over schedule/confirmation records that still have quantity remaining to be delivered. Fully-consumed (already-fulfilled) schedule lines are excluded from that calculation, even if they are chronologically earlier.
Why: without this filter, an old, already-fulfilled schedule line can win the "earliest date" calculation over the actual current, active delivery commitment — producing a nearest-delivery date that is stale by months or years.
Supply delay is measured end-to-end (PO release/creation to goods-receipt), not on a single sub-leg
Lateness (supply_delay_days) is the actual end-to-end lead time minus the requested end-to-end lead time — release/creation of the PO through goods-receipt posting. A narrower sub-leg measurement is retained separately as a diagnostic only, and must not be used to classify a line as on-time or late.
Why: every vendor on-time/OTIF/lead-time-variability metric in the pipeline depends on this figure — measuring only one leg of the journey answers "did this sub-step land on time", not the business question "did the goods arrive when promised".
A source data-quality flag is preserved as an attribute, never as a filter
The source's own data-quality/consistency flag (which identifies purchase orders with potential master-data or process issues that could distort lead-time analysis) is carried through as a column so lead-time figures can optionally be split clean vs. suspect — but it must never be used to silently exclude rows from a query or metric.
Why: excluding flagged rows outright would silently drop real purchase orders from every downstream metric without anyone asking for that; exposing it as an attribute keeps the choice explicit and in the consumer's hands.
5. Inventory
Inventory value/quantity columns can be legitimately negative
A material/plant/period can show a negative value on an inventory quantity or value column. This is not a data-quality defect in the general case — it results from paired reclassification postings between stock types (e.g. moving quantity from unrestricted to quality-hold status), where one stock-type bucket is decremented and another incremented for the same underlying physical inventory.
Why: treating every negative inventory figure as an error and filtering it out would silently drop legitimate reclassification activity and distort net position calculations.
Restricted and unrestricted stock are tracked as separate quantities
On-hand inventory is split into unrestricted (freely usable) and restricted (e.g. quality-hold) quantities as distinct figures, with a restricted-ratio derived from the two, rather than exposing a single blended on-hand number.
Why: restricted stock is not available to cover demand even though it is physically on-hand — blending the two would overstate usable supply for stockout-risk and days-of-supply calculations.
6. Material Master & Planning Parameters
A recalculated reorder point is compared against the configured one to flag miscalibration
Average daily demand (from recent known fiscal months) multiplied by total inbound lead-time days produces a "should-be" reorder point. A material/plant is flagged as calibrated only when its configured reorder point meets or exceeds this recalculated figure.
Why: a configured reorder point that hasn't kept pace with actual demand and lead-time patterns will trigger replenishment too late — this comparison surfaces that gap directly instead of leaving it implicit in the configuration.
The MRP exception flag is a derived proxy, not a genuine SAP exception code
No real SAP MRP exception code exists in the source extracts. A proxy flag is derived instead: true when current unrestricted stock is below the configured safety stock, or above the configured maximum order quantity.
Why: labeling this as a real SAP exception code would overstate its precision — it is a reasonable stand-in built from the parameters actually available, not a system-generated exception.
7. Fiscal Calendar
Fiscal period codes follow a YYYYPPP convention aligned to calendar months
Every fact table's fiscal period code is a 7-character YYYYPPP string, where PPP is a 3-digit period number aligned 1:1 to calendar months (001 = January). DIM_FISCAL_PERIOD is the single source of truth for resolving a period code to its calendar-month and fiscal-year attributes — facts join to it rather than parsing the period code themselves.
Why: a single shared dimension for period resolution means every fact table's date logic (fiscal year boundaries, month-over-month rollups) stays consistent, instead of each table implementing its own parsing.
8. Aggregate-Layer Composite Metrics
BU-level forecast accuracy is computed as MAPE, not an aggregate bias ratio
Business-unit-level forecast error is Mean Absolute Percentage Error — the average, over lines with positive actuals, of each line's own |deviation|/actual — computed independently at the BU grain rather than derived from summed material-level totals.
Why: dividing summed planned by summed actual across a BU blends large- and small-volume materials in a way that understates true error; averaging each line's own ratio avoids that distortion.
A forecast is flagged high-bias above a 25% threshold at the N-3 horizon
A material/period is flagged IS_HIGH_BIAS when the absolute forecast bias ratio at the N-3 (three-period-ahead) horizon exceeds 25% — a business-agreed threshold used to link forecast quality to downstream delivery/stockout impact.
Why: N-3 is the horizon planners act on furthest in advance, so bias there has the most opportunity to be corrected before it causes a supply problem — making it the horizon most worth flagging.
Cross-domain risk score is an equal-weighted starting convention, not a validated business weighting
The composite supply-chain risk score sums four binary domain flags (stockout risk, no-stock delivery miss, late purchase order, high forecast bias) at 25 points each, for a 0–100 scale. Multi-domain risk is flagged when two or more domains trigger simultaneously.
Why: equal weighting is a reasonable, transparent starting point for surfacing multi-domain risk, but it has not been validated or tuned against real business priorities — treat the score as a triage signal, not a precisely calibrated ranking.
Data Assumption Summary
Specific judgment calls made where the source data or a business requirement was ambiguous, and what was decided. These are the assumptions someone querying this data should know about before drawing conclusions.
Defaults, Snapshot Timing & "As-Of" Conventions
| # | Assumption | SQL / Convention |
|---|---|---|
| A1 | Default market scope when a question doesn't name one: US plants. Stated explicitly whenever applied (see R1). | |
| A2 | Default reporting window when a question states no time period: Q1 2026 (see R2). Stated explicitly whenever applied. | |
| A3 | The snapshot anchor for all "as of today / open / overdue" logic is a fixed extract date, not the live calendar date (see R9). This anchor must be updated by hand at the next extract refresh. | |
| A4 | A reporting month is treated as complete only if it ends on or before the live data cutoff, fetched fresh at the time of the question (never a remembered or guessed date, since the data advances over time and a stale cutoff would silently misreport month completeness). Where no period is stated, only complete months are included by default and the covered range is stated (e.g. "Jan–Jun 2026, Jun excluded as in progress"). Where the user states an explicit range, it is honored as given — a month is never dropped to force it "complete" — but if the range's last month is the current (cutoff) month, it is included and flagged as partial through the exact cutoff date. | Not expressible as a static filter — the cutoff is re-queried each time (e.g. |
| A5 | Inventory and other snapshot-based figures are always presented with an explicit as-of date; inventory is never presented as a real-time figure. | Not a filter — a disclosure requirement |
Null / Blank / Missing-Data Handling
| # | Assumption | SQL / Convention |
|---|---|---|
| A6 | Reason/miss-cause columns: NULL or blank is labelled "Uncategorized" (and sorted last in any reason breakdown); a NULL/Unknown share of 10% or more of a total is called out explicitly as a data gap rather than left unremarked. | |
| A7 | Entity columns (customer, plant, material, vendor): NULL or blank is labelled "Unknown Customer / Unknown Plant / …", not silently dropped. | |
| A8 | Display date columns (e.g. actual delivery date): NULL is labelled "Not Yet Delivered" rather than shown blank. | |
| A9 | A NULL cross-plant material status means no global restriction is maintained for that material — never treated as "restricted" and never treated as missing data. | |
| A10 | The net goods-receipt quantity is already sign-adjusted (receipts positive, reversals negative) — no separate sign convention needs to be applied when summing it. | |
Demand & Sales
| Ambiguity | What was decided | Consequence for anyone querying the data |
|---|---|---|
| Which distribution channel counts as in-scope demand/sales | Scoped to the primary consumer channel for demand and sales specifically. DRM is intentionally scoped differently (a channel used for internal sales is kept there). | The channel filter is not uniform across every table by design — a query that assumes the same channel scope applies everywhere will get an inconsistent answer between demand/sales figures and DRM figures. |
| How to tell a placeholder/dummy material code from a real one | A fully-numeric test is used rather than enumerating known placeholder code patterns by name, since different extracts use different placeholder conventions and a new one has appeared before. | If a future source extract introduces yet another placeholder convention that happens to be fully numeric, it would not be caught by this rule — worth a periodic sanity check on volume trends. |
Delivery Reliability (DRM)
| Ambiguity | What was decided | Consequence for anyone querying the data |
|---|---|---|
| What counts as a "real" delivery-reliability outcome vs. an unmeasurable one | Lines with an unmeasurable delay figure, or a rejection reason, are excluded from the DRM denominator entirely (scored as neither hit nor miss) rather than counted as a miss. | The DRM percentage is a rate over the eligible population only — the eligible population is smaller than total line count. Comparing DRM % across two slices with very different eligibility rates can be misleading without also looking at eligible-line volume. |
| Whether an undelivered line with no delivery date is a miss or just unknown | Treated as unknown (excluded) only while its agreed date is still in the future. Once the agreed date has passed, it is scored a miss even with no delivery date recorded. | DRM percentage measured this way will trend slightly lower than a naive calculation that excludes every undelivered line regardless of how overdue it is — this is the intended, more honest behavior. |
| Which of several simultaneously-tripped miss reasons is "the" reason for a line | A fixed domain priority order (Supply, then Sales, then Outbound, then Other) and specific-flag-before-catch-all-flag ordering within each domain. | "Primary delay reason" reporting reflects a chosen priority convention, not necessarily the single most business-critical cause for every individual line — a line can have more than one contributing issue even though only one is reported as primary. |
Purchasing
| Ambiguity | What was decided | Consequence for anyone querying the data |
|---|---|---|
| Which supplier identity is "the" vendor on a PO | Both the DC (distribution-centre, actual PO counterparty) and EP (enterprise-planning, upstream reference) vendor identities are exposed as separate fields rather than one being chosen as canonical. | Any query or metric that needs "the supplier" for a PO must explicitly choose DC or EP vendor — there is no single default column that already made that choice. |
| How to measure whether a PO delivered on time | Measured end-to-end (PO release/creation to goods-receipt), not on a narrower sub-leg. The narrower measurement is kept only as a diagnostic field, explicitly not for on-time classification. | Every on-time/OTIF/lead-time metric in the pipeline reflects the end-to-end definition. If the diagnostic sub-leg field is used instead by mistake, on-time rates will look different (typically better) than the real end-to-end outcome. |
| What a purchase order's data-quality flag should be used for | Carried through as an informational attribute only — explicitly never used as a filter. | Rows flagged by this indicator are NOT excluded anywhere in the pipeline by default. A consumer who wants to analyze "clean" purchase orders only must apply that filter themselves. |
Inventory
| Ambiguity | What was decided | Consequence for anyone querying the data |
|---|---|---|
| Whether a negative inventory value/quantity is a data-quality defect | Treated as expected, not a defect — it results from paired reclassification postings between stock-type buckets (e.g. unrestricted moving to quality-hold) for the same physical inventory. | Filtering out negative values, or treating them as errors to be corrected, would remove legitimate reclassification activity and distort net-position totals. Negative values should be interpreted in context, not discarded. |
Material Master & Planning Parameters
| Ambiguity | What was decided | Consequence for anyone querying the data |
|---|---|---|
| Whether an "MRP exception" flag in this pipeline is a real SAP exception code | No — no genuine SAP MRP exception code exists in the source extracts available to this pipeline. A proxy flag is derived instead (stock below safety stock, or above max order quantity). | Treat MRP_EXCEPTION_FLAG as a reasonable approximation built from available parameters, not as equivalent to a true SAP-generated MRP exception message — it will not exactly match what a planner sees in SAP itself. |
| Are dimension attributes like PRODUCT_CLASS / PLANT_TYPE usable from the semantic views | No — both were found 100% NULL in the live _V2 semantic views and are not exposed with any caveat there; they carry no usable value in the current data. | A question that depends on product class or plant type cannot be answered from the current semantic-view generation. This is a known, current data gap, not a query-writing mistake. |
Composite / Cross-Domain Metrics
| Ambiguity | What was decided | Consequence for anyone querying the data |
|---|---|---|
| How much weight each risk domain (stockout, delivery miss, late PO, high bias) should carry in a combined risk score | Equal weighting (25 points each, 0–100 scale) is used as a transparent starting convention. | The composite risk score has not been validated or tuned against actual business priorities — use it to triage which materials/plants deserve a closer look, not as a precisely calibrated ranking of severity. |
| Whether every material/plant/period with a purchasing or delivery risk signal appears in the cross-domain risk table | No — the cross-domain risk table's row scope follows the supply-health table's coverage (which requires an inventory, demand, or inbound-PO signal). A material/plant/period with only a late-PO or delivery-miss signal and none of those three will not get a row. | A small, known coverage gap: querying only the cross-domain risk table can undercount risk signals that come purely from the purchasing or delivery domains with no accompanying inventory/demand/PO activity that period. |
Persona Details
The intended consumers of the SCM_AGENT_WITH_RECS SCM Assistant Agent and the row-level access scoping designed for each.
Primary KPIs
Secondary KPIs
Top 5 Business Questions & Expected Answers
| # | Question | Expected Answer / Insight | Type |
|---|---|---|---|
| 1 | What is our DRM% for the US market this month vs. last month? | Shows DRM% (e.g. 84% → 79%) with MoM delta. Breaks by plant — 10USE1 handles 42% of US lines so it dominates the overall number. | Descriptive |
| 2 | Which customers have the highest unfulfilled order lines in the past 30 days? | Top 10 Ship-To customers by missed lines. Across 1,898 unique customers, top 5 typically account for 40%+ of misses (Pareto distribution). | Descriptive |
| 3 | What are the top reasons for delivery shortfalls — No Stock, Credit Block, or Too Early? | Too Early = 11%, Appointment Calls = 5.7%, No Stock = 0.3%, Credit Block = 0.07% of lines. ER_ROOT_CAUSE is empty — relies on MISSED_NO_STOCK & NON_SCR_BUCKET_TXT buckets. | Descriptive |
| 4 | For stockout-driven misses, which materials have zero inventory but open purchase orders waiting to arrive? | Three-way cross: DRM (MISSED_NO_STOCK=1) × Inventory (LABST=0) × PO Lead Time (open POs). Reveals materials caught in a supply gap — order expected but not yet landed. | Analytical |
| 5 | Which product families (Hue, WiZ, LED Lamps) have the worst Perfect Delivery Rate and what is the dominant miss category? | Join DRM → Material Master on MATERIAL to get brand/category. Returns DRM% per product family with primary miss bucket — e.g. WiZ = No Stock, LED = Too Early. | Analytical |
Source CSV Files
DRM_AMS_CONS.csv INVENTORY_AMS_CONS.csv PO LT Data US.csv T_MD_MATERIAL_CONSUMER.csvSnowflake Tables
FACT.DRM FACT.INVENTORY FACT.PO_LEAD_TIME DIM.MATERIAL_MASTERPrimary KPIs
Secondary KPIs
Top 5 Business Questions & Expected Answers
| # | Question | Expected Answer / Insight | Type |
|---|---|---|---|
| 1 | Which materials have the highest Plan vs. Actuals Deviation at the 3-month horizon this period? | Ranked list of 1,436 materials by ABS_DEV_N_3. Top biased materials expected to show >30% deviation between planned and actual shipped quantities. | Descriptive |
| 2 | What is the current Inventory Run-Rate Coverage (days of supply) for the top 20 highest-demand materials? | Join Inventory (LABST) ÷ daily demand rate. Highlights materials with <14 days cover — the critical at-risk threshold. Expect 10–15 materials flagged for immediate action. | Descriptive |
| 3 | Which products will face a stock shortfall in the next 30 days based on demand plan, on-hand stock, and open purchase orders? | Three-way join: Demand (planned qty) − Inventory (on-hand + GIT) − PO Lead Time (expected inbound). Shows materials where supply gap is not covered by inbound POs within lead time. | Analytical |
| 4 | Which Business Unit has the worst Planning Effectiveness Score trend over the last 6 months? | ABS_DEV trend across N-3 to N for each of 3 BUs. Shows whether accuracy is improving (narrowing gap) or deteriorating. Expect one BU consistently over-forecasting — driving excess inventory build. | Analytical |
| 5 | Which product categories have Dead Stock Write-Off Risk sitting over 12 months with no demand movement? | From Slow Moving data: 3,342 material-plant combos, 20 plants, 17 periods. BI_5 bucket (longest no-movement) with EUR stock value — direct write-off exposure ranking for finance review. | Descriptive |
Source CSV Files
FC_BIAS_FACC 202605 download US.csv DEMAND_QXP_AMS_CONS.csv INVENTORY_AMS_CONS.csv SLOWMO_AMS_CONS.csv PO LT Data US.csvSnowflake Tables
FACT.FORECAST_BIAS FACT.DEMAND FACT.INVENTORY FACT.SLOW_MOVING FACT.PO_LEAD_TIME DIM.MATERIAL_MASTERPrimary KPIs
Secondary KPIs
Top 5 Business Questions & Expected Answers
| # | Question | Expected Answer / Insight | Type |
|---|---|---|---|
| 1 | Which vendors have the worst Supplier Delivery Performance and what is the average days overrun? | Among 15 US vendors in PO data, top 3 expected to account for 60%+ of supply-driven misses. Output: vendor name, miss count, average days late, linked DRM impact. | Descriptive |
| 2 | Is there a correlation between high Forecast Bias and No-Stock delivery failures for the same materials? | Join Forecast Bias (ABS_DEV_N3) × DRM (miss flag) on MATERIAL. Expected: materials with >25% over-bias show significantly higher No-Stock miss rates — confirms planning-to-execution failure chain. | Analytical |
| 3 | How is our Customer Promise Adherence Rate trending MoM across US plants and which plant is declining fastest? | DRM% time series by PLANT over May 2025 – Jun 2026. Plants 10USE1 / 10USB1 / 10USS1 are US top 3 by volume. Highlights if a specific node is systematically worsening. | Descriptive |
| 4 | Which materials appear in 2 or more risk domains simultaneously — stockout, late PO, and high forecast bias? | Multi-domain cross-join identifies the highest priority items with compounding failure risk. These are the materials where a single fix (e.g. expediting one PO) has the biggest DRM recovery potential. | Analytical |
| 5 | If the top 3 late vendors improved to on-time delivery, how many DRM misses would be eliminated? | Counterfactual: filter PO lines (top-3-late vendors) cross-matched to DRM No-Stock misses. Result is a recoverable DRM% figure — shows leadership the exact uplift achievable through procurement fixes. | Analytical |
Source CSV Files
DRM_AMS_CONS.csv PO LT Data US.csv SALES_AMS_CONS_VIPP.csv FC_BIAS_FACC 202605 download US.csv INVENTORY_AMS_CONS.csv T_MD_MATERIAL_CONSUMER.csvSnowflake Tables
FACT.DRM FACT.PO_LEAD_TIME FACT.SALES FACT.FORECAST_BIAS FACT.INVENTORY DIM.MATERIAL_MASTER DIM.MATERIAL_PLANTTwo additional roles exist specifically to simulate persona-scoped access.
SCM_DEMAND_PLANNER_HUE and SCM_INVENTORY_MGR_US — detailed below — are used to demonstrate what a business-unit-scoped or plant-scoped consumer of SCM_AGENT_WITH_RECS would actually see, as distinct from the unrestricted default role.DEMAND PLANNINGSCM_DEMAND_PLANNER_HUE
A demand planner for the Hue Connected business unit (BU_CODE = 9540, confirmed live via DIM_BUSINESS_UNIT_BYCODE).
Intended tool: the Demand Planning tool, backed by SV_SCM_DEMAND_PLANNING_V2.
Designed row-level scope: a row-access policy restricts FACT_DEMAND_FORECAST, FACT_FORECAST_PERFORMANCE, and FACT_SLOW_MOVING_INVENTORY to BU 9540 rows only. Roles with no scope mapping row see all rows (open by default; scoping is additive, not implicit).
Granted read access: SV_SCM_DEMAND_PLANNING_V2 plus its base CURATED tables — FACT_DEMAND_FORECAST, FACT_FORECAST_PERFORMANCE, FACT_SLOW_MOVING_INVENTORY, FACT_INVENTORY, SUPPLY_POSITION_BY_DATE, DIM_MATERIAL, DIM_PLANT, DIM_FISCAL_PERIOD, DIM_BUSINESS_UNIT_BYCODE.
FACT_INVENTORY and SUPPLY_POSITION_BY_DATE — both read by the same Demand Planning tool for supply-position context — are material+plant grain with no business-unit column anywhere on them, and no reliable material-to-BU dimension exists to derive one from. A Hue-scoped planner asking a supply-position question through this tool would see all-BU inventory rows, not just Hue’s. This is an accepted, documented limitation for the POC stage, not an oversight — a real fix would require building a proper material-to-BU derivation (e.g. from forecast history) as future work.INVENTORYSCM_INVENTORY_MGR_US
An inventory manager responsible for US plants only.
Intended tool: the Inventory tool, backed by SV_SCM_INVENTORY_V2.
Designed row-level scope: a row-access policy restricts FACT_INVENTORY and FACT_SLOW_MOVING_INVENTORY to rows whose plant is flagged DIM_PLANT.IS_US_PLANT = TRUE.
Granted read access: SV_SCM_INVENTORY_V2 plus its base CURATED tables — FACT_INVENTORY, FACT_SLOW_MOVING_INVENTORY, BRIDGE_MATERIAL_PLANT, DIM_MATERIAL, DIM_PLANT, DIM_FISCAL_PERIOD, DIM_BUSINESS_UNIT_BYCODE.
Design Details Worth Knowing
FACT_SLOW_MOVING_INVENTORY feeds both personas' tools (BU-scoped for the demand planner, plant-scoped for the inventory manager). Snowflake allows only one row-access policy per table, so this table is designed to carry a single combined policy that checks both BU and plant conditions together, rather than two separate policies.Unscoped roles see everything. The policy design's fallback behavior is open-by-default: a role with no row in the scope-mapping table for a given policy sees all rows, not zero rows. Scoping is opt-in per role, not a default restriction applied to everyone.
Both personas use the same underlying SCM Assistant Agent. Each role is designed to invoke
SCM_AGENT_WITH_RECS — the same live SCM Assistant Agent — with the row-level scoping expected to apply transparently underneath whichever tool (Demand Planning vs. Inventory) each persona uses.Object-level access is restricted per persona, not just row-level. Each role is designed to receive
SELECT only on the semantic view and CURATED tables its own tool actually needs — a demand planner role is not designed to have any grant on purchasing-related objects, for example, independent of row-level scoping.