One screen instead of seven spreadsheets
The summary in Excel — ClickHouse, 296 DAX measures
Before — three files, two chats, and every Friday: "who has the fresh numbers?". Now: one Excel file, a "Refresh" click — three minutes — the whole company on one screen.
Raw channel and inventory data lands in ClickHouse via Python scripts. On top sit SQL views holding the business logic. Power Query only ever sees those; the model carries 296 DAX measures.
01How it worked before us
When a company runs several stores across two channels, each with its own tax setup and several people on the team, everyone has their own picture. The fulfillment lead looks at one file, sales at another, and the owner asks for a summary that someone assembles by hand for two hours out of four web back-offices.
Everyone has their own "truth" in the numbers — and the first half-hour of any discussion goes not to decisions but to working out whose spreadsheet is the most current. "Let's all open the same report" didn't work, because no single report existed.
The client's team had a sales department, fulfillment specialists (watching stock and replenishment on the channels) and an analyst expected to assemble the whole picture. Before our work each role had its own Excel, and those Excels didn't reconcile. Where it broke, specifically:
- Manual exports from web back-offices. Everyone downloaded their slice of the data at their own time. By meeting time, different people held different cuts — Friday vs Saturday, a morning Shopify pull vs a nightly Amazon one.
- Plans copy-pasted out of the inventory system. Once a week someone exported the plan figures, copied, stitched. By mid-week the plan already lagged reality.
- Lookups and macros as load-bearing structure. One renamed column from a supplier and the spreadsheet fell apart; the analyst spent half a day fixing it. Every data change was a threat.
- Parallel realities. Fulfillment couldn't see ads, sales couldn't see stock, the owner saw their own summary. The whole picture was assembled by hand — 3–5 hours weekly, with discrepancies that never fully closed.
The summary came out late, and the first half-hour of every meeting went to "whose numbers are right" instead of decisions.
02What we built
One file — the whole company
The result is a single Excel file with Power Pivot inside. Click "Refresh" — in 3–4 minutes everything is in place: Shopify and Amazon sales per store, ads, stock across every warehouse, landed cost, current plans. No manual assembly.
The operating summary — the home screen
On open, the owner lands straight on the "Ops summary" sheet. A pivot with the key metrics by channel, store and period — the thing the morning stand-up used to be for.
| Metric | Actual | Plan | % to plan | M/M |
|---|
* ACoS — advertising cost as a share of profit. A falling ACoS is good ▲, a rising one is bad ▼.
Green and red in the "% to plan" column are computed by measures — no manual formatting. For ACoS the logic is inverted: a decrease is good, an increase is bad. That matters: ACoS doesn't rise for happy reasons.
Month-over-month dynamics
Breakdown by product group
The same metrics — by brand and category. Pick a group and see how it performs on each channel: orders, ACoS, plan attainment.
| Group | Shopify | Amazon | ACoS | % to plan | Profit |
|---|
Which cuts are available
The whole table is driven by slicers — click a store or a brand and every metric filters in a fraction of a second:
| Dimension | Who it helps |
|---|---|
| Channel · Shopify / Amazon / Wholesale | All roles |
| Store · [Store A] / B / C | Owner, fulfillment lead |
| Brand · product group | Sales, purchasing |
| Category · by product type | Category manager, purchasing |
| Category owner · per channel | Owner |
| Period · day / month / custom | All roles |
Stock — always next to sales
"Avg daily stock" consolidates the main warehouse, Shopify fulfillment, Amazon FBA, goods in transit to fulfillment, and the wholesale warehouse. If sales rise but stock drops 30%, the fulfillment lead sees it immediately, on the same screen, with no separate file.
How the data refreshes
The user presses one button. No passwords or keys to type — they live on a service sheet inside the file, all set up once.
Managing plans — in an online sheet
Plan figures live in an online spreadsheet on Google Drive. The structure is simple: each row is product code × month × channel × type (units or dollars). Want to adjust the plan? Open the sheet, change the numbers. On the next refresh they're pulled automatically: Power Query downloads the current XLSX and everything flows into the model — "% to plan", cumulative plan, comparison to actual.
Taxes and fees — one sheet, everything recalculates
Each store has its own tax and fee profile. The rates live in a table on the "Stores" sheet right in the workbook. A profile changes — change the rate in one cell, hit "Refresh". DAX pulls it through the Dim_Stores lookup and the profit measures recompute for every period and every SKU at once.
| Store | Sales tax / VAT | Channel fee | Fulfillment | Payment |
|---|---|---|---|---|
| [Store A] | 0% | 12% | $3.20/unit | 2.9% |
| [Store B] | 8.5% | 15% | $3.90/unit | 2.9% |
| [Store C] | 8.5% | 15% | FBA | — |
Date range — one cell
The calendar is built dynamically. The "Calendar scale" sheet has two fields: "Date from" and "Date to". Change the range and Power Query rebuilds the lookup to the right length. At year-end, just change "Date to" to 12/31/2027 — the next year appears in the slicers.
Architecture
Shopify/Amazon/OMS APIs
go straight into Power Query
Channel data is loaded into ClickHouse by a separate process (Python scripts). On top of the raw CH tables sit SQL views that already carry the business logic: correct types, status filters, store joins. Power Query only ever sees those. No CSV parsing on the Excel side.
The ClickHouse connector: fnCH
The key brick of the ETL pipeline is a custom fnCH function. It's a Power Query record that, on workbook open, reads the CH_Sources table straight from an Excel sheet — that's where the keys, users and passwords for each store live.
// fnCH — a record with ClickHouse connection methods let Sources = Excel.CurrentWorkbook(){[Name="CH_Sources"]}[Content], Host = "[IP:8123]", // CH HTTP interface address fnFetchSql = (key as text, sql as text) as table => let src = Table.SelectRows(Sources, each [Key] = key){0}, sqlFull = sql & " FORMAT CSVWithNames", data = Table.PromoteHeaders(Csv.Document( Web.Contents(Host, [ Query = [user = src[User], password = src[Password], query = sqlFull] ]) )) in data, fnFetch = (key as text, fullTable as text) => fnFetchSql(key, "SELECT * FROM " & fullTable), fnCombineTagged = (mapping as list) => Table.Combine(List.Transform(mapping, each let src = Table.SelectRows(Sources, each [Key] = _{0}){0} in Table.AddColumn(fnFetch(_{0}, _{1}), "Store", each src[Model label], type text))) in [ Fetch = fnFetch, FetchSql = fnFetchSql, CombineTagged = fnCombineTagged ]
With that constructor, adding a new store to the report is one line:
// Shopify_Sales — straight from CH, all stores at once Source = fnCH[CombineTagged]({ {"[Store A]", "[db].[view_A]"}, {"[Store B]", "[db].[view_B]"}, {"[Store C]", "[db].[view_C]"} })
Web.Contents in Power Query's privacy settings and the connector runs on any Windows PC.
Plans via Google Drive
SKU plans are kept in online sheets — convenient for the team: collaborative editing, access from any device. Power Query reads them as an XLSX file on Google Drive via a public link — the file downloads on every refresh.
// GD_Excel_Workbook — download the Drive XLSX once, // reused by both "Plans" and "Price template" let FileId = "https://drive.google.com/uc?id=...", Workbook = Excel.Workbook(Web.Contents(FileId), true) // downstream: navigate to the named sheet, promote headers in Workbook
The GD_Excel_Workbook query is factored out: both plans and the price template reference it, so the Drive file downloads exactly once per refresh. Plans are organized by four dimensions: product code × date (the 1st of the month) × channel × type (units / USD).
OMS: stock, landed cost, shipments
Own-warehouse data is pulled from the OMS along two paths:
- Through ClickHouse — for daily stock snapshots (a Python script loads CH, then PQ reads it).
- Directly via the REST API — for wholesale shipments:
fnOMS_ShipmentsExpandedrequests shipments by warehouse UUID with pagination.
The Power Pivot data model
The model is a classic star. Central lookups (the "one" side) filter every fact table (the "many" side):
| Lookup | Purpose |
|---|---|
| Dim_Products | Single SKU catalog: brand, category, owners, tax class |
| Dim_Stores | Tax/fee rates and parameters per store |
| Dim_Calendar | Date hierarchy: day → month → quarter → year |
| Dim_Channel | Shopify / Amazon / Wholesale |
| Shopify products | variant ID: SKU, brand, category, store |
| Amazon catalog | ASIN / SKU: title, marketing price |
Key fact tables: Shopify_Sales (~650k rows), Amazon_Transactions (~500k rows), funnel aggregates, ad statistics, daily stock.
Dim_Products and the Shopify tables the active relationship runs through sku → code, for Amazon through seller_sku → code. If an active relationship already exists, the second is automatically inactive — DAX measures along inactive paths need FILTER + CONTAINS rather than a plain CALCULATE.
Key DAX measures
296 measures in all. A few characteristic ones:
ACoS — from profit, not revenue:
-- Shopify ACoS (of profit), % IF([Actual profit, Shopify], DIVIDE( [ALL_Ad_Spend] - [Shopify ad tax], [Actual profit, Shopify] + ([ALL_Ad_Spend] - [Shopify ad tax]) ) )
This isn't "ad spend as a share of revenue" but "how much ads ate of profit" — fundamentally different numbers when fees and cost of goods are high.
Actual profit, Shopify — layered assembly:
-- Actual profit, Shopify
[Shopify net revenue after fees and payment, USD]
- (
[Shopify COGS, USD]
+ [Shopify all costs except taxes and fees]
+ [Shopify fixed internal costs]
+ [Tax, sales tax in sales, Shopify]
+ [Tax, income, Shopify]
+ [Tax, fulfillment, Shopify]
+ [Tax, payment, Shopify]
)
+ [Tax, VAT included in services, Shopify]
+ [Tax, VAT included in COGS, Shopify]
Each cost line is its own measure. You can enter any cut and see exactly what's pressing on profit: channel fee, taxes, logistics, ads. Not "profit fell", but the specific cause.
Plans with daily distribution:
-- Plans_Shopify (with daily values) -- The monthly plan is split across the actual selling days in the month SUMX( SUMMARIZE('Dim_Calendar', [Year], [Month]), VAR MonthStart = DATE([Year], [Month], 1) VAR DaysInMonth = DAY(EOMONTH(MonthStart, 0)) VAR MonthlyPlan = CALCULATE( SUM('Plans'[Value]), 'Plans'[Channel] = "Shopify", 'Plans'[Plan type] = "Units", 'Plans'[Date] = MonthStart, ALL('Dim_Calendar') ) VAR DailyPlan = DIVIDE(MonthlyPlan, DaysInMonth, 0) VAR DaysInContext = COUNTROWS('Dim_Calendar') RETURN DailyPlan * DaysInContext )
The measure computes the cumulative plan to the current day — not the whole month. Look on the 15th: the plan is "trimmed" exactly to the 15th, so the comparison to actual is correct without manual adjustment.
03Timeline and outcome
| Stage | Date |
|---|---|
| Project start, source audit | November 2025 |
| MVP: first working version (Shopify) | January 2026 |
| Adding Amazon, ads, plans, stock | February–March 2026 |
| Financial model, profit per store | April–May 2026 |
A similar problem on your side?
Tell us — in 30 minutes we'll figure out what's possible.
