Back to Genius hub

Production 3.0 → Genius forecast

How the Ship-Month Revenue Forecast works

A one-off report that turns the Production 3.0 spreadsheet into a forward-looking dollar forecast, bucketed by the month each order is expected to ship. It uses Genius Sales # as the join key into Genius ERP, and Genius Expected Ready to Ship as the date that decides which month an order's dollars land in.

Source Production 3.0 Google Sheet — 4 tabs
Join key Genius Sales #SalesOrderHeaderEntity.Code
Bucketing field Genius Expected Ready to Ship
Output Detail, Summary, By Product Line, Exceptions tabs

Where the data comes from

Production 3.0 is Nathan's shorthand name for the shop's order/planning/production tracking spreadsheet. This report reads four of its tabs — the ones that represent live production work:

TabWhat it tracks
ATT/ ASTAttachment / accessibility structure systems
CVR-AVCargo van ramps
SDRSliding door ramps
MISCMiscellaneous production items

Access is read-only end to end. The report authenticates as a dedicated [email protected] Google service account whose OAuth scope is spreadsheets.readonly — write attempts fail at the credential level, not just by convention.

The core idea: Genius Sales # is the index

Production 3.0 sheet row Customer: Lifegrip Solutions Product #: (multiple SKUs) Zoho #: 81940 Genius Sales #: 10540133 Ready to Ship: 8/14/26 index key Genius ERP lookup SalesOrderHeaderEntity filter: Code[10540133 Returns: TotalBeforeTaxes, CreationDate enrich Forecast row $2,046 pre-tax Ship Month: 2026-08 bucketed by sheet's Ready to Ship date Genius Sales # is the ONLY reliable join key. It maps 1:1 to SalesOrderHeaderEntity.Code — the master record every dollar amount, invoice, and shipment hangs off inside Genius. Note: the bucketing date comes from the SHEET (Ready to Ship), not from Genius. Genius only supplies the dollar amount.

Why the dollar amount is pre-tax only, with no shipping split

What we tested

We checked whether shipping/freight dollars could be broken out separately from product dollars for a forward-looking (still in production) order. Freight charges live on CustomerInvoiceHeaderEntity, joined to the sales order via CustomerOrder = SO.Code.

Tested a real in-production order (Consigli, SO 10535759, expected ship 4/22/26): the invoice query returned zero records. Tested a real already-shipped order (SO 10538619): the invoice existed with a full freight line item.

Conclusion

Invoice and freight data only exists once an order actually ships and gets invoiced. For a forward-looking forecast, that data is structurally unavailable for most rows — it isn't a bug, it's how the business process works.

The report shows pre-tax total only (SalesOrderHeaderEntity.TotalBeforeTaxes), with no product-vs-shipping split. This is documented as an explicit non-goal, not an oversight.

How multi-SKU orders are grouped into one line

A sliding door ramp order is never just one item — it's typically the ramp, a crossover section, a lifting stick, and sometimes a handrail system, all under one customer and one Genius Sales #. The sheet lays these out as visually merged blocks: only the first physical row carries Customer, Zoho #, Genius Sales #, Sales Rep, and dates. Every following row in that block has only the Product # column filled in — all other cells come back genuinely blank from the sheet's API, not carried-forward merge values.

Grouping rule A new order starts at any row where Customer or Genius Sales # is non-blank. Any row right after it with a blank Customer but a real Product # is a continuation of that same order — its Product # gets joined onto the order's product list with "; ". A fully blank row closes out the block.

Real example — Lifegrip Solutions, SDR tab

Sheet rowCustomerProduct #Genius Sales #
5Lifegrip Solutions1-FG-SDR083060SL-V410540133
6(blank)1-FG-SDC0529SL(blank)
7(blank)1-FG-TRLS30(blank)
8(blank)10-FG-HRDBA-SYS(blank)

Becomes one forecast line item:

Customer: Lifegrip Solutions
Genius Sales #: 10540133
Product: 1-FG-SDR083060SL-V4; 1-FG-SDC0529SL; 1-FG-TRLS30; 10-FG-HRDBA-SYS
Pre-Tax Total: $2,046
Ship Month: 2026-08
A bug this caught during development: the first version of the grouping logic closed a block on a blank separator row by simply discarding it, instead of saving it first. That silently dropped almost every real order — caught immediately because the SDR tab's order count collapsed from an expected ~21 down to 2. Fixed by saving the current block before clearing it on a blank row. Re-verified against live data afterward.

Not every look-alike case is a grouping bug

Some customer names repeat on the sheet without being one multi-SKU order. Example: "Monterey Peninsula Unified School District" appears three times on the ATT/AST tab, each with its own Zoho # (81624, 81753, 81754) and no Genius Sales # yet (placeholder rows, still at quote stage). These are three genuinely separate orders under the same customer name — correctly kept as three separate rows, not merged into one.

Why an order lands in Exceptions

Every row that can't be forecasted is kept, not silently dropped, so the total dollar universe stays reconcilable. There are four reasons a line item ends up in the Exceptions tab instead of the forecast:

No Genius Sales # on sheet most common Quote-stage or not-yet-entered orders. Placeholder rows like "Ramp System Place Holder" have a Zoho # but no Genius SO yet — there's nothing to look up.
Ready to Ship is blank Genius Sales # exists, but production hasn't entered an expected ship date yet. No date means no month to bucket it into.
Unparseable or implausible date Free-text notes instead of a date ("ON HOLD per BD", multi-line scheduling notes) or a real date typo, e.g. 9/11/69 instead of 9/11/26.
Genius Sales # not found in Genius The sheet lists a Genius Sales # that the live Genius API doesn't recognize — typo, cancelled SO, or a number that was never actually created in Genius.
Guard against the date-typo trap: ready-to-ship dates are only accepted if the parsed year falls between 2024 and 2029. A 2-digit-year typo like 9/11/69 would otherwise silently parse to the year 1969 and land in a garbage bucket instead of getting flagged — this exact case was caught on a live run.

What the output workbook looks like

TabWhat's in it
Detail One row per forecasted order, grouped by Source Tab, then by Ship Month within each tab. Subtotal after each tab+month group, a total after each tab, and a grand total at the bottom. All dollars: whole-dollar, comma-separated, right-justified.
Summary Ship Month × Source Tab matrix of dollar subtotals, with a month total column, a grand total row, and an exception-count breakdown by reason.
By Product Line Same Tab → Month grouping as Detail, but a leaner column set (Product, Customer, Genius Sales #, Pre-Tax Total, Ready to Ship) for scanning individual production items per tab per month.
Exceptions Full detail on every row that couldn't be forecasted, with the specific reason spelled out so it can be fixed on the sheet or in Genius.

First live run (2026-08-10)

136 raw sheet rows across the 4 tabs correctly collapsed into 70 real orders after grouping:

Source tabReal orders
ATT/ AST20
CVR-AV26
SDR21
MISC3

Of those, 59 line items forecasted cleanly (valid Genius Sales #, valid Genius match, parseable ready-to-ship date), for a total of $795,262 pre-tax across Ship Months 2026-04 through 2026-09. 11 rows landed in Exceptions across the four reasons above. Zero Genius API lookup failures.

Re-running the report

This is a one-off report right now — not scheduled or cron'd. To re-run it with current sheet data:

cd /Users/nathanshiba/.openclaw/workspace-forge
python3 scripts/production3_ship_forecast.py

# optional custom output path
python3 scripts/production3_ship_forecast.py --out /path/to/output.xlsx

Related pages

This report reuses the same Genius API access pattern as Order Lookup. See How Order Lookup Accesses Genius API for the underlying authentication and entity-fetch mechanics, and PO Number Field Overload Map for why PoNumber is never used as a production join key — only SalesOrderHeaderEntity.Code is safe for that.