odoo-demo-architect: discovery -> live Odoo demo + sales docs, AI-built

Public skill for Claude Code. Mines a discovery call, researches native-vs-custom,
stands up a working Odoo demo over XML-RPC (modules + data + tested hero features),
and generates run-of-show, one-pager, and a 1-page demo script.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SBytQq5dcASi4ETm7n6THb
This commit is contained in:
SMEtools
2026-06-28 20:13:22 +03:00
co-authored by Claude Opus 4.8
commit 618bdcb444
11 changed files with 571 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
# secrets / per-engagement config — never publish
scripts/creds.json
scripts/demo_config.json
**/__pycache__/
*.pyc
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 SMEtools
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+67
View File
@@ -0,0 +1,67 @@
# Odoo Demo Architect
**Turn a discovery call into a complete, working Odoo demo — and the documents to sell it — with an AI agent doing the build.**
A [Claude Code](https://claude.com/claude-code) skill. Point it at a discovery transcript and a reachable Odoo instance; it mines the client's pains, researches what Odoo does natively vs. what needs building, stands up a **live database** (modules + realistic data + the 12 "hero" features that win the deal), **tests** that those features actually fire, and generates three sales documents: a **run-of-show**, a **one-pager** for the decision-maker, and a **one-page step-by-step demo script**.
Built from a real engagement where this produced a full Odoo 19 Enterprise demo — credit-limit hard-block with manager override, weight-based fleet load-building, EGP data, three sales docs — ready for a CEO meeting, with the founder barely touching the keyboard.
---
## What you get
- A **live Odoo demo DB** tailored to the client (any Odoo: Online, self-hosted, or Odoo.sh).
- **Working hero features**, no-code where possible, custom module only when necessary.
- A **verification log** proving each hero fired (real numbers).
- Three **self-contained HTML docs** (brand-themed): run-of-show, one-pager, 1-page script.
## Install
```bash
# clone into your Claude Code skills folder
git clone https://github.com/Smetools/odoo-demo-architect ~/.claude/skills/odoo-demo-architect
```
Or drop the folder anywhere Claude Code loads skills. Then just ask Claude: *"build an Odoo demo for <client> from this discovery call."*
## Prerequisites
- Claude Code.
- A reachable Odoo instance + an **API key** (Settings → My Profile → Account Security → New API Key).
- On **Odoo.sh**: create the project and enter any partner trial code first (web-only), then come back with the DB URL + API key.
- Optional but recommended: the **Perplexity MCP** (for the native-vs-custom research step). Web search works too.
## Quickstart
1. Put your Odoo creds in env vars (or `scripts/creds.json`):
`ODOO_URL`, `ODOO_DB`, `ODOO_LOGIN`, `ODOO_KEY`.
2. Verify the connection: `python scripts/odoo_connect.py` → prints `server_version`.
3. Copy `demo_config.example.json``scripts/demo_config.json`, fill it from the discovery call.
4. Ask Claude to run the pipeline (see `SKILL.md`), or run `python scripts/build_demo.py` for the data layer, then let the agent build + test the heroes and generate the docs.
## How it works (pipeline)
1. **Mine the discovery** — pains in the client's words; the 12 heroes.
2. **Research native-vs-custom** for each hero, for the client's exact Odoo version.
3. **Connect** over XML-RPC; confirm the version.
4. **Install** the scope's modules.
5. **Load realistic data** (idempotent; currency before invoices; pre-stage so heroes fire live).
6. **Build heroes** — no-code first, custom module as fallback.
7. **Test every hero** on a throwaway record; keep demo objects pristine.
8. **Generate the docs** from the brand templates.
9. **Hand off** — login, cheat-sheet, verification log, doc links.
Full procedure in [`SKILL.md`](SKILL.md).
## Read this before touching Odoo 19
Several core fields were renamed in v19 (`res.users.group_ids`, `res.groups` lost `category_id`, etc.), credit limit is **warning-only** natively, and fleet load-by-weight needs `stock_fleet`. All the footguns are in [`reference/odoo19-field-gotchas.md`](reference/odoo19-field-gotchas.md) so you don't relearn them the hard way.
## Files
```
SKILL.md the procedure (what the agent follows)
scripts/odoo_connect.py XML-RPC connector + helpers
scripts/build_demo.py config-driven data + hero builder (template)
demo_config.example.json per-client config shape
reference/odoo19-field-gotchas.md version-specific fields + footguns
templates/ brand.css + the three doc templates
```
## License
MIT. Use it, fork it, sell with it.
## Credit
Built by [SMEtools](https://www.smetools.io) — Odoo implementation + AI automation. Made with Claude Code.
+75
View File
@@ -0,0 +1,75 @@
---
name: odoo-demo-architect
description: Build a complete, client-tailored Odoo demo from a discovery call. Mines the client's pains, researches native-vs-custom for each hero feature, stands up a LIVE Odoo database (modules + realistic data + working hero features) over XML-RPC, tests it, and generates the sales docs (run-of-show, one-pager, step-by-step script). Use when prepping an Odoo sales demo, POC, or proof-of-value before a client meeting. Triggers: "build an Odoo demo", "demo DB for <client>", "Odoo proof of concept", "stand up a demo before the meeting".
---
# Odoo Demo Architect
Turn a discovery call into a polished, working Odoo demo + the documents to sell it — end to end, with minimal human steering. Built from a real engagement that won on first showing.
## What it produces
1. A **live Odoo database** with the client's modules installed, realistic industry data, and the 12 "hero" features that map to their pain — actually working, not slideware.
2. Three **sales documents** (brand-themed, self-contained HTML): a **run-of-show**, a **one-pager** for the decision-maker, and a **1-page step-by-step demo script**.
3. A **verification log** proving each hero feature fired (e.g. "credit block raised at 56,379 > 50,000; override confirmed").
## Inputs you need before starting
- **Discovery material** — a call transcript, recording, or notes describing the client's business + pains. (If using Knowcap, pull the transcript first.)
- **Agreed scope** — which Odoo areas are in Phase 1 (Sales, Inventory, Accounting, Fleet, etc.).
- **A reachable Odoo instance + credentials** — `URL`, `DB`, `login`, and an **API key** (Settings → My Profile → Account Security → New API Key). Works on Odoo Online, self-hosted, or Odoo.sh.
- *Odoo.sh note:* create the project + enter any partner trial code FIRST (web UI only — no API for project creation). Then proceed here once the DB is up and you have an API key.
## The pipeline (run in order)
### 1. Mine the discovery
From the transcript/notes, extract and write down:
- The **business shape** (entities, what they sell, who they sell to). Watch for "it's a group" that is really ONE company in Odoo — confirm chart-of-accounts/P&L/warehouse are shared.
- The **top 35 pains in the client's own words** (quote them). These become the demo beats.
- The **12 hero features** — the differentiators that justify the spend. Everything else is supporting flow.
### 2. Research native-vs-custom (don't assume)
For EACH hero feature, verify whether Odoo does it natively, no-code, or needs custom code — for the client's **exact Odoo version**. Use the Perplexity MCP (`perplexity_ask`) or web search. Ask precisely, e.g. *"In Odoo <ver>, is there a native hard-block (not warning) on SO confirmation over credit limit?"* Capture the verdict + citations. This step repeatedly overturns assumptions (e.g. credit limit is **warning-only** natively; v19 fleet dispatch IS native but only via `stock_fleet`).
### 3. Connect (XML-RPC)
Use `scripts/odoo_connect.py`. Authenticate, print `server_version` to confirm the major version (field names differ across versions — see `reference/odoo19-field-gotchas.md`). All build steps go through the `x(model, method, *args, **kwargs)` helper.
### 4. Install modules for the scope
`button_immediate_install` on the `ir.module.module` ids. Common set: `sale_management`, `stock`, `account_accountant`, `stock_delivery`, `stock_picking_batch`, `fleet`, `web_studio`, `base_automation`, `contacts`. For fleet load-by-weight on v19 also install `stock_fleet` (Stock Transport — adds vehicle capacity + batch dispatch). Module installs are heavy; allow several minutes.
### 5. Generate + load realistic demo data
Industry-appropriate, minimal but believable. Drive it from a `demo_config.json` (see `demo_config.example.json`) and `scripts/build_demo.py`. Rules:
- **Get-or-create by name** so re-runs are idempotent.
- Set the right **currency** first (activate it with `context={'active_test': False}`, set on `res.company`) — BEFORE posting any invoices (currency can't change once journal entries exist).
- Give products a **weight** if fleet is in scope. Give customers **credit limits** (`use_partner_credit_limit=True`, `credit_limit=...`) if credit is in scope.
- **Pre-stage state** to make heroes fire live: e.g. post an unpaid invoice to put a customer's receivable JUST under their limit, so a live order tips them over. Keep one demo quotation in **draft** for the live click.
- Seed stock with `inventory_mode` context so deliveries can reserve (go Ready).
### 6. Build the hero features — no-code first, module as fallback
- **Prefer no-code:** server actions / automated rules (`base.automation`) / Studio. Portable to any Odoo, no deploy.
- **Custom module only if needed:** generate a minimal module, push to the instance's git (Odoo.sh) or hand it to the user to deploy, then install + test. Keep modules tiny and version-correct.
- Example hero (credit hard-block + manager override): override `sale.order.action_confirm` to raise `UserError` when `partner.credit + amount_total > credit_limit` unless the user is in an approver group; add an "Override" button gated to that group. (No-code equivalent: a Studio approval rule on the Confirm button + a stored "over limit" flag.)
### 7. Test every hero over XML-RPC
On a THROWAWAY record: trigger the action, assert the block fires, assert the override path works, then delete the throwaway. Leave the real demo objects pristine. Print a verification log with the actual numbers.
### 8. Generate the sales docs
From `templates/` (genericized, brand-themed), produce three self-contained HTML files, filling in the client's data:
- **run-of-show** — beats with exact click-paths + what to say.
- **one-pager** — before/after, what's automated, investment, the path to go-live; branded for the *selling* company; decision-maker audience.
- **demo-script-1pager** — 7-ish numbered steps (Do + Say), prints on one A4.
### 9. Hand off
Output: the login, the pre-loaded data cheat-sheet, the verification log, and links to the three docs. Remind the user to rehearse once and keep the demo quotation in draft.
## Guardrails
- **Never invent that a feature works — test it.** Step 7 is mandatory; a demo hero that fails live loses the deal.
- **Research before building** (step 2). Version assumptions are the #1 source of wasted effort.
- **Don't hard-code the client.** Everything client-specific lives in `demo_config.json`.
- **Currency before invoices. Weight before fleet. Pre-stage before the live click.** Order matters.
- Read `reference/odoo19-field-gotchas.md` before touching Odoo 19 — several core fields were renamed.
## Files
- `scripts/odoo_connect.py` — XML-RPC connector + `x()` helper (correct context-kwarg passing).
- `scripts/build_demo.py` — config-driven data + hero builder (template to adapt per engagement).
- `demo_config.example.json` — the shape of a per-client config.
- `reference/odoo19-field-gotchas.md` — version-specific field names + footguns learned the hard way.
- `templates/` — the three doc templates (brand-themed, `{{placeholder}}` driven).
+46
View File
@@ -0,0 +1,46 @@
{
"_comment": "Per-client demo config. Fill from the discovery call. Generic food-distributor example below — replace with the client's real shape. Amounts in the chosen currency.",
"client": "Acme Distribution",
"currency": "EGP",
"use_credit_limit": true,
"scope_modules": ["sale_management", "stock", "account_accountant", "stock_delivery", "stock_picking_batch", "fleet", "stock_fleet", "web_studio", "base_automation"],
"products": [
{ "name": "Cooking Oil 16L", "weight": 15.0, "price": 1150 },
{ "name": "Rice 25kg", "weight": 25.0, "price": 1400 },
{ "name": "Sugar 50kg Sack", "weight": 50.0, "price": 1900 },
{ "name": "Frozen Carton 10kg","weight": 10.5, "price": 900 },
{ "name": "Canned Case 24x", "weight": 9.0, "price": 540 }
],
"customers": [
{ "name": "Grand Hotel", "credit_limit": 50000 },
{ "name": "Corner Restaurant", "credit_limit": 30000 },
{ "name": "Industrial Foods Co","credit_limit": 200000 }
],
"vehicles": [
{ "model": "Small Truck", "category": "Small Truck (1 ton)", "plate": "DEMO-1001", "capacity_kg": 1000 },
{ "model": "Large Truck", "category": "Large Truck (3 ton)", "plate": "DEMO-2002", "capacity_kg": 3000 }
],
"pricelist": {
"name": "B2B HoReCa",
"percent": 5.0,
"assign_to": ["Grand Hotel", "Corner Restaurant"]
},
"prestage_receivables": [
{ "_comment": "Put Grand Hotel JUST under its 50000 limit so a live order tips it over (the credit-block hero).",
"customer": "Grand Hotel", "amount": 46000 },
{ "customer": "Corner Restaurant", "amount": 5000 },
{ "customer": "Industrial Foods Co", "amount": 20000 }
],
"heroes": [
{ "name": "credit-limit hard block + manager override",
"approach": "no-code-first: Studio approval on Confirm, or custom action_confirm override; gate override to an approver group" },
{ "name": "fleet load by weight",
"approach": "native v19 via stock_fleet: vehicle capacity on fleet.vehicle.model.category, batch shows used_weight_percentage" }
]
}
+45
View File
@@ -0,0 +1,45 @@
# Odoo 19 (Enterprise) — field names & footguns
Hard-won notes from a real v19 demo build over XML-RPC. Check these before you waste a build cycle. Always confirm the major version first: `common.version()["server_version"]`.
## Renamed core fields (these break v17/v18 code)
| Concept | v17/18 | **v19** |
|---|---|---|
| User's groups | `groups_id` | **`group_ids`** |
| Group's app category | `category_id` on `res.groups` | **removed** — use `privilege_id` (model `res.groups.privilege`), or just omit |
| (so in security XML) | `<field name="groups_id" ...>` | `<field name="group_ids" ...>` |
A custom module that sets `category_id` on `res.groups` or `groups_id` on `res.users` will fail to install on v19 with `Invalid field ... / Cannot ...`. Drop the category or use `privilege_id`.
## fields_get returns null attributes over XML-RPC (some v19 builds)
`fields_get([], {"attributes": ["string","type"]})` may return each field with `string=None`. The **keys (technical names) are still correct** — match on keys, not labels. (`fields_named()` in `odoo_connect.py` does this.)
## Product model (v19)
- Goods that track stock: `type="consu"` **plus** `is_storable=True` (the old `type="product"` is gone).
- Weight: `product.template.weight` (kg by default).
## Credit limit (Accounting)
- Native behaviour is **WARNING ONLY** — it does NOT block SO confirmation. A hard block needs a Studio approval rule or a custom `action_confirm` override.
- Enable: `res.config.settings.account_use_credit_limit = True` (+ `account_default_credit_limit`). Apply via `create` then `execute`.
- Per-customer: `res.partner.use_partner_credit_limit = True`, `credit_limit = <amount>`. **These are non-stored** — you cannot `search` on `use_partner_credit_limit` (Postgres "not stored" error); read it per-record instead.
- `res.partner.credit` = current Total Receivable (computed from posted AR moves). Pre-stage it by posting an unpaid customer invoice.
## Fleet load-by-weight / Dispatch (v19) — IS native, but gated
- Native `fleet` alone = vehicle cost/maintenance; **no capacity fields**.
- Install **`stock_fleet`** ("Stock Transport"). It adds the dispatch fields to `stock.picking.batch`: `vehicle_id`, `vehicle_category_id`, `vehicle_weight_capacity`, `used_weight_percentage`, `estimated_shipping_weight`, `has_dispatch_management`.
- Capacity lives on **`fleet.vehicle.model.category`** → fields `weight_capacity`, `volume_capacity`. Create a category with capacity, assign it to the vehicle (`fleet.vehicle.category_id`), then a batch with that vehicle computes `used_weight_percentage`.
- Demo flow: confirm SOs → deliveries → create `stock.picking.batch` with `vehicle_id` + `picking_ids` → read `used_weight_percentage`.
## Currency
- New databases ship most currencies **inactive**. Find with `search([["name","=","EGP"]], context={"active_test": False})`, set `active=True`, then `res.company.currency_id`.
- Change company currency **BEFORE** posting any invoices/journal entries — Odoo blocks the switch once entries exist.
## Stock seeding (so deliveries go Ready)
- Create `stock.quant` with `inventory_quantity` and `context={"inventory_mode": True}`, then call `action_apply_inventory` with the same context. The apply may warn harmlessly if already applied; check by reserving (`stock.picking.action_assign` → state `assigned`).
## XML-RPC helper footgun
- `execute_kw(..., method, args, kwargs)`: a **context dict must go in kwargs**, never as a positional arg. `search(domain, {"active_test":False})` sends the dict as `offset``psycopg2 can't adapt type 'dict'`. Pass `context=` as a keyword (see `x()` in `odoo_connect.py`).
## Module install over XML-RPC
- `x("ir.module.module","button_immediate_install",[ids])`. Heavy — let it run minutes. On a failed XML-data load it rolls back cleanly (module stays uninstalled); fix and retry.
- After deploying a custom module to git (Odoo.sh), wait for the rebuild, then `x("ir.module.module","update_list")` and search for it before installing.
+107
View File
@@ -0,0 +1,107 @@
"""Config-driven Odoo demo data builder (template — adapt per engagement).
Reads demo_config.json (see demo_config.example.json) and creates:
currency, products (with weight), customers (with credit limits),
vehicle categories + vehicles (with capacity), a B2B pricelist,
and pre-staged receivables (posted unpaid invoices) so heroes fire live.
Idempotent: get-or-create by name. Run order matters (currency before invoices).
Requires odoo_connect.py in the same folder + creds (env or creds.json).
"""
import json, os
from odoo_connect import x, get_or_create, URL, DB
CFG = os.path.join(os.path.dirname(os.path.abspath(__file__)), "demo_config.json")
cfg = json.load(open(CFG, encoding="utf-8"))
# 1. currency (must run before invoices)
cur = cfg.get("currency")
if cur:
ids = x("res.currency", "search", [["name", "=", cur]], context={"active_test": False})
if not ids:
ids = [x("res.currency", "create", {"name": cur, "rounding": 0.01})]
x("res.currency", "write", ids, {"active": True})
comp = x("res.company", "search", [], limit=1)[0]
try:
x("res.company", "write", [comp], {"currency_id": ids[0]})
except Exception as e:
print(" ! currency switch blocked (invoices exist?):", str(e)[:120])
print("currency:", cur)
# 2. settings
s = x("res.config.settings", "create", {
"account_use_credit_limit": bool(cfg.get("use_credit_limit", True)),
"group_product_pricelist": True,
})
x("res.config.settings", "execute", [s])
# 3. products
prod = {}
for p in cfg.get("products", []):
vals = {"name": p["name"], "list_price": p.get("price", 0.0),
"weight": p.get("weight", 0.0), "type": "consu", "is_storable": True}
try:
prod[p["name"]] = get_or_create("product.template", [["name", "=", p["name"]]], vals)
except Exception:
vals.pop("is_storable", None)
prod[p["name"]] = get_or_create("product.template", [["name", "=", p["name"]]], vals)
print("products:", len(prod))
# 4. vehicle categories + vehicles
brand = None
if cfg.get("vehicles"):
brand = get_or_create("fleet.vehicle.model.brand", [["name", "=", "Demo Fleet"]], {"name": "Demo Fleet"})
for v in cfg.get("vehicles", []):
cat = get_or_create("fleet.vehicle.model.category", [["name", "=", v["category"]]],
{"name": v["category"], "weight_capacity": v.get("capacity_kg", 0.0)})
model = get_or_create("fleet.vehicle.model", [["name", "=", v["model"]]],
{"name": v["model"], "brand_id": brand, "category_id": cat})
get_or_create("fleet.vehicle", [["license_plate", "=", v["plate"]]],
{"model_id": model, "license_plate": v["plate"], "category_id": cat})
print("vehicles:", len(cfg.get("vehicles", [])))
# 5. customers with credit limits
cust = {}
for c in cfg.get("customers", []):
cust[c["name"]] = get_or_create("res.partner", [["name", "=", c["name"]]],
{"name": c["name"], "is_company": True, "customer_rank": 1,
"use_partner_credit_limit": True, "credit_limit": c.get("credit_limit", 0.0)})
print("customers:", len(cust))
# 6. B2B pricelist (optional global % discount)
pl_cfg = cfg.get("pricelist")
if pl_cfg:
pl = get_or_create("product.pricelist", [["name", "=", pl_cfg["name"]]], {"name": pl_cfg["name"]})
if pl_cfg.get("percent") and not x("product.pricelist.item", "search", [["pricelist_id", "=", pl]]):
x("product.pricelist.item", "create", {"pricelist_id": pl, "applied_on": "3_global",
"compute_price": "percentage", "percent_price": pl_cfg["percent"]})
for cname in pl_cfg.get("assign_to", []):
if cname in cust:
x("res.partner", "write", [cust[cname]], {"property_product_pricelist": pl})
# 7. pre-stage receivables (post unpaid invoices)
def first_variant(tmpl_id):
return x("product.product", "search", [["product_tmpl_id", "=", tmpl_id]], limit=1)[0]
any_prod = first_variant(next(iter(prod.values()))) if prod else None
for stage in cfg.get("prestage_receivables", []):
cid = cust.get(stage["customer"])
if not cid:
continue
mv = x("account.move", "create", {
"move_type": "out_invoice", "partner_id": cid,
"invoice_line_ids": [(0, 0, {"product_id": any_prod, "quantity": 1,
"price_unit": stage["amount"], "tax_ids": [(6, 0, [])]})]})
try:
x("account.move", "action_post", [mv])
except Exception as e:
print(" ! post for", stage["customer"], str(e)[:100])
# 8. verify credit headroom
print("\n-- credit headroom --")
for c in cfg.get("customers", []):
cid = cust[c["name"]]
r = x("res.partner", "read", [cid], ["credit", "credit_limit"])[0]
flag = "OVER" if r["credit"] > r["credit_limit"] else "ok"
print(f" {c['name']:30} owes={r['credit']:>12,.0f} limit={r['credit_limit']:>12,.0f} {flag}")
print("\nDONE. Instance:", URL, "DB:", DB)
+76
View File
@@ -0,0 +1,76 @@
"""Generic Odoo XML-RPC connector for odoo-demo-architect.
Reads credentials from environment variables (or a creds.json next to this file):
ODOO_URL e.g. https://my-instance.odoo.com (no trailing slash)
ODOO_DB database name
ODOO_LOGIN user login (often an email; "admin" on Odoo.sh trials)
ODOO_KEY an API key (Settings > My Profile > Account Security > New API Key)
Usage:
from odoo_connect import x, common, UID, URL, DB
UID # authenticated user id
common.version() # confirm server_version BEFORE building (field names vary by version)
x("res.partner", "search_read", [["customer_rank", ">", 0]], fields=["name"])
The x() helper passes keyword args straight into execute_kw's kwargs, so pass
Odoo kwargs natively: x(model, "search", domain, limit=5, context={"active_test": False})
Do NOT pass a context dict positionally — that becomes `offset` and Postgres errors.
"""
import os, json, ssl, xmlrpc.client
_here = os.path.dirname(os.path.abspath(__file__))
def _cfg(key, default=None):
v = os.environ.get(key)
if v:
return v
path = os.path.join(_here, "creds.json")
if os.path.exists(path):
with open(path) as f:
return json.load(f).get(key, default)
return default
URL = (_cfg("ODOO_URL") or "").rstrip("/")
DB = _cfg("ODOO_DB")
LOGIN = _cfg("ODOO_LOGIN") or "admin"
KEY = _cfg("ODOO_KEY")
if not (URL and DB and KEY):
raise SystemExit("Set ODOO_URL, ODOO_DB, ODOO_KEY (and optionally ODOO_LOGIN) "
"as env vars or in creds.json next to this script.")
_ctx = ssl.create_default_context()
common = xmlrpc.client.ServerProxy(f"{URL}/xmlrpc/2/common", context=_ctx, allow_none=True)
UID = common.authenticate(DB, LOGIN, KEY, {})
if not UID:
raise SystemExit("Authentication failed — check DB / LOGIN / API key.")
models = xmlrpc.client.ServerProxy(f"{URL}/xmlrpc/2/object", context=_ctx, allow_none=True)
def x(model, method, *args, **kwargs):
"""execute_kw wrapper. Positional args -> Odoo positional params;
keyword args (limit, fields, context, ...) -> Odoo kwargs."""
return models.execute_kw(DB, UID, KEY, model, method, list(args), kwargs)
def get_or_create(model, domain, vals, label=None):
"""Idempotent create: update if a record matches `domain`, else create."""
ids = x(model, "search", domain, limit=1)
if ids:
x(model, "write", ids, vals)
return ids[0]
return x(model, "create", vals)
def fields_named(model, *needles):
"""Return field technical names on `model` whose name/label contains any needle.
NOTE: on some Odoo 19 builds fields_get attribute VALUES return null over XML-RPC,
so match on the technical KEYS, which are always reliable."""
keys = list(x(model, "fields_get", [], {}).keys())
nl = [n.lower() for n in needles]
return sorted(k for k in keys if any(n in k.lower() for n in nl))
if __name__ == "__main__":
print("server_version:", common.version().get("server_version"))
print("uid:", UID, "url:", URL, "db:", DB)
+33
View File
@@ -0,0 +1,33 @@
# Doc templates
Three self-contained HTML docs the skill produces in step 8. Inline `brand.css`
into each (one `<style>` block, no external deps except Google Fonts). Replace the
footer brand with the SELLING company. Fill `{{placeholders}}` from the engagement.
## 1. `run-of-show` (internal — the presenter drives from this)
Sections, in order:
- **Hero band**: client × your-company, date/time, audience, environment.
- **The one-line story** (lead): mirror the client's pain back as one sentence.
- **Status grid** (4 cards): what's built + tested (module live, fleet %, currency/data counts, staged quote).
- **Pain → demo beat** table (their words → the beat that kills it → status pill).
- **Run-of-show** rows: each beat = time, what to DO (exact click-path), what to SAY.
- **Pre-loaded data** cheat-sheet (customers/limits, products/weights, vehicles/capacity, ready objects).
- **Risks & tips** + a **close script** (dark card).
## 2. `one-pager` (external — leave-behind for the decision-maker)
- Hero band branded for YOUR company; audience = the economic buyer.
- **Today vs. with Odoo** before/after table.
- **What gets automated** (4 bullets, each tied to a pain).
- **Investment & timeline** grid (price, all-phases, duration, payment terms) — use the SOW's agreed figures/currency.
- **Path to go-live** step grid (demo = HERE).
- A dark "why Enterprise / why custom" card.
## 3. `demo-script-1pager` (internal — the one-page follow-along)
Compact, prints on one A4. A login strip, then a numbered table: `# | Do (click path) | Say`.
~7 steps: frame → clean order → ⭐hero block → ⭐override → fleet load → invoice → close.
Hero rows highlighted. Footer ref strip with the staged data. See
`demo-script-1pager.template.html`.
## Placeholders used
`{{CLIENT}}` `{{SELLER}}` `{{DATE_TIME}}` `{{AUDIENCE}}` `{{ODOO_URL}}` `{{ADMIN_LOGIN}}`
`{{PRICE}}` `{{ALL_PHASES}}` `{{DURATION}}` `{{PAYMENT_TERMS}}` and per-row step content.
+40
View File
@@ -0,0 +1,40 @@
/* Reusable brand theme for odoo-demo-architect docs.
Light, blue #005EFF, navy #191F2E, Inter + Space Grotesk. Inline this into each
self-contained HTML doc (no external deps except Google Fonts). Swap the brand
footer per the SELLING company. */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Space+Grotesk:wght@500;600;700&display=swap');
:root{--blue:#005EFF;--blue-dark:#0046bd;--blue-light:rgba(0,94,255,0.08);--cyan:#00C6FF;
--navy:#191F2E;--txt:#191F2E;--dim:#414651;--faint:#6b7280;--bg:#F5F7FA;
--hero:radial-gradient(circle at 50% 0%,#E3F2FD 0%,#F8FAFC 70%);--surface:#fff;--line:#e5e9f0;
--ok:#16a34a;--warn:#d97706;--bad:#dc2626;--sh-sm:0 2px 4px rgba(0,0,0,.02);
--sh-md:0 12px 24px -6px rgba(0,0,0,.05);--r:16px;--r-sm:10px;}
*{box-sizing:border-box}
body{margin:0;background:var(--bg);color:var(--txt);font:15px/1.55 Inter,system-ui,sans-serif;-webkit-font-smoothing:antialiased}
.wrap{max-width:1000px;margin:0 auto;padding:36px 28px 60px}
h1,h2,h3{font-family:'Space Grotesk',sans-serif;color:var(--navy);letter-spacing:-.02em;line-height:1.2}
h1{font-size:32px;margin:0 0 6px;font-weight:700}
h2{font-size:20px;margin:34px 0 12px;font-weight:600;border-bottom:1px solid var(--line);padding-bottom:8px}
h3{font-size:15px;margin:18px 0 6px;font-weight:600;color:var(--blue)}
.sub{color:var(--dim);margin:0 0 4px;font-size:15px}.src{color:var(--faint);font-size:12px;margin:0 0 22px}
a{color:var(--blue);text-decoration:none}
.gradient-text{background:linear-gradient(135deg,var(--blue),var(--cyan));-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent}
.label{font-family:'Space Grotesk',sans-serif;font-size:11px;font-weight:700;color:var(--blue);text-transform:uppercase;letter-spacing:.1em;background:var(--blue-light);padding:4px 12px;border-radius:100px;display:inline-block;margin-bottom:14px}
.lead{background:#fff;border:1px solid var(--line);border-left:4px solid var(--blue);padding:14px 18px;border-radius:var(--r-sm);margin:16px 0;box-shadow:var(--sh-sm)}
.grid{display:grid;grid-template-columns:repeat(4,1fr);gap:14px;margin:22px 0}
.card{background:#fff;border:1px solid var(--line);border-radius:var(--r);padding:18px;box-shadow:var(--sh-sm)}
.card .n{font-family:'Space Grotesk',sans-serif;font-size:27px;font-weight:700;color:var(--navy)}.card .n.blue{color:var(--blue)}.card .l{color:var(--dim);font-size:13px;margin-top:3px}
.card-dark{background:var(--navy);color:#fff;border-radius:var(--r);padding:22px 24px;box-shadow:var(--sh-md);margin:18px 0}.card-dark h3{color:#fff}.card-dark p{color:#cbd5e1;margin:6px 0 0}
table{width:100%;border-collapse:collapse;font-size:14px;background:#fff;border:1px solid var(--line);border-radius:var(--r-sm);overflow:hidden}
th,td{text-align:left;padding:10px 12px;border-bottom:1px solid var(--line);vertical-align:top}
th{color:var(--faint);font-weight:600;font-size:11px;text-transform:uppercase;letter-spacing:.04em;background:#fafbfc}tr:last-child td{border-bottom:none}
.pill{display:inline-block;padding:2px 10px;border-radius:100px;font-size:11.5px;font-weight:600}.pill.live{background:#dcfce7;color:#15803d}.pill.bad{background:#fee2e2;color:#b91c1c}
.box{background:#fff;border:1px solid var(--line);border-left:4px solid var(--ok);border-radius:var(--r-sm);padding:12px 16px;margin:10px 0;box-shadow:var(--sh-sm)}.box.warn{border-left-color:var(--warn)}.box.info{border-left-color:var(--blue)}
.steps{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin:18px 0}
.step{background:#fff;border:1px solid var(--line);border-radius:var(--r);padding:16px;position:relative;box-shadow:var(--sh-sm)}.step.here{border-color:var(--blue);box-shadow:0 0 0 1px var(--blue) inset}
.step .num{font-family:'Space Grotesk',sans-serif;font-size:11px;color:var(--faint);font-weight:700;text-transform:uppercase}.step .ttl{font-family:'Space Grotesk',sans-serif;font-size:15px;font-weight:700;color:var(--navy);margin:4px 0 6px}.step .d{font-size:12.5px;color:var(--dim)}
.step.here .badge{position:absolute;top:-10px;right:10px;background:var(--blue);color:#fff;font-size:11px;font-weight:700;padding:2px 9px;border-radius:100px}
code{background:var(--blue-light);border:1px solid #d6e4ff;border-radius:5px;padding:1px 6px;font:12.5px ui-monospace,Consolas,monospace;color:var(--blue-dark)}
footer{margin-top:40px;color:var(--faint);font-size:12px;border-top:1px solid var(--line);padding-top:16px}
.hero{background:var(--hero);padding:44px 28px 32px}.hero .wrap{padding:0;max-width:1000px;margin:0 auto}
@media(max-width:760px){.grid,.steps{grid-template-columns:1fr 1fr}h1{font-size:25px}}
@media print{body{background:#fff}.wrap{padding:0}@page{size:A4;margin:12mm}}
@@ -0,0 +1,56 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{CLIENT}} Demo — Step-by-Step (1 page)</title>
<style>
/* >>> inline templates/brand.css here <<< plus the small extras below */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Space+Grotesk:wght@600;700&display=swap');
:root{--blue:#005EFF;--cyan:#00C6FF;--navy:#191F2E;--txt:#191F2E;--dim:#414651;--faint:#6b7280;--bg:#F5F7FA;--line:#e5e9f0;--ok:#16a34a;--warn:#d97706;--bad:#dc2626;--blue-light:rgba(0,94,255,0.08)}
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--txt);font:13px/1.45 Inter,system-ui,sans-serif}
.wrap{max-width:920px;margin:0 auto;padding:22px 26px 30px}
h1{font-family:'Space Grotesk',sans-serif;font-size:23px;margin:0 0 2px;color:var(--navy);letter-spacing:-.02em}
.sub{color:var(--dim);font-size:13px;margin:0 0 2px}.src{color:var(--faint);font-size:11px;margin:0 0 12px}
.gradient-text{background:linear-gradient(135deg,var(--blue),var(--cyan));-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent}
.bar{display:flex;flex-wrap:wrap;gap:8px;background:#fff;border:1px solid var(--line);border-left:4px solid var(--blue);border-radius:8px;padding:9px 12px;margin:0 0 14px;font-size:12px}
.bar b{color:var(--navy)}code{background:var(--blue-light);border:1px solid #d6e4ff;border-radius:4px;padding:1px 5px;font:11.5px ui-monospace,Consolas,monospace;color:#0046bd}
table{width:100%;border-collapse:collapse;background:#fff;border:1px solid var(--line);border-radius:8px;overflow:hidden}
th,td{text-align:left;padding:8px 10px;border-bottom:1px solid var(--line);vertical-align:top}
th{color:var(--faint);font-weight:600;font-size:10.5px;text-transform:uppercase;background:#fafbfc}tr:last-child td{border-bottom:none}
td.n{font-family:'Space Grotesk',sans-serif;font-weight:700;color:var(--blue);width:26px;text-align:center;font-size:15px}
td.do{width:46%}td.do b{color:var(--navy)}td.say{color:var(--dim);font-style:italic}
tr.hero td{background:#fffdf6}tr.hero td.n{color:var(--warn)}
.tag{display:inline-block;font-size:10px;font-weight:700;padding:1px 7px;border-radius:100px;margin-left:4px}.tag.block{background:#fee2e2;color:#b91c1c}.tag.ok{background:#dcfce7;color:#15803d}
.ref{display:flex;flex-wrap:wrap;gap:14px;margin-top:12px;font-size:11.5px;color:var(--dim)}.ref div{background:#fff;border:1px solid var(--line);border-radius:8px;padding:7px 10px}.ref b{color:var(--navy)}
footer{margin-top:12px;color:var(--faint);font-size:10.5px;border-top:1px solid var(--line);padding-top:8px}
@media print{body{background:#fff}.wrap{padding:0}@page{size:A4;margin:12mm}}
</style>
</head>
<body>
<div class="wrap">
<h1>{{CLIENT}} Demo — <span class="gradient-text">Step-by-Step</span></h1>
<p class="sub">{{DATE_TIME}} · {{AUDIENCE}} · Odoo {{ODOO_VERSION}}</p>
<p class="src">Follow top to bottom. ~25 min.</p>
<div class="bar">
<span><b>Login:</b> <code>{{ODOO_URL}}</code></span>
<span><b>Manager:</b> <code>{{ADMIN_LOGIN}}</code></span>
<span><b>Rep:</b> <code>{{REP_LOGIN}}</code> / <code>{{REP_PASSWORD}}</code></span>
</div>
<table>
<thead><tr><th>#</th><th>Do (click path)</th><th>Say</th></tr></thead>
<tbody>
<tr><td class="n">1</td><td class="do"><b>Frame it.</b> Open the Sales app.</td><td class="say">"{{FRAME_LINE}}"</td></tr>
<tr><td class="n">2</td><td class="do"><b>Clean order.</b> Sales → Orders → New → {{CLEAN_CUSTOMER}} → add products → Confirm <span class="tag ok">goes through</span></td><td class="say">"{{CLEAN_LINE}}"</td></tr>
<tr class="hero"><td class="n">3</td><td class="do"><b>⭐ {{HERO1}}.</b> {{HERO1_CLICKS}} <span class="tag block">BLOCKED</span></td><td class="say">"{{HERO1_LINE}}"</td></tr>
<tr class="hero"><td class="n">4</td><td class="do"><b>⭐ Override.</b> {{OVERRIDE_CLICKS}} <span class="tag ok">confirmed</span></td><td class="say">"{{OVERRIDE_LINE}}"</td></tr>
<tr><td class="n">5</td><td class="do"><b>{{HERO2}}.</b> {{HERO2_CLICKS}}</td><td class="say">"{{HERO2_LINE}}"</td></tr>
<tr><td class="n">6</td><td class="do"><b>Invoice.</b> On a confirmed order → Create Invoice → Post.</td><td class="say">"{{INVOICE_LINE}}"</td></tr>
<tr><td class="n">7</td><td class="do"><b>Close.</b> Hand over the one-pager.</td><td class="say">"{{CLOSE_LINE}}"</td></tr>
</tbody>
</table>
<div class="ref">{{DATA_REF_CHIPS}}</div>
<footer>Rehearse once, leave the demo quote in draft · have a hotspot backup · {{SELLER}} × {{CLIENT}}</footer>
</div>
</body>
</html>