Cases/ E-commerce/ Operating summary

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.

Operating summary Plan / actual ACoS · profit · stock Shopify · Amazon ClickHouse + Power Pivot 2.5 months to MVP
Industry Consumer goods — toys and learning kits · Several stores, selling on Shopify and Amazon · Own warehouse + marketplace fulfillment + wholesale
Driver's seat What they open in the morning, where they change numbers, what's clear at a glance — without calling a colleague. The engineering side is the toggle in the top-right corner.
Under the hood Architecture, the fnCH connector, the ETL chain, key DAX measures. To see it through the user's eyes, use the toggle in the top-right corner.
Summary assembly 0 min ▼ was 3–5 hours
Sources 1 file ▲ vs 4–7 spreadsheets
Lookup errors 0 ▲ eliminated
Versions of "the truth" 1 ▲ was one each

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:

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.

Summary assembly time — 0 minutes. The daily refresh is automatic. The owner opens the file and the data is already inside.

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.

Ops summary · sales data as of May 24 · auto-refreshed
synthetic data · structure matches the production model
Channel
Store
Period
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

Orders by month, USD updates when you switch channel

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.

Product groups · Shopify vs Amazon May · actual vs plan
Group
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:

DimensionWho it helps
Channel · Shopify / Amazon / WholesaleAll roles
Store · [Store A] / B / COwner, fulfillment lead
Brand · product groupSales, purchasing
Category · by product typeCategory manager, purchasing
Category owner · per channelOwner
Period · day / month / customAll roles
A useful combination: pick a brand → immediately see its ACoS on Shopify vs Amazon, plan attainment in units, and stock across every warehouse right now. That picture used to be assembled from three separate spreadsheets.

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.

From practice. Analyzing a brand's plan, you can look at orders (Shopify), current warehouse stock and ad spend for the period at once. It's instantly clear whether the plan missed because of "no ads" or because of "no stock".

How the data refreshes

Shopify / Amazon APIautomatically, overnight
ClickHousecentral store
Excel: "Refresh all"~3–4 min
Every sheet currentin one pass

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.

Online sheetteam edits together
Google DriveXLSX by link
"Refresh"in Excel
Model recalculatednothing by hand

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 & tax/fee reference change rates here → recalculation is automatic
Store Sales tax / VAT Channel fee Fulfillment Payment
[Store A]0%12%$3.20/unit2.9%
[Store B]8.5%15%$3.90/unit2.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

Data sources
Store
Excel model
Shopify APIorders · funnel · stock · ads · fulfillment
Amazon SP-APItransactions · analytics · daily stock · ads
OMS REST APIstock · inbound · landed cost · prices · wholesale shipments
Google Driveplans by SKU × channel × month · XLSX by link
Workbook sheetsStores · Calendar scale · CH-sources lookup
ClickHouse SQL views per store A Python script loads
Shopify/Amazon/OMS APIs
OMS shipments, Drive plans and workbook sheets
go straight into Power Query
Power Query 42 queries · fnCH · fnOMS · GD_Excel_Workbook
Power Pivot ~30 model tables · 296 DAX measures

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]"}
})
Why not ODBC? ClickHouse serves CSV over HTTP — that works with no driver installs on each workstation. Allow 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:

The Power Pivot data model

The model is a classic star. Central lookups (the "one" side) filter every fact table (the "many" side):

LookupPurpose
Dim_ProductsSingle SKU catalog: brand, category, owners, tax class
Dim_StoresTax/fee rates and parameters per store
Dim_CalendarDate hierarchy: day → month → quarter → year
Dim_ChannelShopify / Amazon / Wholesale
Shopify productsvariant ID: SKU, brand, category, store
Amazon catalogASIN / SKU: title, marketing price

Key fact tables: Shopify_Sales (~650k rows), Amazon_Transactions (~500k rows), funnel aggregates, ad statistics, daily stock.

Active vs inactive relationships. Between 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

StageDate
Project start, source auditNovember 2025
MVP: first working version (Shopify)January 2026
Adding Amazon, ads, plans, stockFebruary–March 2026
Financial model, profit per storeApril–May 2026
From the first call to a working summary — about 2.5 months. The file now refreshes daily and is the single source of data for the team. Most of the time went not into DAX but into building the SQL views in ClickHouse — the ETL pipeline was assembled per store, accounting for the quirks of each back-office's exports.

A similar problem on your side?

Tell us — in 30 minutes we'll figure out what's possible.

view mode