WooCommerce model

Product sales over time

Recreates WooCommerce Analytics' Products and Variations reports (items sold, net sales, orders) at line grain, with the same sales equation as sales_over_time.

Grain
day x store x order x line
Built from
  1. 6
    Raw tables
  2. 6
    Staging
  3. 1
    Core
  4. 1
    Analytics
Reads from WooCommerce
wp_optionswp_postmetawp_postswp_wc_order_product_lookupwp_wc_order_statswp_woocommerce_order_items
Ships as
Weld and dbt SQL, open source on GitHub
core.woocommerce.product_sales_over_time
1-- core.woocommerce.product_sales_over_time
2-- Recreates WooCommerce Analytics' Products and Variations reports (items sold,
3-- net sales, orders) at line grain, with the same sales equation as
4-- sales_over_time.
5--
6-- Grain: day x store x order x line. A line is one row of wc_order_product_lookup:
7-- a product line on an order (row_type SALE) or on a refund (row_type RETURN).
8-- order_id is always the ORIGINAL order, so a refunded line and the line it
9-- reverses share an order_id but not a line id.
10--
11-- Same report date and status rules as sales_over_time (read from the store's
12-- analytics settings, applied through wc_order_stats), so the two agree on which
13-- orders count and on which day.
14--
15-- Product lines only. Shipping lines, fees and order-level refund amounts that are
16-- not tied to a product are not in the lookup table, so net_sales here sums to
17-- less than sales_over_time.net_sales whenever a store charges fees or refunds a
18-- lump sum. Discounts do tie exactly: sales_over_time takes its coupons from these
19-- same lines.
20--
21-- Orders per product: COUNT(DISTINCT order_id) over SALE rows, which is the
22-- Products report's "Orders" column. Not a column here, because a distinct count
23-- does not survive being summed across days.
24--
25-- Names: product_name is the current catalog name; line_item_name is the name as
26-- it read on the order and survives the product being renamed or deleted.
27--
28-- Depends on: staging.woocommerce.order_product_lookup, .order_stats,
29--             .order_items, .product, .product_sku, .store_settings
30
31WITH settings AS (
32    SELECT
33        woocommerce_store,
34        currency,
35        COALESCE(analytics_date_type, 'date_paid') AS date_type,
36        ARRAY_CONCAT(
37            ['wc-auto-draft', 'wc-trash'],
38            COALESCE(excluded_statuses, ['wc-pending', 'wc-failed', 'wc-cancelled'])
39        ) AS excluded_statuses
40    FROM {{staging.woocommerce.store_settings}}

This model and the 7 it depends on. Deploy them together; refs like {{staging.…}} resolve to the matching folders in Weld, and the dbt view uses source() and ref().

Notes

How it counts. And where it stops.

Same report date and status rules as sales_over_time (read from the store's analytics settings, applied through wc_order_stats), so the two agree on which orders count and on which day. Product lines only. Shipping lines, fees and order-level refund amounts that are not tied to a product are not in the lookup table, so net_sales here sums to less than sales_over_time.net_sales whenever a store charges fees or refunds a lump sum. Discounts do tie exactly: sales_over_time takes its coupons from these same lines. Orders per product: COUNT(DISTINCT order_id) over SALE rows, which is the Products report's "Orders" column. Not a column here, because a distinct count does not survive being summed across days. Names: product_name is the current catalog name; line_item_name is the name as it read on the order and survives the product being renamed or deleted.

Raw tables it reads

  • raw.woocommerce.wp_options
  • raw.woocommerce.wp_postmeta
  • raw.woocommerce.wp_posts
  • raw.woocommerce.wp_wc_order_product_lookup
  • raw.woocommerce.wp_wc_order_stats
  • raw.woocommerce.wp_woocommerce_order_items

More WooCommerce models

Put WooCommerce in front
of your agents.

Connect WooCommerce, deploy the models, and ask questions in Claude, Cursor or ChatGPT. Free for 14 days, no credit card.