User Guide & Methodology Reference
Breww Sync pulls your sales orders, product catalogue, customer records, and batch/racking data from the Breww API, then computes 8 analytics tabs covering revenue, production, customer retention, stock forecasting, and product volumes, broken down by week with year-on-year comparisons.
I've verified all claims to the best of my ability, but as before, only you can properly scrutinise these figures to make sure nothing is off.
Tabs appear in this order along the bottom of the spreadsheet:
The underscore-prefixed tabs are raw data caches, you can ignore these.
A week-by-week overview of the business for the current year. Each row is one business week, with sales revenue, production volume, and customer counts, compared against the same week last year. A "Stock Weeks" section on the right shows how many weeks of stock remain for each beer. Where a beer has both kegs and cans, each SKU gets its own row.
Year-on-year revenue comparison for every external customer, broken down by week. Each customer gets two rows: the top row shows 2026 weekly revenue, and the bottom row (italicised) shows 2025. Customers are sorted by total 2026 revenue so far.
Every beer product ranked by year-to-date volume sold, with comparisons across three time windows: last week, last 4 weeks, and year-to-date. Products with zero current-year volume are excluded.
Customers who haven't ordered in 2 or more weeks, sorted first by fewest weeks without an order, then by all-time order count. A "Retain" checkbox controls whether each customer stays in the forecast. Customers under 6 weeks are pre-checked; 6+ weeks are unchecked. You can override either way, and after the next sync, your choices persist across refreshes.
Projected weekly volume (in litres) for each beer product across 52 weeks of 2026. These projections are based on 2025 trading patterns from retained customers, adjusted using the profile curve for newer accounts. Products are sorted by total projected volume. Values are rounded to whole litres.
Projected weekly revenue (in £) for each retained customer across 52 weeks of 2026. Customers are sorted by total projected revenue. Each customer's projection uses their 2025 ordering pattern, with one of two methods applied depending on when they started trading.
Normal text on white: projected from actual 2025 trading history ("History" method)
Italic text on grey: estimated via the profile curve ("Profile" method, customer started trading after Week 13 of 2025)
Production data in two sections: a summary box at the top showing per-beer averages from recent brews, and a detailed log of every racking event below.
FYI: it seems that a lot of brews are missing start dates and/or final gravity measurements, which I've confirmed to be genuinely missing from the data in Breww. This means some cells appear blank, hopefully it's not a total mystery why those figures aren't in the record.
Section 1, Profile Curve: A static reference table showing the typical distribution of when new customers place orders across the year (based on 293 new customer orders from 2024). Highlighted rows indicate weeks with >3% of annual volume.
Section 2, New Customers: All customers with their first orders in 2026. Each has an editable field for projected revenue. The figures entered here will be reflected in projections after the next analytics refresh.
Section 3, Calculator: Type an estimated annual volume for a new customer and see the projected weekly volume based on the profile curve.
The spreadsheet refreshes in three manual steps, each triggered from the Breww Sync menu in the menu bar:
Fetches all data from Breww API into 5 hidden raw tabs
~4–5 minutesBuilds Batch Tracking, Lapsed & Lost, On Trade Sales, Product Detail
~30–60 secondsBuilds Summary, Profiles, Forecast Product, Forecast Customer
~30–60 secondsEach step will show a confirmation dialog before running and a summary dialog when complete.
If you see an "execution time exceeded" message, that's just because you didn't click "OK" soon enough after the code finished running, dismiss and ignore.
| Menu Item | Purpose |
|---|---|
| Set API Key | Store your Breww API key (one-time) |
| Initial Setup (Create Tabs) | Create or reset all 14 tabs with headers |
| Sync Raw Data (API) | Fetch fresh data from Breww |
| Analytics 1: Core | Compute core analytics tabs |
| Analytics 2: Summary & Forecasts | Compute summary and forecast tabs |
| Diagnose (Check Raw Data) | Debug tool: shows row counts and data samples |
| Export All Tabs (CSV) | Export every tab as a CSV file to Google Drive |
This section documents the exact calculation logic for each metric, a comprehensive spec to inform further development.
Analytics 2 reads "Retain" checkbox state from Lapsed & Lost (built in Analytics 1)
Key architectural point: Analytics 1 and 2 never call the Breww API. They read exclusively from the raw tabs via the DataLayer. This means you can re-run analytics without consuming API rate limit.
Revenue field mapping: All revenue figures use the Breww API field order_lines[].value, the ex-VAT net line amount. This is the correct metric for B2B trade sales reporting.
Row scope: One row per business week from Week 1 to the current week of the current year.
| Metric | Formula | Source |
|---|---|---|
| Sales (£) | SUM(order.lineTotal) for all order lines in this business week/year, excluding internal accounts | _Orders |
| Sales LY (£) | Same formula for the same business week of the previous year | _Orders |
| Sales YoY (%) | (Sales - Sales_LY) / Sales_LY, "New" if LY=0 and CY>0; blank if both zero | Derived |
| Production (HL) | SUM(batch.totalVolL / 100) for batches with completion date in this business week/year | _Batches |
| Production LY (HL) | Same formula for the previous year | _Batches |
| Customers Ordering | Count of unique customerId values with lineTotal > 0, excluding internal accounts | _Orders |
| Customers LY | Same count for the previous year | _Orders |
#dcfce7) if this year > last year#fecaca) if this year < last yearScope: One row per active (non-obsolete) beer product with either stock or projected demand.
| Metric | Formula |
|---|---|
| Stock (litres) | stockQty × product.volumeLitres |
| Projected weekly demand | SUM(projection weeks[currentWeek..52]) / (52 - currentWeek) |
| Weeks of stock | stockLitres / weeklyDemand, rounded to 1dp |
#fca5a5 strong red#fecaca red#fed7aa orange#fef3c7 amber#dcfce7 green (healthy)Sort order: Fewest weeks of stock first (most urgent at top).
Deduplication: By normalised full product name. Beers with a normalisation rule merge their variants; beers without keep separate rows per packaging format.
Row scope: Two rows per external customer (2026 row + 2025 row). Only customers with any orders in 2025 or 2026 are included.
Sort order: Total 2026 revenue descending.
| Cell | Formula |
|---|---|
| Week N (2026 row) | SUM(order.lineTotal) for this customer in business week N of 2026. Blank if zero. |
| Week N (2025 row) | Same for 2025. If total 2025 revenue = 0, all cells show a dash (-). |
Formatting: 2025 rows are italic with font colour #9ca3af. £#,##0 format. Frozen: 1 header row, 2 columns.
Row scope: One row per normalised product name with non-zero current-year YTD volume. Only beer products included.
Sort order: YTD volume descending.
| Metric | Formula |
|---|---|
| Volume (L) | SUM(order.quantity × product.volumeLitres) for matching orders |
| YoY (%) | ROUND(((YTD - YTD_LY) / YTD_LY) × 100) |
Row scope: One row per active beer (normalised name), computed from the last 4 gyles per beer.
| Metric | Formula |
|---|---|
| Avg Residency (days) | Mean of per-gyle max residencies. Residency = (datetimeDone - batch.datetimeStarted) in days. |
| Avg Yield (%) | totalVol / (totalVol + totalLost) across all 4 gyles |
| Avg ABV | (startingGravity - finalGravity) × 131.25 per gyle. No fallback to recipe ABV. |
| Total Vol (L) | SUM(volSuccessfulL) across all racking actions in the 4 gyles |
| Total Lost (L) | SUM(volLostL) across all racking actions in the 4 gyles |
| Brews Analysed | Count of gyles used (1-4) |
Batch data scope: Only batches with planned_start_date ≥ 2024-01-01. All batch statuses included (Planned, In Progress, Completed, Cancelled).
Every batch action (racking event) from _BatchActions, sorted by gyle number descending then datetimeDone ascending.
Row scope: All customers with 2+ weeks since their last order, excluding internal accounts.
Sort order: Weeks Without Order ascending, then All-Time Orders descending.
| Metric | Formula |
|---|---|
| Weeks Without Order | FLOOR((now - lastOrderDate) / (7 × 86400000)) |
| All-Time Orders | Count of unique orderNumber values |
| Avg Spend/Order | totalSpend / orderCount |
| Avg Spend/Week | totalSpend / activeWeekCount |
| Avg Spend/Month | totalSpend / MAX(activeWeekCount / 4.33, 1) |
| Total Spend (12m) | SUM(lineTotal) for orders in last 12 months |
| Retain | Boolean checkbox, controls forecast inclusion |
| Customer State | Previously on List? | Default Value |
|---|---|---|
| New, < 6 weeks gap | No | Checked (included) |
| New, ≥ 6 weeks gap | No | Unchecked (excluded) |
| Returning, any gap | Yes | Preserved (your last choice) |
#fefce8 light yellow#fef3c7 amber#fed7aa orange#fecaca red#fca5a5 strong redBefore clearing: readRetainOverrides_() reads col A + col H for ALL rows. After rebuild: hasOwnProperty check restores manual state; new customers get week-based default. If a customer orders again and drops below the 2-week threshold, they leave the tab entirely.
The Profiles tab has a horizontal three-section layout.
52 rows showing expected distribution of when new customers place orders across the year, based on 293 new customer orders from 2024. Weeks where percentage > 3% get an amber background.
Dynamic list of customers who have ordered in 2026 but had no orders in 2025. Includes an editable Est. Annual £ field (amber border), manual overrides feed into the forecast engine. Default estimate: actual 2026 revenue ÷ SUM(profile percentages for active weeks).
Interactive calculator: type an estimated annual volume and see projected weekly volume based on the profile curve.
Row scope: One row per normalised beer product name. Multiple product codes for the same beer are merged.
Sort order: Total projected volume descending.
| Cell | Formula |
|---|---|
| Week N | SUM(productProjection.weeks[N].vol) across all product codes for this beer, rounded to integer |
| 2026 Total | Sum of all 52 weeks |
Volume unit: Litres (integer, format #,##0 "L").
Row scope: One row per retained customer.
Sort order: Total projected revenue descending.
| Cell | Formula |
|---|---|
| Week N | customerProjection.weeks[N], rounded to 2dp |
| 2026 Total | Sum of all 52 weeks |
#f5f5f5), italic font (pre-onboarding weeks)The following accounts are excluded from all customer-facing analytics:
Acme
40FT Taproom
40FT Taprooms
40ft Brewery
40FT Brewery
Matching logic: Case-insensitive substring match via isExcludedAccount().
Non-beer products (no drinkName) are excluded from Product Detail, Forecast (Product), Stock Weeks, and Batch Tracking Summary.
The forecast engine (buildForecastProjection_()) projects 2025 trading data into a 52-week 2026 estimate.
Customers who have ordered in 2026 but have no 2025 orders are treated as new customers: always retained, always use Profile method, and support manual annual revenue overrides from the Profiles tab.
The Retain checkbox on the Lapsed & Lost tab is authoritative, the engine is a pure function of checkbox state.
| Customer State | On L&L Tab? | Retain | Forecast |
|---|---|---|---|
| Active (< 2 weeks gap) | No | N/A | Included |
| Lapsed (2–5 weeks) | Yes | Pre-checked ✓ | Included |
| Lapsed (2–5 weeks) | Yes | Manually unchecked ✗ | Excluded |
| Lost (6+ weeks) | Yes | Pre-unchecked ✗ | Excluded |
| Lost (6+ weeks) | Yes | Manually checked ✓ | Included |
| New 2026 customer | Maybe | Ignored | Always included |
| Condition | Method | Description |
|---|---|---|
| New 2026 customer (no 2025 orders) | Profile | Extrapolate from 2026 orders using profile curve |
| First 2025 order Weeks 1–13 | History | Full year of trading data: use actual weekly values directly |
| First 2025 order after Week 13 | Profile | Actual data for active weeks, profile curve for pre-onboarding weeks |
Cutoff: CONFIG.FORECAST_FULL_YEAR_CUTOFF_WEEK = 13
projectedWeek[w] = actual2025Revenue[w] || 0 (for w = 1..52)
activePctSum = SUM(profilePct[w-1]) for w = firstWeek..52annualRevenue = actualTotalRevenue / activePctSumprojectedWeek[w] = annualRevenue × profilePct[w-1]projectedWeek[w] = actualRevenue[w] || 0The same logic applies to product volumes (litres instead of £). For new 2026 customers with a manual revenue override, product volumes are scaled proportionally.
Business week alignment: All year-on-year comparisons use business week numbers, not calendar dates. Week 7 of 2026 is compared to Week 7 of 2025.
| Value | Meaning | Condition |
|---|---|---|
+N% | Revenue increased by N% | LY > 0, CY > LY |
-N% | Revenue decreased by N% | LY > 0, CY < LY |
0% | Flat | LY > 0, CY = LY |
New | New this year | LY = 0 across entire year, CY > 0 |
N/A | Not applicable | Product existed LY but no YTD sales |
| Blank | No data | CY = 0 |
- | No history | On Trade Sales: no 2025 orders at all |
The normaliseProductName() function merges variant product names using case-insensitive substring matching:
| If name contains | Normalised to |
|---|---|
"deep" | Deep, merges "Nitro Deep", "Deep Lager", "Deep - 30L Keg", etc. |
"dalston sunrise" | Dalston Sunrise, merges all variants |
| Neither pattern | Returned unchanged |
Applied in: Product Detail, Batch Tracking summary, Forecast (Product), and Stock Weeks.
All date-to-week conversions use Matthew's "first full week" convention (not ISO 8601):
CONFIG.FETCH_START_DATE = '2024-01-01').
Europe/London. The default (America/New_York) causes date-only ISO strings to be parsed incorrectly, shifting dates by a day.