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:
@@ -0,0 +1,5 @@
|
||||
# secrets / per-engagement config — never publish
|
||||
scripts/creds.json
|
||||
scripts/demo_config.json
|
||||
**/__pycache__/
|
||||
*.pyc
|
||||
@@ -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.
|
||||
@@ -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 1–2 "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 1–2 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.
|
||||
@@ -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 1–2 "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 3–5 pains in the client's own words** (quote them). These become the demo beats.
|
||||
- The **1–2 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).
|
||||
@@ -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" }
|
||||
]
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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.
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user