Acquisitions left several ERPs and no number anyone trusted. I built the foundation underneath.
Silver and gold layers in a Fabric lakehouse, modelled tables, validation at the door, and snapshot history. Every other project on this page runs on it.
Context
De Havilland grew by acquisition, Viking Air among others, and each business arrived with its own ERP and its own way of working. Reporting lived in Excel. Even the Power BI dashboards were wired to static spreadsheets that someone refreshed by hand.
The result was predictable: two teams could bring two different numbers for the same KPI to the same meeting, and nobody could prove which was right.
How I approached it
- Model before dashboards. Raw ERP extracts land in a silver layer. I build gold tables on top of them in the Fabric lakehouse and in SSMS, so a part, a supplier, a site and an order mean the same thing in every report.
- Pick the pattern per source, not per habit. ELT where the platform can transform cheaply after loading. ETL where a source is messy enough that it should be cleaned before it lands. Star schemas where reporting speed matters, snowflake where a shared hierarchy needs a single owner. Choosing per case cut compute metering costs and stopped the refresh jobs that used to crash the server.
- Derive the business status once. Whether a line is past due, blocked or closed is decided in the gold table, not re-invented in each report.
- Keep the history the ERP overwrites. Snapshot tables sized to each use case, so trend lines and audits survive daily ERP updates.
What I built
The gold tables, snapshots and validation checks, and 28 automated reports on top of them. The leadership scorecard in Microsoft Fabric covers supplier OTD, on-time order placement, PO coverage, contract coverage, NCR burndown, pick-to-PO ratio, parts at shelf-life expiry risk, and PO lines awaiting supplier confirmation.
CREATE OR REPLACE TABLE gold.sales_order_lines USING DELTA AS
SELECT
CONCAT(l.order_no, '-', l.line_no, '-', l.release_no) AS so_key,
l.item_no,
CASE
WHEN l.state IN ('Invoiced/Closed', 'Cancelled') THEN 'Closed'
WHEN o.state IN ('Planned', 'Blocked') THEN o.state
WHEN TO_DATE(l.promised_date) < TO_DATE(l.ingested_at)
THEN 'Past Due'
ELSE 'Future Due'
END AS reporting_status,
CASE l.priority WHEN 1 THEN 'AOG' WHEN 2 THEN 'Critical'
ELSE 'Routine' END AS priority_text,
ROUND(CASE WHEN l.state IN ('Cancelled', 'Invoiced/Closed') THEN 0
ELSE l.qty_due - l.qty_shipped END, 2) AS open_qty,
ROUND(l.qty_due * l.unit_price * l.conv_factor
* (1 - l.discount / 100.0), 2) AS net_amount,
l.ingested_at AS loaded_at
FROM silver.order_lines l
JOIN silver.orders o ON o.order_no = l.order_no;Result
Leadership reviews run on one scorecard instead of competing spreadsheets. Twenty-eight reports refresh on their own, and fourteen KPIs, nine of them Tier 0, each have one shared definition. I chaired the DHC Data Analytics Committee for nine months to keep it that way across business units.