commit 618bdcb444d908fd1b2ba680ff06d76c1ec4e3cc Author: SMEtools Date: Sun Jun 28 20:13:22 2026 +0300 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 Claude-Session: https://claude.ai/code/session_01SBytQq5dcASi4ETm7n6THb diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..51d84b4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +# secrets / per-engagement config — never publish +scripts/creds.json +scripts/demo_config.json +**/__pycache__/ +*.pyc diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..35f1fa4 --- /dev/null +++ b/LICENSE @@ -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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..0d9a35f --- /dev/null +++ b/README.md @@ -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 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. diff --git a/SKILL.md b/SKILL.md new file mode 100644 index 0000000..1217cec --- /dev/null +++ b/SKILL.md @@ -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 ", "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 , 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). diff --git a/demo_config.example.json b/demo_config.example.json new file mode 100644 index 0000000..81e38ba --- /dev/null +++ b/demo_config.example.json @@ -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" } + ] +} diff --git a/reference/odoo19-field-gotchas.md b/reference/odoo19-field-gotchas.md new file mode 100644 index 0000000..bcab272 --- /dev/null +++ b/reference/odoo19-field-gotchas.md @@ -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) | `` | `` | + +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 = `. **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. diff --git a/scripts/build_demo.py b/scripts/build_demo.py new file mode 100644 index 0000000..06f490c --- /dev/null +++ b/scripts/build_demo.py @@ -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) diff --git a/scripts/odoo_connect.py b/scripts/odoo_connect.py new file mode 100644 index 0000000..ef0ba78 --- /dev/null +++ b/scripts/odoo_connect.py @@ -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) diff --git a/templates/README.md b/templates/README.md new file mode 100644 index 0000000..1717666 --- /dev/null +++ b/templates/README.md @@ -0,0 +1,33 @@ +# Doc templates + +Three self-contained HTML docs the skill produces in step 8. Inline `brand.css` +into each (one ` + + +
+

{{CLIENT}} Demo — Step-by-Step

+

{{DATE_TIME}} · {{AUDIENCE}} · Odoo {{ODOO_VERSION}}

+

Follow top to bottom. ~25 min.

+
+ Login: {{ODOO_URL}} + Manager: {{ADMIN_LOGIN}} + Rep: {{REP_LOGIN}} / {{REP_PASSWORD}} +
+ + + + + + + + + + + +
#Do (click path)Say
1Frame it. Open the Sales app."{{FRAME_LINE}}"
2Clean order. Sales → Orders → New → {{CLEAN_CUSTOMER}} → add products → Confirm goes through"{{CLEAN_LINE}}"
3⭐ {{HERO1}}. {{HERO1_CLICKS}} BLOCKED"{{HERO1_LINE}}"
4⭐ Override. {{OVERRIDE_CLICKS}} confirmed"{{OVERRIDE_LINE}}"
5{{HERO2}}. {{HERO2_CLICKS}}"{{HERO2_LINE}}"
6Invoice. On a confirmed order → Create Invoice → Post."{{INVOICE_LINE}}"
7Close. Hand over the one-pager."{{CLOSE_LINE}}"
+
{{DATA_REF_CHIPS}}
+ +
+ +