{"id":"ddc3148a-9dd0-406a-958e-b30651ba8044","entityType":"agent","slug":"clawhub-zmtucker-drivethru-odoo","name":"drivethru-odoo","canonicalUrl":"https://www.xpersona.co/agent/clawhub-zmtucker-drivethru-odoo","canonicalPath":"/agent/clawhub-zmtucker-drivethru-odoo","generatedAt":"2026-10-10T03:57:21.277Z","source":"CLAWHUB","claimStatus":"UNCLAIMED","verificationTier":"NONE","summary":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-09T20:33:23.739Z","emptyReason":null},"description":"Talk to an Odoo ERP through its `drivethru_mcp` MCP server — discover the available Odoo tools at runtime and call them to look up eBay products/inventory, push eBay orders and read tracking, run the Accounts Payable PO→vendor-bill flow, review documents in the Documents app against their purchase orders and fix incorrect PO line pricing (the \"check the Purchasing folder against the POs\" / vendor-invoice pricing-review workflow, filing each document into Matched or Questions), schedule MRP production batches, drive vendor replenishment purchasing (run the replenishment report → curate lines → add to a PO → hand style/color/size/qty to the vendor's purchasing skill → write pricing + confirmation back and confirm the PO), and retrieve internal SOPs / best practices / policies from the Knowledge base scoped to the asking person's permissions. Use whenever the user needs to read from or write to Odoo, especially when you are answering a person inside an Odoo Discuss conversation.","descriptionLabel":"Source description","evidenceSummary":"Capability contract not published. No trust telemetry is available yet. 2K downloads reported by the source. Last updated 10/9/2026.","installCommand":"clawhub skill install s17fq291581evd78xzn1930b0n87bfny:drivethru-odoo","sourceUrl":"https://clawhub.ai/zmtucker/drivethru-odoo","homepage":"https://clawhub.ai/zmtucker/skills/drivethru-odoo","primaryLinks":[{"label":"View on ClawHub","url":"https://clawhub.ai/zmtucker/drivethru-odoo","kind":"source"},{"label":"Homepage","url":"https://clawhub.ai/zmtucker/skills/drivethru-odoo","kind":"homepage"}],"safetyScore":84,"overallRank":62,"popularityScore":66,"trustScore":null,"claimedByName":null,"isOwner":false,"seoDescription":"drivethru-odoo technical dossier on Xpersona with agent coverage, OPENCLEW support, and live trust metadata."},"coverage":{"evidence":{"source":"public-profile","verified":false,"confidence":"medium","updatedAt":"2026-10-09T20:33:23.739Z","emptyReason":null},"protocols":[{"protocol":"OPENCLEW","label":"OpenClaw","status":"self-declared","notes":"Declared in the public agent profile."}],"capabilities":[],"verifiedCount":0,"selfDeclaredCount":1,"capabilityMatrix":{"rows":[{"key":"OPENCLEW","type":"protocol","support":"unknown","confidenceSource":"profile","notes":"Listed on profile"}],"flattenedTokens":"protocol:OPENCLEW|unknown|profile"}},"adoption":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-09T20:33:23.739Z","emptyReason":null},"stars":null,"forks":null,"downloads":2021,"packageName":null,"latestVersion":"0.9.4","tractionLabel":"2K downloads"},"release":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-09T20:33:23.738Z","emptyReason":null},"lastUpdatedAt":"2026-10-09T20:33:23.739Z","lastCrawledAt":"2026-10-09T20:33:23.738Z","lastIndexedAt":null,"nextCrawlAt":"2026-10-10T20:33:23.738Z","lastVerifiedAt":null,"highlights":[{"version":"0.9.4","createdAt":"2026-09-17T21:58:23.073Z","changelog":"drivethru-odoo 0.9.4 - Updated documentation in SKILL.md for clarity and precision. - Incremented version number to 0.9.4.","fileCount":14,"zipByteSize":43305},{"version":"0.9.3","createdAt":"2026-09-17T14:43:31.434Z","changelog":"drivethru-odoo v0.9.3 - Updated documentation in SKILL.md to clarify usage instructions for native and shell-based tool access. - Improved step-by-step guidance for testing MCP connectivity and troubleshooting. - No changes to core functionality—documentation and workflow updates only.","fileCount":14,"zipByteSize":43084},{"version":"0.9.2","createdAt":"2026-09-17T13:58:30.791Z","changelog":"drivethru-odoo 0.9.2 - Updated SKILL.md version to 0.9.2 with minor metadata changes. - Removed obsolete skill-card.md file. - Minor internal updates to scripts/odoo_mcp.py (details not specified).","fileCount":14,"zipByteSize":42762},{"version":"0.9.1","createdAt":"2026-09-02T18:21:28.894Z","changelog":"drivethru-odoo v0.9.1 - Updated documentation in SKILL.md for clarity and accuracy. - Removed the redundant skill-card.md file. - Minor scripts/odoo_mcp.py adjustments (details not specified).","fileCount":14,"zipByteSize":42507},{"version":"0.9.0","createdAt":"2026-08-20T19:33:46.150Z","changelog":"drivethru-odoo 0.9.0 - Updated documentation in SKILL.md and production_scheduling.md for clarity and maintainability. - Removed skill-card.md for a streamlined skill package. - No changes to core functionality.","fileCount":14,"zipByteSize":42173},{"version":"0.7.0","createdAt":"2026-08-20T17:48:19.516Z","changelog":"Version 0.7.0 - Added the new `production_set_run_order` tool to the Production tool suite. - Updated documentation to include the expanded Production scheduling capabilities. - Removed the legacy skill-card.md file.","fileCount":14,"zipByteSize":41508},{"version":"0.6.0","createdAt":"2026-07-21T23:53:32.306Z","changelog":"## drivethru-odoo 0.6.0 - Enhanced instructions for connecting to Odoo, including native callable tool support and shell fallback with explicit troubleshooting. - Clarified agent requirements: both environment variables and MCP server/tool attachment are now required and documented. - Added guidance on discovering and loading available tools at runtime; users are instructed not to rely on static lists. - Improved context for shell helper scripts (like po_docs.py), especially for document processing efficiency. - Removed redundant or deprecated documentation (e.g., skill-card.md file deleted).","fileCount":14,"zipByteSize":41487},{"version":"0.5.0","createdAt":"2026-07-21T13:55:32.441Z","changelog":"**Added PO pricing review and document handling.** - Introduced PO pricing review workflow: allows reviewing Documents-app files against purchase orders, extracting text for efficient auditing (scripts/po_docs.py). - Documents can now be moved to Matched or Questions folders after review. - Added dependencies (pypdf) to support local PDF text extraction for pricing-review. - Updated documentation to reflect document and pricing-review workflow. - Removed outdated skill-card.md file.","fileCount":14,"zipByteSize":39121}]},"execution":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No published capability contract is available yet."},"installCommand":"clawhub skill install s17fq291581evd78xzn1930b0n87bfny:drivethru-odoo","setupComplexity":"low","setupSteps":["Install using `clawhub skill install s17fq291581evd78xzn1930b0n87bfny:drivethru-odoo` in an isolated environment before connecting it to live workloads.","No published capability contract is available yet, so validate auth and request/response behavior manually.","Review the upstream CLAWHUB listing at https://clawhub.ai/zmtucker/drivethru-odoo before using production credentials."],"contract":{"contractStatus":"missing","authModes":[],"requires":[],"forbidden":[],"supportsMcp":false,"supportsA2a":false,"supportsStreaming":false,"inputSchemaRef":null,"outputSchemaRef":null,"dataRegion":null,"contractUpdatedAt":null,"sourceUpdatedAt":null,"freshnessSeconds":null},"invocationGuide":{"preferredApi":{"snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-zmtucker-drivethru-odoo/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-zmtucker-drivethru-odoo/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-zmtucker-drivethru-odoo/trust"},"curlExamples":["curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-zmtucker-drivethru-odoo/snapshot\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-zmtucker-drivethru-odoo/contract\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-zmtucker-drivethru-odoo/trust\""],"jsonRequestTemplate":{"query":"summarize this repo","constraints":{"maxLatencyMs":2000,"protocolPreference":["OPENCLEW"]}},"jsonResponseTemplate":{"ok":true,"result":{"summary":"...","confidence":0.9},"meta":{"source":"CLAWHUB","generatedAt":"2026-10-10T03:57:21.272Z"}},"retryPolicy":{"maxAttempts":3,"backoffMs":[500,1500,3500],"retryableConditions":["HTTP_429","HTTP_503","NETWORK_TIMEOUT"]}},"endpoints":{"dossierUrl":"https://www.xpersona.co/api/v1/agents/clawhub-zmtucker-drivethru-odoo/dossier","snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-zmtucker-drivethru-odoo/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-zmtucker-drivethru-odoo/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-zmtucker-drivethru-odoo/trust"}},"reliability":{"evidence":{"source":"runtime-metrics","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No trust, reliability, or runtime telemetry is available."},"trust":{"status":"unavailable","handshakeStatus":"UNKNOWN","verificationFreshnessHours":null,"reputationScore":null,"p95LatencyMs":null,"successRate30d":null,"fallbackRate":null,"attempts30d":null,"trustUpdatedAt":null,"trustConfidence":"unknown","sourceUpdatedAt":null,"freshnessSeconds":null},"decisionGuardrails":{"doNotUseIf":["Contract metadata is missing or unavailable for deterministic execution."],"safeUseWhen":[],"riskFlags":["missing_or_unavailable_contract","trust_data_unavailable","schema_references_missing"],"operationalConfidence":"low"},"executionMetrics":{"observedLatencyMsP50":null,"observedLatencyMsP95":null,"estimatedCostUsd":null,"uptime30d":null,"rateLimitRpm":null,"rateLimitBurst":null,"lastVerifiedAt":null,"verificationSource":null},"runtimeMetrics":{"successRate":null,"avgLatencyMs":null,"avgCostUsd":null,"hallucinationRate":null,"retryRate":null,"disputeRate":null,"p50Latency":null,"p95Latency":null,"lastUpdated":null}},"benchmarks":{"evidence":{"source":"no-benchmark-data","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No benchmark suites or observed failure patterns are available."},"suites":[],"failurePatterns":[]},"artifacts":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-09T20:33:23.739Z","emptyReason":null},"readme":"Skill: drivethru-odoo\n\nOwner: zmtucker\n\nSummary: Talk to an Odoo ERP through its `drivethru_mcp` MCP server — discover the available Odoo tools at runtime and call them to look up eBay products/inventory, push eBay orders and read tracking, run the Accounts Payable PO→vendor-bill flow, review documents in the Documents app against their purchase orders and fix incorrect PO line pricing (the \"check the Purchasing folder against the POs\" / vendor-invoice pricing-review workflow, filing each document into Matched or Questions), schedule MRP production batches, drive vendor replenishment purchasing (run the replenishment report → curate lines → add to a PO → hand style/color/size/qty to the vendor's purchasing skill → write pricing + confirmation back and confirm the PO), and retrieve internal SOPs / best practices / policies from the Knowledge base scoped to the asking person's permissions. Use whenever the user needs to read from or write to Odoo, especially when you are answering a person inside an Odoo Discuss conversation.\n\nTags: latest:0.9.4\n\nVersion history:\n\nv0.9.4 | 2026-09-17T21:58:23.073Z | auto\n\ndrivethru-odoo 0.9.4\n\n- Updated documentation in SKILL.md for clarity and precision.\n- Incremented version number to 0.9.4.\n\nv0.9.3 | 2026-09-17T14:43:31.434Z | auto\n\ndrivethru-odoo v0.9.3\n\n- Updated documentation in SKILL.md to clarify usage instructions for native and shell-based tool access.\n- Improved step-by-step guidance for testing MCP connectivity and troubleshooting.\n- No changes to core functionality—documentation and workflow updates only.\n\nv0.9.2 | 2026-09-17T13:58:30.791Z | auto\n\ndrivethru-odoo 0.9.2\n\n- Updated SKILL.md version to 0.9.2 with minor metadata changes.\n- Removed obsolete skill-card.md file.\n- Minor internal updates to scripts/odoo_mcp.py (details not specified).\n\nv0.9.1 | 2026-09-02T18:21:28.894Z | auto\n\ndrivethru-odoo v0.9.1\n\n- Updated documentation in SKILL.md for clarity and accuracy.\n- Removed the redundant skill-card.md file.\n- Minor scripts/odoo_mcp.py adjustments (details not specified).\n\nv0.9.0 | 2026-08-20T19:33:46.150Z | auto\n\ndrivethru-odoo 0.9.0\n\n- Updated documentation in SKILL.md and production_scheduling.md for clarity and maintainability.\n- Removed skill-card.md for a streamlined skill package.\n- No changes to core functionality.\n\nv0.7.0 | 2026-08-20T17:48:19.516Z | auto\n\nVersion 0.7.0\n\n- Added the new `production_set_run_order` tool to the Production tool suite.\n- Updated documentation to include the expanded Production scheduling capabilities.\n- Removed the legacy skill-card.md file.\n\nv0.6.0 | 2026-07-21T23:53:32.306Z | auto\n\n## drivethru-odoo 0.6.0\n\n- Enhanced instructions for connecting to Odoo, including native callable tool support and shell fallback with explicit troubleshooting.\n- Clarified agent requirements: both environment variables and MCP server/tool attachment are now required and documented.\n- Added guidance on discovering and loading available tools at runtime; users are instructed not to rely on static lists.\n- Improved context for shell helper scripts (like po_docs.py), especially for document processing efficiency.\n- Removed redundant or deprecated documentation (e.g., skill-card.md file deleted).\n\nv0.5.0 | 2026-07-21T13:55:32.441Z | auto\n\n**Added PO pricing review and document handling.**\n\n- Introduced PO pricing review workflow: allows reviewing Documents-app files against purchase orders, extracting text for efficient auditing (scripts/po_docs.py).\n- Documents can now be moved to Matched or Questions folders after review.\n- Added dependencies (pypdf) to support local PDF text extraction for pricing-review.\n- Updated documentation to reflect document and pricing-review workflow.\n- Removed outdated skill-card.md file.\n\nv0.4.0 | 2026-07-10T11:02:34.921Z | auto\n\nVersion 0.4.0 of drivethru-odoo\n\n- Added internal knowledge base retrieval capabilities through new `knowledge_search_articles` and `knowledge_get_article` tools, enabling access to SOPs, best practices, and policies scoped to each user's permissions.\n- Updated documentation to explain how to retrieve and answer questions from Odoo Knowledge articles using the asking person's Odoo `user_id`.\n- Clarified usage instructions for working inside Odoo Discuss conversations, including details about permission scoping for knowledge queries.\n- Removed `skill-card.md` file.\n\nv0.3.1 | 2026-07-08T19:34:42.621Z | auto\n\n- Updated tool list: added new tools under Production and introduced a \"Mfg data (read-only)\" group (`mfg_list_models`, `mfg_fields`, `mfg_read`).\n- Expanded production flows: referenced the dedicated `drivethru-production-scheduler` skill for full scheduling use cases.\n- Minor clarifications and enhancements to instructions for discovering available tools and recommended flows.\n- Version bump from 0.3.0 to 0.3.1.\n\nv0.3.0 | 2026-07-06T19:55:37.414Z | auto\n\nVersion 0.3.0 introduces full replenishment-to-purchasing support:\n\n- Added end-to-end replenishment purchasing flow: run the replenishment report, curate, generate POs, drive vendor checkout, and write back results.\n- Updated documentation to detail the new replenishment → vendor purchasing workflow, including specific tool usage and sample flows.\n- Expanded the list of available tools, now including replenishment and purchasing commands.\n- Added reference: `references/replenishment_purchasing.md` for in-depth guidance.\n- Removed deprecated file: `skill-card.md`.\n\nv0.2.0 | 2026-06-21T19:44:37.931Z | auto\n\n- Migrated from static agent_api REST wrappers to dynamic MCP tools integration, using the new drivethru_mcp server.\n- Switched environment config: now requires ODOO_MCP_URL and ODOO_MCP_TOKEN instead of ODOO_URL/ODOO_API_KEY.\n- Replaced all previous tool scripts with a single `scripts/odoo_mcp.py` helper — enabling tool discovery and runtime schema fetching.\n- Removed deprecated skill-card.md documentation; updated SKILL.md with revised usage and best practices.\n- Legacy scripts (sales.py, ap.py, production.py) are now deprecated, with MCP tools as the preferred integration method.\n\nv0.1.0 | 2026-06-03T15:05:40.865Z | auto\n\nInitial release of drivethru-odoo: a skill to interact with Odoo ERP for eBay fulfillment, AP flow, and MRP scheduling.\n\n- Connects to Odoo's agent_api REST module using environment-based API key authentication.\n- Three domains supported: eBay sales/inventory, Accounts Payable (vendor bill flow), and Production (MRP) scheduling, each via a dedicated script.\n- Actions include product/inventory lookup, eBay order push, vendor bill creation, production batch scheduling, and more.\n- Supports dry-run mode to preview write actions without affecting Odoo data.\n- Requires ODOO_URL and ODOO_API_KEY to be set in the environment.\n\nArchive index:\n\nArchive v0.9.4: 14 files, 43305 bytes\n\nFiles: references/agent_api_endpoints.md (4898b), references/po_pricing_review.md (9131b), references/production_scheduling.md (5198b), references/replenishment_purchasing.md (9698b), scripts/_cli.py (2681b), scripts/ap.py (3424b), scripts/odoo_client.py (18932b), scripts/odoo_mcp.py (6482b), scripts/po_docs.py (14463b), scripts/production.py (4981b), scripts/sales.py (7240b), skill-card.md (2630b), SKILL.md (23213b), _meta.json (133b)\n\nFile v0.9.4:SKILL.md\n\n---\nname: drivethru-odoo\ndescription: Talk to an Odoo ERP through its `drivethru_mcp` MCP server — discover the available Odoo tools at runtime and call them to look up eBay products/inventory, push eBay orders and read tracking, run the Accounts Payable PO→vendor-bill flow, review documents in the Documents app against their purchase orders and fix incorrect PO line pricing (the \"check the Purchasing folder against the POs\" / vendor-invoice pricing-review workflow, filing each document into Matched or Questions), schedule MRP production batches, drive vendor replenishment purchasing (run the replenishment report → curate lines → add to a PO → hand style/color/size/qty to the vendor's purchasing skill → write pricing + confirmation back and confirm the PO), and retrieve internal SOPs / best practices / policies from the Knowledge base scoped to the asking person's permissions. Use whenever the user needs to read from or write to Odoo, especially when you are answering a person inside an Odoo Discuss conversation.\nversion: 0.9.4\nemoji: 🏭\nhomepage: https://www.odoo.com\nmetadata:\n  openclaw:\n    requires:\n      env: [ODOO_MCP_URL, ODOO_MCP_TOKEN]\n      bins: [python3]\n    primaryEnv: ODOO_MCP_TOKEN\n    envVars:\n      ODOO_MCP_URL:\n        required: true\n        description: >\n          Full URL of the Odoo MCP endpoint, e.g.\n          `https://odoo.example.com/drivethru_mcp/v1` (note the path — this is\n          the MCP server exposed by the `drivethru_mcp` Odoo module, not the\n          Odoo base URL).\n      ODOO_MCP_TOKEN:\n        required: true\n        description: >\n          The `drivethru.mcp_key` value from the Odoo `drivethru_mcp` module,\n          sent as `Authorization: Bearer`. Treat as a secret; never paste into\n          chat.\n    install:\n      uv:\n        - mcp>=1.9.0\n        - pypdf>=4.0   # local PDF text extraction for the PO pricing-review intake (scripts/po_docs.py)\n---\n\n# Odoo Drive Thru MCP integration\n\nThis skill gives you the Odoo **`drivethru_mcp`** MCP server — the curated Odoo\ntool surface (`documents_*`, `ap_*`, `po_*`, `mfg_*`, `production_*`,\n`replenish_*`, `knowledge_*`, `ebay_*`, `docs_*`) exposed over Streamable-HTTP.\n`ODOO_MCP_URL` and `ODOO_MCP_TOKEN` are already configured for this agent.\n**Never tell the user you can't reach Odoo, that you \"don't have the tools in\nthis thread,\" or cite the web instead — either call a tool, or state the exact\ntool call you attempted and the error it returned.**\n\n## How you call Odoo (runtime-aware — read this first)\n\nThere are two ways the `drivethru_mcp` tools reach you. Work out which you have,\nin this order:\n\n1. **Native / callable MCP tools — preferred, no shell needed.** If the Odoo\n   tools are attached to you as native tools, **call them directly** — e.g.\n   `documents_list_folders {\"name\": \"Purchasing\"}`. This is how a chat agent\n   (an Odoo Discuss bot, etc.) uses Odoo; it does **not** need a shell.\n   - They may be **deferred / lazy-loaded**: if your runtime hides tools until\n     they're loaded, they won't show up until you discover them. **Search your\n     available tools / tool registry for names like `documents_list_folders`,\n     `ap_search_purchase_orders`, `po_get_messages`, and load them before\n     calling.** \"I don't see them yet\" is not \"I don't have them.\"\n   - The names are the `drivethru_mcp` tool names (they may appear under a\n     server prefix, e.g. `…__documents_list_folders`); arguments and return\n     shapes match the table below.\n\n2. **Shell script fallback — only if you have NO native tools but DO have a\n   shell.** Use the bundled client, which opens its own MCP connection with the\n   same env vars:\n\n   ```bash\n   python3 scripts/odoo_mcp.py tools                        # list tools + JSON input schemas\n   python3 scripts/odoo_mcp.py call <tool> '{\"...\":\"...\"}'  # call one (args as 2nd arg or on stdin)\n   ```\n\n   Each invocation prints one JSON object on stdout, or\n   `{\"error\": {\"type\": ..., \"message\": ...}}` with a non-zero exit on failure.\n\n**One-call smoke test (either path).** *\"How many documents are in the\nPurchasing folder that need matching?\"* is a single call —\n`documents_list_folders {\"name\": \"Purchasing\"}`, then read `document_count` on\nthe returned folder (native), or `python3 scripts/odoo_mcp.py call\ndocuments_list_folders '{\"name\": \"Purchasing\"}'` (shell). A count back means\nOdoo is reachable — proceed. No count? Go to **Troubleshooting** below; do not\nfall back to guessing or the web.\n\n`scripts/po_docs.py` is a shell helper for the **PO pricing review**: it lists a\nDocuments-app folder and returns each file's **extracted text** (never base64 or\na page render) so a many-document run doesn't fill the context window, plus a\n`move` action to file a document into `Matched`/`Questions`. It's a\ncontext-economy optimization for shell runtimes — with only native tools, use\n`documents_search` + `documents_get` one document at a time instead (don't pull\na whole folder's base64 into context). See the pricing-review section below.\n\n## Requirements: env vars set **and** the MCP attached\n\nTwo separate things must both be true — **the env vars alone are not enough:**\n\n- **`ODOO_MCP_URL` and `ODOO_MCP_TOKEN` are set** in the agent's environment\n  (`ODOO_MCP_TOKEN` is sent as `Authorization: Bearer`). If either is missing,\n  the script path exits with `{\"error\": {\"type\": \"config_error\", ...}}` (exit 2)\n  and a native call can't authenticate. Secrets come from the environment; never\n  ask the user to paste the key into chat.\n- **The `drivethru_mcp` server is attached to *this* agent** — as a native MCP\n  connection (native-tools path) **or** via a shell + Python (script path).\n  Setting the env vars does **not** by itself attach the tools; that's an\n  operator step (see **Operator setup** below), and it's the usual reason an\n  agent \"has the skill but no tools.\"\n\n## The live tool list is the source of truth\n\nTool names and their exact input schemas come from the server, not from memory —\n**discover the live tools first** (load/list your native Odoo tools, or run\n`python3 scripts/odoo_mcp.py tools`) and read a tool's `inputSchema` before\ncalling it. The current Odoo exposes these groups (names may evolve — trust the\nlive list over this table):\n\n| Domain      | Representative tools                                                      |\n| ----------- | ------------------------------------------------------------------------ |\n| eBay        | `ebay_list_products`, `ebay_inventory`, `ebay_create_order`, `ebay_order_tracking` |\n| Accounts Pay| `ap_search_purchase_orders`, `ap_get_purchase_order`, `ap_update_po_lines`, `ap_create_vendor_bill`, `ap_get_vendor_bill`, `ap_search_vendors`; PO chatter: `po_post_message`, `po_get_messages` |\n| Documents app | `documents_list_folders`, `documents_search`, `documents_get`, `documents_create`, `documents_update`, `documents_post_message` — folders/files, chatter, and activities (the door to the Purchasing folder for the PO pricing review) |\n| Production  | `production_overview`, `production_schedule_queue`, `production_set_run_order`, `production_list_batches`, `production_get_batch`, `production_batch_supply`, `production_schedule_batch`, `production_plan_batch`, `production_bulk_schedule`, `production_list_workcenters`, `production_get_workcenter`, `production_list_production_centers`, `production_list_decoration_methods` |\n| Mfg data (read-only) | `mfg_list_models`, `mfg_fields`, `mfg_read` — read ALL fields on any whitelisted manufacturing model (batches, POs, receipts, decorations, tracking, …) |\n| Replenishment / Purchasing | `replenish_run_report`, `replenish_get_orderpoint`, `replenish_to_po`, `replenish_update_po`, `replenish_confirm_po`, `po_post_message`, `po_get_messages` |\n| Internal knowledge (SOPs, best practices) | `knowledge_search_articles`, `knowledge_get_article` — permission-scoped to the asking person |\n| Operator docs | `docs_list`, `docs_get` — fetch the operator reference for deeper context |\n\nFor a full production-scheduling pass (rank the queue → publish the run order\n→ check receipts → place batches on machines, never starting before goods are\nreceived), use the dedicated **`drivethru-production-scheduler`** skill, which\ndrives these same tools.\n\n## Operator setup: attaching the `drivethru_mcp` MCP (native tools)\n\nFor the **native-tools** path, the `drivethru_mcp` server has to be attached to\nthe agent as an MCP connection — **installing this skill and setting the env\nvars does not do that.** A skill only ships instructions, scripts, and declared\nPython deps; the skill format has no field that attaches an MCP server, so\nattaching is an **operator/platform step**. Add `drivethru_mcp` to the agent's\nMCP servers using the same env vars, as a Streamable-HTTP server:\n\n```jsonc\n// e.g. openclaw.json → mcp.servers\n\"drivethru_mcp\": {\n  \"url\":       \"<ODOO_MCP_URL>\",          // the .../drivethru_mcp/v1 endpoint\n  \"transport\": \"streamable-http\",\n  \"headers\":   { \"Authorization\": \"Bearer <ODOO_MCP_TOKEN>\" }\n}\n```\n\nOnce it's attached, the tools become native and callable — this is exactly how a\nshell-less Discuss chat agent gets them. Two things to know:\n\n- Some hosts **regenerate the agent config on every boot** from environment /\n  provisioning, so hand-editing the file on the volume won't survive a restart —\n  add the server through the host's agent/MCP configuration, not by hand.\n- If your platform can't attach an arbitrary MCP server, the agent **must** have\n  a shell + `python3` so it can use `scripts/odoo_mcp.py` instead. An agent with\n  **neither** native tools **nor** a shell cannot reach Odoo — that's a\n  provisioning gap to fix, not something the agent can work around.\n\n## Troubleshooting: the agent says it can't reach Odoo\n\nIf you (the agent) are about to say *\"I don't have the Odoo tools in this\nthread\"* or reach for the web — **stop** and run this checklist first:\n\n1. **Do you actually have the tools?** List/search your available tools for\n   `documents_list_folders` (or `ap_search_purchase_orders`). If your runtime\n   defers tools, they may need loading first — load them, then call. \"I don't\n   see them\" usually means \"not loaded yet,\" not \"not available.\"\n2. **Run the one-call smoke test.** `documents_list_folders {\"name\":\n   \"Purchasing\"}`. A folder with a `document_count` back ⇒ you're connected;\n   keep going. If it errors, capture the exact error.\n3. **Is `drivethru_mcp` attached to *this* agent?** No native tool **and** no\n   shell ⇒ the MCP was never attached (env vars alone don't attach it). That's\n   an operator step — see **Operator setup** above; tell the user plainly\n   instead of guessing.\n4. **Are the env vars set?** A `config_error` from the script path, or an auth\n   error from a native call, means `ODOO_MCP_URL` / `ODOO_MCP_TOKEN` are missing\n   or wrong — an operator must set them.\n5. **Report precisely.** If you still can't reach Odoo, state *the exact tool\n   call you attempted and the exact error you got* (e.g. `documents_list_folders\n   {\"name\":\"Purchasing\"}` → `connection_error: …`). Never fabricate an answer or\n   cite the web in place of a tool call.\n\n## Working inside an Odoo Discuss conversation\n\nYou are often answering a **person in an Odoo Discuss DM** (your messages are\nposted straight back into their thread). The bridge prefixes each forwarded\nmessage with a fenced **`[Conversation context]`** block that names the person\nand gives their **Odoo `user_id`**. Lift that `user_id` out and pass it to the\n`knowledge_*` tools so internal-knowledge answers are scoped to what this\nperson is allowed to see (details below). If the context says the sender has no\nlinked Odoo user, don't guess a `user_id` — the knowledge tools can't be used\nfor them. So:\n\n- **Be concise and conversational.** Reply in plain prose, not raw JSON dumps —\n  summarize what the tools returned. Surface a tool error's human-readable\n  message rather than the raw envelope.\n- **Ask for missing information** instead of guessing identifiers. If you need a\n  PO, batch, or order id, look it up with a search/list tool first.\n- **Confirm before writes.** Creating orders/bills, updating prices, and\n  scheduling/planning batches change live Odoo data. State exactly what you're\n  about to do and get the user's go-ahead before calling a write tool.\n- Use `docs_list` / `docs_get` when you need the operator-level rules behind a\n  workflow (e.g. how vendor-bill matching or batch scheduling is meant to work).\n\n## Typical flows\n\n- \"What eBay inventory do we have for A-1, B-2?\" → `ebay_inventory`\n  `{\"skus\": [\"A-1\", \"B-2\"]}`.\n- \"Find the PO for vendor X and bill it.\" → `ap_search_purchase_orders` →\n  `ap_get_purchase_order` → (confirm) → `ap_create_vendor_bill`.\n- \"Check the docs in the Purchasing folder against the POs and fix the pricing.\"\n  → the document-driven PO pricing-review flow below.\n- \"Show the production plan and schedule batch 142 onto workcenter 3.\" →\n  `production_overview` → `production_get_batch` `{\"batch_id\": 142}` → (confirm)\n  → `production_schedule_batch`\n  `{\"batch_id\": 142, \"primary_workcenter_id\": 3, \"date_planned_start\": \"...\"}`.\n- \"Purchase all Adidas items for Batch12345.\" → the replenishment → purchasing\n  flow below.\n- \"What's our SOP for reprinting a damaged order?\" → the internal-knowledge\n  flow below (`knowledge_search_articles` → `knowledge_get_article`).\n\n## Internal knowledge retrieval (SOPs, best practices, policies)\n\nYou double as an **internal operations knowledge agent**. When someone asks a\n\"how do we…\", \"what's our policy on…\", or \"what's the SOP for…\" question,\nanswer it from the Odoo **Knowledge** base rather than from memory — and only\nfrom articles **that person is allowed to see**.\n\nTwo tools, both **permission-scoped to the asking person's `user_id`** (which\nyou take from the `[Conversation context]` block, see above):\n\n1. **Search.** `knowledge_search_articles`\n   `{\"user_id\": <ctx uid>, \"query\": \"reprint damaged order\"}` — free text\n   matches title + body; `root_only` / `parent_id` walk the article tree. Only\n   articles this user may read come back (each with a breadcrumb `path`,\n   `snippet`, and `url`).\n2. **Read in full.** `knowledge_get_article`\n   `{\"user_id\": <ctx uid>, \"article_id\": <id>}` — returns the article's\n   plain-text `body` plus metadata. Answer from this, not from the snippet.\n\n**Permission handling.** The tools query Odoo *as that user*, so access is\nenforced by Odoo's own rules — you don't manage permissions yourself. If\n`knowledge_get_article` returns `accessible: false`, the person simply doesn't\nhave access to that article: tell them that plainly (offer to point them to\nwhoever owns it), and **don't** try to work around it or read it as anyone\nelse. Never pass a `user_id` you weren't given in the conversation context.\n\nWhen you answer, summarize the SOP/policy in plain prose and cite the article\ntitle (and `url` when present) so the person can open the source. Read\n`docs_get {\"slug\": \"knowledge\"}` for the operator-level rules behind this.\n\n## Document-driven PO pricing review (Purchasing folder)\n\nWhen asked to *\"go through the Purchasing folder and check each document's\npricing against the PO, fix any wrong lines, mark the PO checked, and file the\ndocument\"* (vendor order confirmations / acknowledgements / invoices), own the\nwhole loop. This runs many times a day over many documents, so two rules are\nnon-negotiable:\n\n1. **Don't bloat the context window.** `documents_get` returns each file as\n   base64 and a PDF reader adds a page image — huge next to the ~300–600 chars\n   of text that matter. Extract text **out of context, once**, with the intake\n   helper instead of looping `documents_get`/a PDF reader over the folder:\n\n   ```bash\n   python3 scripts/po_docs.py extract '{\"folder\": \"Purchasing\"}'\n   ```\n\n   It fetches every file over the same MCP endpoint, decodes and extracts the\n   text locally, and returns compact JSON (`documents[].text`, no base64, no\n   render). Read that. Only fall back to `documents_get` for a document the\n   helper marks `needs_vision: true` (scanned/image-only), one at a time.\n\n   **No shell (native tools only)?** Enumerate with `documents_list_folders` /\n   `documents_search`, then read documents with `documents_get` **one at a\n   time** — read the text and drop the base64, never pulling a whole folder's\n   bytes into context.\n\n2. **Every document ends in Matched or Questions.** The Purchasing folder has\n   sibling subfolders `Matched` and `Questions`; nothing stays in the inbox.\n\nPer document: read the **PO# from the text** (not the filename — filenames may\ncarry the vendor's order number), then `ap_search_purchase_orders` →\n`ap_get_purchase_order`, pair each line by **(style, color, size)**, compare\n`price_unit`, and batch every fix into one `ap_update_po_lines`. Totals are a\ncross-check only — a shipment acknowledgement is often one box of a\nmulti-shipment order, so a total gap explained by un-shipped lines is not a\ndiscrepancy. Post a \"checked\" note with `po_post_message`. Then file it:\n\n```bash\n# reconciled (matched, or corrected with confidence)\npython3 scripts/po_docs.py move '{\"document_id\": <id>, \"to\": \"Matched\", \"under\": \"Purchasing\"}'\n# a real question — raise it on the document, assign Zach, then file to Questions\ndocuments_post_message {\"document_id\": <id>, \"body\": \"<question>\", \"activity_user\": \"Zach Tucker\"}\npython3 scripts/po_docs.py move '{\"document_id\": <id>, \"to\": \"Questions\", \"under\": \"Purchasing\"}'\n```\n\nOnly escalate a **genuine** ambiguity (unreadable PO#, prices that don't\nreconcile, unexpected/missing lines, wrong vendor). An unambiguous fix backed\nby the vendor document is a Matched correction, not a question.\n\nFull procedure, matching rules (partial shipments, freight/fees, PO#-from-body),\na worked example, and a **low-cost model recommendation** for running this at\nvolume are in\n[`references/po_pricing_review.md`](references/po_pricing_review.md).\n\n## Replenishment → vendor purchasing\n\nWhen the user asks you to **buy** what the replenishment report says to buy for\na vendor (e.g. *\"Purchase all items from Adidas for Batch12345\"*), you own the\nwhole loop: read the report, curate the lines, put them on a PO, drive the\nvendor's checkout, then write the result back and confirm. The\n`drivethru_mcp` tools handle the Odoo side; a separate **vendor purchasing\nskill** handles the actual buying.\n\nRun it in this order — **confirm the curated set with the user before you\nreplenish, and again before you confirm the PO** (both are live writes):\n\n1. **Run the report.** `replenish_run_report` with the vendor filter (e.g.\n   `{\"vendor\": \"Adidas\"}`). It rebuilds the replenishment report and returns\n   the shortage lines with `style` / `color` / `size` / `qty_to_order` / `price`\n   / `production_batch_name` / `sale_order_name` / `is_out_of_stock`.\n2. **Curate.** Keep only the lines the request calls for — *\"for Batch12345\"*\n   means keep lines whose `production_batch_name` matches; *\"all Adidas\"* means\n   keep them all. This filtering is your responsibility. Show the user the kept\n   lines (style/color/size/qty) and get a go-ahead.\n3. **Add to a PO.** `replenish_to_po` `{\"orderpoint_ids\": [...]}`. This runs\n   `action_replenish` and **returns the draft PO(s)** with each line's\n   `line_id` + style/color/size/`product_qty`. Anything that couldn't be\n   bought (manufacture route, held sales order) comes back under `warnings` —\n   surface it.\n4. **Find the vendor skill & purchase.** Locate the vendor's purchasing skill —\n   named `drivethru-<vendor>-click` (e.g. **`drivethru-adidas-click`**) — and\n   read its `SKILL.md`. For adidas that's a CLI: `echo '{...}' | python3\n   scripts/adidas.py create-purchase-order`, taking `{purchase_order:\n   {po_number, lines:[{style, size, quantity}]}, confirm}` (map the Odoo PO\n   `name` → `po_number`; color is optional — the adidas article encodes it).\n   **Dry-run first** (`confirm: false`), show the user, then `confirm: true`\n   places a real order. If no such skill exists, stop and tell the user; do not\n   invent a checkout.\n5. **Write back.** Take the skill's result — match each result line to its Odoo\n   PO line by **(style, size)** to get the `line_id`, parse the `$`-prices to\n   numbers — and apply it with `replenish_update_po`: `price_unit` per\n   `line_id`, `vendor_order_number` ← the `confirmation_number`.\n6. **Handle out-of-stock / exceptions.** The adidas skill pauses on a line it\n   can't fill and returns `status: \"needs_confirmation\"` with `out_of_stock`.\n   On that, `po_post_message` onto the PO with `issue_type: \"out_of_stock\"` and\n   an `activity_user_id` so purchasing/sales sees it — then **pause that PO**.\n   `po_get_messages` later to read the reply, then re-run the vendor skill with\n   the chosen policy (`on_insufficient_stock: \"order\"` / `\"skip\"`, or edited\n   lines to substitute). Do not confirm a PO with an unresolved issue.\n7. **Confirm.** Once pricing is written and there are no open issues,\n   `replenish_confirm_po`. It confirms **without re-submitting to the vendor**\n   (the buy already happened via the skill).\n\nThe full hand-off contract (the exact dataset shape you pass the vendor skill\nand the result shape you expect back) is in\n[`references/replenishment_purchasing.md`](references/replenishment_purchasing.md).\nRead `docs_get {\"slug\": \"replenishment\"}` for the operator-level rules.\n\n## Errors\n\nThese are the **script path's** error envelope; on the **native path** a failed\ntool call surfaces its own error / `isError` result directly — read it and relay\nthe human-readable message, don't retry blindly or give up silently.\n\n- `config_error` (exit 2) — `ODOO_MCP_URL` / `ODOO_MCP_TOKEN` missing.\n- `invalid_arguments` (exit 2) — bad CLI usage or non-JSON arguments.\n- `connection_error` — Odoo unreachable, transport error, or the key was\n  rejected (the MCP server requires a valid key even to list tools).\n- A tool that ran but failed returns a normal MCP result with `isError: true`\n  and a human-readable message in its content — surface that to the user.\n\n## References\n\n- [`references/agent_api_endpoints.md`](references/agent_api_endpoints.md) — the\n  underlying Odoo operations (now exposed as MCP tools); `tools/list` is\n  authoritative for the live schema.\n- [`references/production_scheduling.md`](references/production_scheduling.md) —\n  the MRP data model behind the production tools.\n- [`references/replenishment_purchasing.md`](references/replenishment_purchasing.md) —\n  the replenishment → vendor-purchasing loop and the vendor-skill hand-off\n  contract (dataset in, result out).\n\n## Legacy REST scripts (deprecated)\n\n`scripts/sales.py`, `scripts/ap.py`, `scripts/production.py`, and\n`scripts/odoo_client.py` are the previous REST wrappers for the old `agent_api`\nmodule. They still work against the `drivethru_mcp` module's REST back-compat\nroutes (`/agent_api/v1/*`) but are deprecated — prefer `scripts/odoo_mcp.py`.\nThey will be removed once all deployments are on the MCP surface.\n\nFile v0.9.4:_meta.json\n\n{\n  \"ownerId\": \"kn715tnf30wegyr6mdbfa17avd87bjr6\",\n  \"slug\": \"drivethru-odoo\",\n  \"version\": \"0.9.4\",\n  \"publishedAt\": 1789682303073\n}\n\nFile v0.9.4:references/agent_api_endpoints.md\n\n# Odoo `agent_api` endpoint surface\n\nAll endpoints are served by the Odoo `agent_api` addon, authenticated with the\n`X-Agent-API-Key` header. Responses are JSON. The client unwraps a top-level\n`{\"success\": true, \"data\": {...}}` envelope when present and raises on\n`success: false` or non-2xx status.\n\nBase URL = `ODOO_URL` (no trailing slash). All paths below are relative to it.\n\n## Sales / eBay — `scripts/sales.py`\n\n| Method | Path                                              | Action         |\n| ------ | ------------------------------------------------- | -------------- |\n| GET    | `/agent_api/v1/ebay/products`                     | `list-products` |\n| GET    | `/agent_api/v1/ebay/inventory?skus=A,B`           | `inventory`    |\n| POST   | `/agent_api/v1/ebay/orders`                       | `create-order` |\n| GET    | `/agent_api/v1/ebay/orders/<odoo_order_id>/tracking` | `tracking`  |\n\n### Product shape (from `list-products`)\n\n```json\n{\n  \"sku\": \"ABC-1\", \"title\": \"...\", \"description\": \"...\", \"brand\": \"...\",\n  \"mpn\": \"...\", \"condition\": \"NEW\", \"category_id\": \"...\",\n  \"images\": [\"https://...\"], \"aspects\": {\"Color\": [\"Navy\"]},\n  \"cost\": 12.50, \"quantity\": 7, \"weight_oz\": 6.0,\n  \"package_length_in\": 0, \"package_width_in\": 0, \"package_height_in\": 0\n}\n```\n\n### Order-create payload (POST body built by `create-order`)\n\nThe `create-order` action accepts an eBay order object and maps it to:\n\n```json\n{\n  \"ebay_order_id\": \"...\", \"ebay_legacy_order_id\": \"...\", \"creation_date\": \"...\",\n  \"buyer_username\": \"...\", \"buyer_email\": \"...\", \"currency\": \"USD\",\n  \"subtotal\": 0.0, \"tax\": 0.0, \"shipping\": 0.0, \"total\": 0.0,\n  \"line_items\": [\n    {\"line_item_id\": \"...\", \"sku\": \"...\", \"title\": \"...\", \"quantity\": 1,\n     \"unit_price\": 0.0, \"line_total\": 0.0, \"tax\": 0.0}\n  ],\n  \"shipping_address\": {\"name\": \"...\", \"address_line_1\": \"...\", \"address_line_2\": \"...\",\n     \"city\": \"...\", \"state\": \"...\", \"postal_code\": \"...\", \"country\": \"US\",\n     \"phone\": \"...\", \"email\": \"...\"},\n  \"billing_address\": { ... }\n}\n```\n\nResponse: `{odoo_order_id, odoo_order_name, already_existed, confirmed,\npartner_id, shipping_partner_id, invoice_partner_id, confirm_error}`.\n`already_existed: true` ⇒ idempotent skip (Odoo already had this eBay order).\n\n### Tracking response\n\n`{\"shipped\": true|false, \"tracking\": {\"carrier\", \"tracking_number\",\n\"shipped_date\"} | null}`. `tracking` is null until the order ships.\n\n## Accounts Payable — `scripts/ap.py`\n\n| Method | Path                                              | Action            |\n| ------ | ------------------------------------------------- | ----------------- |\n| GET    | `/agent_api/v1/ap/purchase_orders`                | `search-pos`      |\n| GET    | `/agent_api/v1/ap/purchase_orders/<po_id>`        | `get-po`          |\n| PUT    | `/agent_api/v1/ap/purchase_orders/<po_id>/lines`  | `update-po-lines` |\n| POST   | `/agent_api/v1/ap/invoices`                       | `create-bill`     |\n| GET    | `/agent_api/v1/ap/invoices/<bill_id>`             | `get-bill`        |\n| GET    | `/agent_api/v1/ap/vendors`                        | `search-vendors`  |\n\n- `search-pos` query params: `search`, `vendor`, `state`, `limit`.\n- `update-po-lines` body: `{\"lines\": [{\"line_id\", \"price_unit\"}],\n  \"freight_cost\"?, \"fees_cost\"?}`.\n- `create-bill` body: `{\"po_id\", \"vendor_bill_number\"?, \"invoice_date\"?,\n  \"line_ids\"?, \"reviewer_user_id\"?, \"review_note\"?, \"expected_total\"?,\n  \"tolerance\"?}`. The bill is created in **draft**; Odoo's create() override\n  auto-adds buying-group fee lines, a freight line (if `freight_cost > 0`), a\n  misc-fees line (if `fees_cost > 0`), and partner-level purchase fee lines.\n\n## Production scheduling — `scripts/production.py`\n\n| Method | Path                                                   | Action               |\n| ------ | ------------------------------------------------------ | -------------------- |\n| GET    | `/agent_api/v1/production/overview`                    | `overview`           |\n| GET    | `/agent_api/v1/production/batches`                     | `list-batches`       |\n| GET    | `/agent_api/v1/production/batches/<id>`                | `get-batch`          |\n| PUT    | `/agent_api/v1/production/batches/<id>/schedule`       | `schedule`           |\n| POST   | `/agent_api/v1/production/batches/<id>/plan`           | `plan`               |\n| POST   | `/agent_api/v1/production/batches/bulk_schedule`       | `bulk-schedule`      |\n| GET    | `/agent_api/v1/production/workcenters`                 | `list-workcenters`   |\n| GET    | `/agent_api/v1/production/workcenters/<id>`            | `get-workcenter`     |\n| GET    | `/agent_api/v1/production/production_centers`          | `production-centers` |\n| GET    | `/agent_api/v1/production/decoration_methods`          | `decoration-methods` |\n\nSee [`production_scheduling.md`](production_scheduling.md) for the data model\nand field semantics.\n\nFile v0.9.4:references/po_pricing_review.md\n\n# Document-driven PO pricing review (batch)\n\nThe pattern behind *\"go through every document in the Purchasing folder, check\nits pricing against the purchase order, fix any line that's wrong, mark the PO\nchecked, and file the document.\"* This runs many times a day over 5–50+\ndocuments, so the whole design is built to (a) keep the model's context window\nsmall no matter how many documents there are, and (b) leave the Purchasing\ninbox empty when it's done — every document ends up in **Matched** or\n**Questions**.\n\nRead `docs_get {\"slug\": \"documents\"}` and `docs_get {\"slug\": \"invoices\"}` for\nthe underlying tool semantics; this file is the operating procedure.\n\n## The efficiency rule: extract text out-of-context, once\n\n`documents_get` returns each file's bytes as **base64**, and reading a PDF\nthrough a multimodal reader adds a **page image** on top. Both are enormous\nnext to the ~300–600 characters of text that actually matter per document. Do\nthat per file across a folder and the context window fills with base64 and\nrenders — the single biggest cost in a multi-document run.\n\n**So do not loop `documents_get` (or a PDF reader) over the folder.** Use the\nintake helper, which fetches every file, decodes and extracts the text\n**locally**, and returns compact JSON — text only, no base64, no image:\n\n```bash\npython3 scripts/po_docs.py extract '{\"folder\": \"Purchasing\"}'\n```\n\nReturns `{\"folder\", \"count\", \"documents\": [{document_id, name, mimetype,\ntext, chars, needs_vision, open_activities}]}`. One call = the whole folder as\nplain text. Read that; work from it.\n\n- **`needs_vision: true`** on a document means the text pass came up empty (a\n  scanned/image-only PDF or an image file). Only for those, fall back to\n  `documents_get {\"document_id\"}` and read the bytes with a vision-capable\n  reader — never for the whole folder.\n- If the helper can't run (no shell, or `ODOO_MCP_URL`/`ODOO_MCP_TOKEN` unset),\n  fall back to `documents_get` **one document at a time**, extract what you\n  need, and don't carry the base64 forward. Never fetch the whole folder's\n  bytes into context at once.\n\n## Reading a document (per file)\n\nExtract from the document's **text**, not its filename:\n\n- **PO number** — the authoritative PO# is *inside* the document. Filenames can\n  carry the vendor's order number instead (e.g. a file named\n  `Order Acknowledgement 48482500.pdf` whose real PO is `P13183` in the body).\n  Search Odoo on the PO# you read from the text.\n- **Line items** — item/style, color, size, quantity, and **unit price** per\n  line. Vendor size upcharges are normal (base sizes one price; 2XL/3XL/4XL\n  higher) — that's correct pricing, not an error.\n- **Totals are a cross-check, not the source of truth.** A shipment\n  acknowledgement is often **one box of a multi-shipment order**, so its total\n  is legitimately less than the PO total — the missing lines ship later. Match\n  **line by line**, and treat a total gap that's fully explained by un-shipped\n  lines as *not* a discrepancy.\n\n## Matching against the PO\n\n1. `ap_search_purchase_orders {\"search\": \"<PO#>\"}` → the PO `id`. Confirm the\n   vendor and `partner_ref` line up with the document.\n2. `ap_get_purchase_order {\"po_id\": <id>}` → the lines, each with its `line_id`\n   and current `price_unit`. The PO must be **confirmed** (`state = \"purchase\"`)\n   to edit lines.\n3. Pair each document line to a PO line by **(style/item, color, size)** — not\n   by row order; vendors and Odoo often sort differently. A partial shipment\n   matches a subset of the PO's lines; the rest staying unmatched is expected.\n4. Compare `price_unit` per line.\n\n## Correcting pricing\n\nBatch every mismatch on a PO into **one** `ap_update_po_lines` call:\n\n```\nap_update_po_lines {\"po_id\": <id>, \"lines\": [{\"line_id\": <id>, \"price_unit\": <doc price>}]}\n```\n\n- `freight_cost` / `fees_cost` are PO-level fields — set them only when the\n  **document itself** provides an authoritative freight/fee figure. An order\n  acknowledgement usually doesn't itemize freight (e.g. shipped \"UPS Ground\n  Collect\"); a pre-existing freight estimate or a partner-level fee on the PO\n  that the document doesn't contradict is **not** a line-pricing error — note\n  it for the eventual invoice match and leave it.\n- The tool returns `new_amount_total` — sanity-check it against the document's\n  subtotal (accounting for any partial shipment).\n\n## Mark the PO checked\n\nPost a note onto the PO so purchasing/sales can see the review, whether or not\nanything changed:\n\n```\npo_post_message {\"po_id\": <id>, \"body\": \"Pricing checked against <document> — <what you found / corrected>. ...\"}\n```\n\nState the source document, list any line corrections (old → new), and call out\npartial-shipment or freight/fee context so a human isn't confused by a total\nthat doesn't tie to the PO header.\n\n## File the document — ALWAYS (Matched / Questions)\n\nEvery reviewed document must leave the Purchasing inbox. The folder has two\nsibling subfolders — **Matched** and **Questions**. File each document by the\noutcome:\n\n- **Reconciled** (prices matched, or you corrected them with confidence) → move\n  the document to **Matched**.\n- **A genuine question** (can't read the PO#, prices don't reconcile,\n  unexpected/missing lines, wrong vendor, anything you're not sure how to fix)\n  → **don't guess.** Raise it on the *document* and assign a reviewer, then move\n  the document to **Questions**:\n\n  ```\n  documents_post_message {\"document_id\": <id>, \"body\": \"<the specific question>\", \"activity_user\": \"Zach Tucker\"}\n  ```\n\nMove with the helper (resolves the subfolder by name under the parent):\n\n```bash\npython3 scripts/po_docs.py move '{\"document_id\": <id>, \"to\": \"Matched\", \"under\": \"Purchasing\"}'\npython3 scripts/po_docs.py move '{\"document_id\": <id>, \"to\": \"Questions\", \"under\": \"Purchasing\"}'\n```\n\nOr directly: resolve the subfolders once with\n`documents_list_folders {\"parent_id\": <Purchasing id>}`, then\n`documents_update {\"document_id\": <id>, \"fields\": {\"folder_id\": <Matched|Questions id>}}`.\nThere is no delete tool by design — moving (or `active:false` to archive) is how\ndocuments leave the inbox.\n\nOnly raise a Questions activity for a **real** ambiguity. An unambiguous\ncorrection fully supported by the vendor document (a size priced $2 off the\nvendor's own confirmation) is a Matched fix, not a question — applying it is\nexactly what the task asks for.\n\n## Per-document report\n\nFor each document, report: PO number, which lines changed (old → new) or that\nnone did, whether you posted the \"checked\" note, and whether it went to Matched\nor to Questions (and why). End with a folder tally so the inbox state is\nobvious.\n\n## Worked example\n\nFolder `Purchasing` (5 docs). Intake:\n`po_docs.py extract '{\"folder\": \"Purchasing\"}'`.\n\n- **SanMar Order Confirmation, PO# P13189** — line ST485 Black size **S** reads\n  $11.94 on the confirmation but $13.94 on the PO (the other base sizes are\n  $11.94; 2XL $12.94, 3XL $14.94 match). Fix:\n  `ap_update_po_lines {\"po_id\": 13145, \"lines\": [{\"line_id\": 40941, \"price_unit\": 11.94}]}`\n  (total $1,047.90 → $1,041.90). `po_post_message` the correction → **Matched**.\n- **PO# P13193 / P13194** — every line matches. `po_post_message` \"checked, no\n  change\" → **Matched**.\n- **SanMar Shipment Acknowledgement, PO# P13137** — Box 1 of a multi-shipment\n  order: 6 of the PO's 11 lines, all $1.79 and matching; document total $53.70\n  vs PO $98.45 is just the 5 un-shipped lines. Not a discrepancy. Note the\n  partial-shipment context in the \"checked\" message → **Matched**.\n- **Workwear Outfitters Order Acknowledgement (PO# P13183 read from the body,\n  not the \"48482500\" filename)** — both lines match; PO also carries freight\n  $2.00 and a −$2.38 fee the acknowledgement doesn't itemize (reconcile at\n  invoice). Line pricing correct → **Matched**.\n\nNone needed Questions. Had any document been unreadable or failed to reconcile,\nit would have gotten a `documents_post_message` activity to Zach Tucker and\nmoved to **Questions**.\n\n## Model / cost note\n\nThis workload is structured extraction + numeric comparison + a deterministic\ntool sequence, with the heavy PDF work pushed into `po_docs.py` (outside the\nmodel). That keeps per-document tokens tiny and makes a **small, low-cost model\nviable**:\n\n- **`claude-haiku-4-5`** ($1 / $5 per Mtok) — the low-cost default. Ample for\n  reading extracted text, pairing lines, and driving the tools, precisely\n  because the script does the extraction and the Questions/Zach path catches\n  anything it's unsure about.\n- **`claude-sonnet-5`** ($3 / $15; intro $2 / $10 through 2026-08-31) — the\n  step-up when first-pass accuracy on messy or unfamiliar vendor layouts\n  matters more than the extra cost, still far below Opus. Prefer it if you see\n  Haiku mis-pairing lines or missing partial-shipment/freight nuances.\n\nThe safety net is structural: keep the Questions → Zach escalation on, and a\ncheaper model that flags uncertainty stays safe on live financial data.\n(Model IDs/pricing per the `claude-api` skill catalog; verify with the Models\nAPI if in doubt.)\n\nFile v0.9.4:references/production_scheduling.md\n\n# Production scheduling (MRP) data model\n\nThe production actions in `scripts/production.py` map the\n`/agent_api/v1/production/*` surface of the Production Scheduling Agent\nIntegration Schematic.\n\n## Entities\n\n| Entity                   | Odoo model              | Meaning                                            |\n| ------------------------ | ----------------------- | -------------------------------------------------- |\n| **Production batch**     | `mrp.production.batch`  | A batch of MOs sharing a machine and time slot.    |\n| **Workcenter**           | `mrp.workcenter`        | A machine, e.g. \"Manual Press A\".                  |\n| **Production center**    | `production.center`     | An area grouping machines, e.g. \"Screen Print Floor\". |\n| **Decoration method**    | `decoration.method`     | A process type, e.g. \"Screen Print\", \"Embroidery\". |\n\n## The writable scheduling fields\n\nEach batch is scheduled by writing any subset of:\n\n| Field                     | Meaning                                              |\n| ------------------------- | ---------------------------------------------------- |\n| `sequence`                | **Run order** — the batch's rank in the queue, and the primary scheduling decision. Rank a whole queue with `production_set_run_order`. |\n| `manual_sequence`         | A manufacturing manager pinned this batch by hand; agent `sequence` writes skip it. |\n| `primary_workcenter_id`   | Machine assignment.                                  |\n| `production_center_id`    | Derived from the workcenter if omitted.              |\n| `date_planned_start`      | ISO 8601 (UTC). Optional — a refinement on the run order, not a substitute for it. |\n| `date_planned_finished`   | Auto-computed from start + Σ MO `duration_expected` unless set explicitly. |\n\n## Event dates only lead when they're near\n\nAn `event_date` that is late or within `event_horizon_days` (default 5) sets\n`event_imminent` and outranks everything else in the queue. A **distant** event\ndate does not: past the horizon a batch is ordered by its *governing deadline*\n— the earlier of its event and ship dates — so a far-off event gives way to a\nsooner ship date instead of jumping the queue.\n\n## Art readiness tiers the queue\n\n`production_schedule_queue` does not rank on dates alone. A batch whose decals\naren't printed (`print_decals_status` = `no` / `partial` / `sample`) or whose\nartwork isn't ready (`art_status` != `done`) can't run, so it is **deferred\nbelow runnable work** — until it's due within `art_release_days` (default 3),\nwhen `art_at_risk` flips true and it is **expedited to the top**.\n\nA top-ranked batch with `art_ready: false` is a call to chase the art, not a\njob to schedule; `art_blockers` names what's missing. The\n`drivethru-production-scheduler` skill covers this in full.\n\nThe server applies writes in safe order: run order → `production_center_id` →\n`primary_workcenter_id` (derives center if omitted) → `date_planned_start`\n(auto-computes finish) → `date_planned_finished` (explicit value wins).\n\n## Planning workflow\n\n1. **`overview`** — one round-trip returning open `batches`, `workcenters`\n   (each with `scheduled_batches` inlined showing current load),\n   `production_centers`, `decoration_methods`, and `open_batch_total`. If\n   `open_batch_total > len(batches)`, raise `batch_limit` or page.\n2. **`get-batch`** — for each batch you intend to place. The detail payload adds:\n   - `decorations[]` with `decoration_production_ready` flags\n   - `sales_orders[]` with `commitment_date` / `event_date`\n   - `orders[]` (manufacturing orders) with per-MO durations\n   - `eligible_production_centers[]` — pre-filtered by capability\n   - `eligible_workcenters[]` — pre-filtered, each with `scheduled_batches`\n   - `batch_operations[]` — pre-production ops (e.g. \"Burn Screens\")\n3. **`production_set_run_order`** — publish the ranking (ids in run order).\n   This is the decision the floor works from; do it before, and independently\n   of, any machine/slot placement.\n4. **`schedule`** (single) or **`bulk-schedule`** (many) — write decisions.\n   `bulk-schedule` with `atomic: true` (default) rolls back the whole request on\n   any error; `atomic: false` applies what it can and reports per-entry errors.\n5. **`plan`** (optional) — run Odoo's native `button_plan()` slot allocator on a\n   batch. Requires `primary_workcenter_id` already set and the batch not\n   done/cancel.\n\n## Eligibility helpers (apply these when choosing a machine)\n\n- A workcenter can run a decoration method if its `decoration_method_ids` is\n  empty (no restriction) **or** contains the method id.\n- A workcenter is schedulable only if `active` **and** `allow_production_batch`.\n- A production center accepts a batch if `piece_count >= minimum_piece_count`\n  and (its `decoration_method_ids` is empty **or** includes the batch's method).\n- A batch has a pending \"Burn Screens\" operation when `is_screens_burned` is\n  false and it has `decoration_method_ids`.\n\n## Status flags worth surfacing\n\n`is_late`, `is_scheduled_late`, `is_scheduled_close`, `art_status`,\n`is_screens_burned`, `receipt_status`, plus `ship_date` / `event_date` for\ndeadline reasoning.\n\nFile v0.9.4:references/replenishment_purchasing.md\n\n# Replenishment → vendor purchasing\n\nThe full loop for buying what the Odoo replenishment report says to buy, for a\nvendor whose ordering is done by a **browser-driven purchasing skill** (e.g.\n`drivethru-adidas-click`) rather than an Odoo-side API integration.\n\n```\nreplenish_run_report ──▶ curate ──▶ replenish_to_po ──▶ [vendor skill checkout]\n        ▲                                                        │\n        │                                                        ▼\n   (re-run if ids                              replenish_update_po / po_post_message\n    went stale)                                                  │\n                                                                 ▼\n                                                       replenish_confirm_po\n```\n\nEvery step is one `drivethru_mcp` MCP tool, except the checkout, which is a\ndifferent skill. Two steps write live Odoo data (`replenish_to_po`,\n`replenish_confirm_po`) — **confirm with the user before each**.\n\n## 1. Run the report — `replenish_run_report`\n\nRebuilds the replenishment report (splitting demand by production batch / sales\norder) and returns the shortage lines. Filter to the vendor you're buying for.\n\n```json\n// call\n{ \"vendor\": \"Adidas\" }\n// or exact: { \"vendor_id\": 412 }, and/or scope: { \"batch\": \"Batch12345\" }\n```\n\nEach returned line:\n\n```json\n{\n  \"id\": 90871,                       // orderpoint id — the handle for replenish_to_po\n  \"product_id\": 5521, \"product_sku\": \"ADI-TEE-BLK-L\",\n  \"style\": \"AD100\", \"color\": \"Black\", \"size\": \"L\",\n  \"qty_to_order\": 12, \"qty_forecast\": -12, \"qty_on_hand\": 0,\n  \"price\": 8.50, \"uom\": \"Units\",\n  \"vendor_id\": 412, \"vendor_name\": \"Adidas\",\n  \"vendor_uses_integration\": false,  // false ⇒ buy via a purchasing skill, not Odoo's API\n  \"production_batch_id\": 771, \"production_batch_name\": \"Batch12345\",\n  \"sale_order_id\": 3310, \"sale_order_name\": \"S03310\",\n  \"is_out_of_stock\": false, \"has_alert\": false\n}\n```\n\nUseful args: `refresh` (default true — set false to skip the rebuild),\n`only_report_lines` (default true — set false to also include standing\nreordering rules), `only_to_order` (default true), `product`, `style`,\n`out_of_stock_only`, `limit`. `replenish_get_orderpoint {\"orderpoint_id\": …}`\nreturns one line with full demand attribution (driving MOs / PC tickets / SOs)\nand any open alerts.\n\n## 2. Curate\n\nKeep only the lines the user asked for — this is **your** filter, applied to the\nreport output:\n\n- *\"all items from Adidas\"* → every line for the vendor.\n- *\"…for Batch12345\"* → lines where `production_batch_name == \"Batch12345\"`.\n- *\"…for order S03310\"* → lines where `sale_order_name == \"S03310\"`.\n\nShow the kept lines back to the user (style / color / size / qty) and get a\ngo-ahead before replenishing.\n\n## 3. Add to a PO — `replenish_to_po`\n\n```json\n{ \"orderpoint_ids\": [90871, 90872, 90873] }\n```\n\nRuns `action_replenish` and **returns the draft PO(s)** it created or grew.\nStandard Odoo doesn't report which PO a replenish produced; this tool diffs the\ndraft PO lines around the call to recover it — so trust its `purchase_orders`,\nnever re-search for \"the newest draft PO\".\n\n```json\n{\n  \"replenished_orderpoint_ids\": [90871, 90872, 90873],\n  \"purchase_order_count\": 1,\n  \"purchase_orders\": [\n    {\n      \"id\": 6120, \"name\": \"P06120\", \"state\": \"draft\",\n      \"partner_name\": \"Adidas\", \"vendor_order_number\": null,\n      \"lines\": [\n        { \"line_id\": 44011, \"style\": \"AD100\", \"color\": \"Black\", \"size\": \"L\",\n          \"product_qty\": 12, \"price_unit\": 8.50, \"product_sku\": \"ADI-TEE-BLK-L\" }\n      ]\n    }\n  ],\n  \"warnings\": []\n}\n```\n\n`warnings` flags lines that produced no PO (manufacture route, held sales order,\nno buy rule) and any ids that went stale — surface these to the user.\n\n## 4. Run the vendor skill — `drivethru-adidas-click`\n\n**Discover the skill.** The vendor's purchasing skill is named\n`drivethru-<vendor>-click` — for Adidas, **`drivethru-adidas-click`**. Read its\n`SKILL.md`; it is a CLI, invoked one action at a time:\n\n```bash\necho '<json-args>' | python3 scripts/adidas.py create-purchase-order\n```\n\nIf there is no matching skill for the vendor, stop and tell the user — do not\nimprovise a checkout.\n\n**Input you build from the returned PO.** Map the Odoo PO `name` → `po_number`\n(adidas caps it at 18 chars / a restricted charset and auto-sanitizes with a\nwarning), and each PO line → `{style, size, quantity}`:\n\n```json\n{\n  \"purchase_order\": {\n    \"po_number\": \"P06120\",\n    \"lines\": [\n      { \"style\": \"JW4306\", \"size\": \"L\", \"quantity\": 12 }\n    ]\n  },\n  \"on_insufficient_stock\": \"pause\",\n  \"confirm\": false\n}\n```\n\n- **`color` is optional** — the adidas article number (`style`) already encodes\n  the colorway, so you don't pass color. Keep the Odoo line's color/size on your\n  side to map the result back.\n- **Dry-run first.** `confirm: false` fills the cart + checkout and returns a\n  preview without ordering. Only set `confirm: true` once the user has approved\n  — it places a **real order** (no sandbox).\n- There is **no `line_id`** in the adidas payload; the join key back to the Odoo\n  PO line is **(style, size)** — match on those (plus color if a style has\n  multiple colorways in the same PO).\n\n**Result you get back** (`schemas.py::OrderResult`):\n\n```json\n{\n  \"status\": \"submitted\",\n  \"po_number\": \"P06120\",\n  \"confirmation_number\": \"0123456789\",\n  \"order_total\": \"$101.40\",\n  \"total_quantity\": 12,\n  \"lines\": [\n    { \"style\": \"JW4306\", \"color\": \"BLACK\", \"size\": \"L\",\n      \"quantity\": 12, \"unit_price\": \"$8.45\", \"line_total\": \"$101.40\" }\n  ],\n  \"out_of_stock\": [],\n  \"warnings\": []\n}\n```\n\n- `status`: `submitted` (order placed — `confirmation_number` set) · `dry_run`\n  (confirm was false) · `needs_confirmation` (see step 6) · `error`.\n- **Prices are strings** (`\"$8.45\"`) read off the priced review page — parse the\n  numeric value before writing it back.\n\n## 5. Write the result back — `replenish_update_po`\n\nMap the adidas result → the Odoo draft PO (this is the pre-confirm sibling of\n`ap_update_po_lines`, which is for confirmed POs). Match each result line to its\nOdoo PO line by (style, size) to get the `line_id`; parse the `$`-prices to\nnumbers; `confirmation_number` → `vendor_order_number`:\n\n```json\n{\n  \"po_id\": 6120,\n  \"vendor_order_number\": \"0123456789\",\n  \"lines\": [\n    { \"line_id\": 44011, \"price_unit\": 8.45 }\n  ]\n}\n```\n\n`order_total` is the **net wholesale** total (freight / FedEx charges aren't\nbroken out by the skill); only pass `freight_cost` if you have a separate\nfreight figure. You can also pass these same fields directly to\n`replenish_confirm_po` to price and confirm in one call.\n\n## 6. Out-of-stock & other exceptions — `po_post_message` / `po_get_messages`\n\nThe adidas skill has its own out-of-stock gate. With `on_insufficient_stock:\n\"pause\"` (the default), a line that can't be fully filled makes it **order\nnothing** and return:\n\n```json\n{ \"status\": \"needs_confirmation\",\n  \"out_of_stock\": [ { \"style\": \"JW4306\", \"size\": \"L\", \"requested\": 12, \"available\": 4 } ],\n  \"message\": \"...\" }\n```\n\nOn that result, record the issue **onto the Odoo PO** and pause it:\n\n```json\n// po_post_message\n{\n  \"po_id\": 6120,\n  \"issue_type\": \"out_of_stock\",\n  \"body\": \"adidas is short on JW4306 / L — ordered 12, only 4 available. Backorder the 4, substitute, or drop?\",\n  \"activity_user_id\": 6,\n  \"activity_summary\": \"Out of stock on P06120\"\n}\n```\n\nThis posts a chatter note and drops a To-Do activity for the responsible user.\n**Do not confirm the PO while an issue is open.** Later, read the reply:\n\n```json\n// po_get_messages\n{ \"po_id\": 6120 }\n```\n\nReturns the chatter (newest first, plain text) + open activities. Then act on the\nhuman's path forward by **re-running `create-purchase-order`** with the matching\npolicy:\n\n- **Order anyway** (accept delayed/backordered delivery) → `on_insufficient_stock: \"order\"`.\n- **Drop** the short line(s) → `on_insufficient_stock: \"skip\"`, and set that Odoo\n  line's `product_qty` → 0 via `replenish_update_po`.\n- **Substitute** → edit the `lines` (different size/style) and re-run.\n\nIf the user's original request already pre-authorized a choice (*\"order anything\nout of stock\"* / *\"drop out-of-stock items\"*), set `on_insufficient_stock`\nup-front and skip the pause. Only once a clean `submitted` result comes back do\nyou write pricing and confirm.\n\n## 7. Confirm — `replenish_confirm_po`\n\n```json\n{ \"po_id\": 6120 }\n```\n\nConfirms the PO (`state` → `purchase`, creating receipts) **with `skip_api`\nalways on** — the vendor skill already placed the order, so Odoo must not push\nit through a vendor integration again. Returns `resubmitted_to_vendor_api:\nfalse` to make that explicit.\n\n## Safety notes\n\n- **Never double-order.** Confirmation happens only through\n  `replenish_confirm_po`; it is hard-wired to skip Odoo's vendor submission.\n  The real purchase happens once, in the vendor skill.\n- **Report lines are volatile.** The report rebuilds its orderpoint rows on\n  every run, so an `orderpoint_id` can disappear between `run_report` and\n  `replenish_to_po`. If ids come back under `warnings` as vanished, re-run\n  `run_report` and retry with fresh ids.\n- **Confirm live writes with the user.** `replenish_to_po` and\n  `replenish_confirm_po` change live Odoo data — state what you're about to do\n  and get a go-ahead, per the skill's write-safety rule.\n- **Integrated vendors are different.** If a line's `vendor_uses_integration`\n  is true, that vendor already has an Odoo-side API integration — buying it is\n  the normal confirmed-PO flow, not a `drivethru-<vendor>-click` skill. This\n  loop is for the click-to-buy vendors.\n\nFile v0.9.4:skill-card.md\n\n## Description:\n\nConnects an agent to an Odoo ERP through the drivethru_mcp MCP server for inventory and order lookup, Accounts Payable workflows, document-backed PO pricing review, production scheduling, replenishment purchasing, and permission-scoped Knowledge base retrieval.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[zmtucker](https://clawhub.ai/user/zmtucker)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nEmployees and operations agents use this skill to read from and write to Odoo during Odoo Discuss support, order management, purchasing, Accounts Payable, production scheduling, replenishment, document review, and internal SOP lookup workflows.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: Broad live ERP access can read or change orders, bills, production schedules, purchasing, inventory, documents, and internal knowledge.\n\nMitigation: Install only for agents and users that should have broad Odoo authority; prefer least-privilege or read-only credentials where possible.\n\nRisk: The security evidence says business-changing safeguards rely mostly on agent instructions rather than technical locks.\n\nMitigation: Require explicit dry-run behavior or user approval before live writes, and verify the attached drivethru_mcp server and any vendor checkout skills separately.\n\nRisk: ERP, customer, vendor, or internal knowledge data may be sensitive.\n\nMitigation: Avoid echoing customer PII or sensitive ERP data into chat unless necessary for the task.\n\n## Reference(s):\n\n- [Odoo](https://www.odoo.com)\n- [Odoo agent_api endpoint surface](references/agent_api_endpoints.md)\n- [Document-driven PO pricing review](references/po_pricing_review.md)\n- [Production scheduling data model](references/production_scheduling.md)\n- [Replenishment vendor purchasing](references/replenishment_purchasing.md)\n\n## Skill Output:\n\n**Output Type(s):** [text, markdown, shell commands, configuration, guidance]\n\n**Output Format:** [Markdown guidance with inline JSON examples and shell commands; helper scripts return JSON.]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Requires ODOO_MCP_URL and ODOO_MCP_TOKEN for MCP access; several workflows can perform live Odoo writes after confirmation.]\n\n## Skill Version(s):\n\n0.9.4 (source: frontmatter and server release evidence)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment.\n\nArchive v0.9.3: 14 files, 43084 bytes\n\nFiles: references/agent_api_endpoints.md (4898b), references/po_pricing_review.md (9131b), references/production_scheduling.md (5198b), references/replenishment_purchasing.md (9698b), scripts/_cli.py (2681b), scripts/ap.py (3424b), scripts/odoo_client.py (18932b), scripts/odoo_mcp.py (6482b), scripts/po_docs.py (13896b), scripts/production.py (4981b), scripts/sales.py (7240b), skill-card.md (2699b), SKILL.md (23213b), _meta.json (133b)\n\nFile v0.9.3:SKILL.md\n\n---\nname: drivethru-odoo\ndescription: Talk to an Odoo ERP through its `drivethru_mcp` MCP server — discover the available Odoo tools at runtime and call them to look up eBay products/inventory, push eBay orders and read tracking, run the Accounts Payable PO→vendor-bill flow, review documents in the Documents app against their purchase orders and fix incorrect PO line pricing (the \"check the Purchasing folder against the POs\" / vendor-invoice pricing-review workflow, filing each document into Matched or Questions), schedule MRP production batches, drive vendor replenishment purchasing (run the replenishment report → curate lines → add to a PO → hand style/color/size/qty to the vendor's purchasing skill → write pricing + confirmation back and confirm the PO), and retrieve internal SOPs / best practices / policies from the Knowledge base scoped to the asking person's permissions. Use whenever the user needs to read from or write to Odoo, especially when you are answering a person inside an Odoo Discuss conversation.\nversion: 0.9.3\nemoji: 🏭\nhomepage: https://www.odoo.com\nmetadata:\n  openclaw:\n    requires:\n      env: [ODOO_MCP_URL, ODOO_MCP_TOKEN]\n      bins: [python3]\n    primaryEnv: ODOO_MCP_TOKEN\n    envVars:\n      ODOO_MCP_URL:\n        required: true\n        description: >\n          Full URL of the Odoo MCP endpoint, e.g.\n          `https://odoo.example.com/drivethru_mcp/v1` (note the path — this is\n          the MCP server exposed by the `drivethru_mcp` Odoo module, not the\n          Odoo base URL).\n      ODOO_MCP_TOKEN:\n        required: true\n        description: >\n          The `drivethru.mcp_key` value from the Odoo `drivethru_mcp` module,\n          sent as `Authorization: Bearer`. Treat as a secret; never paste into\n          chat.\n    install:\n      uv:\n        - mcp>=1.9.0\n        - pypdf>=4.0   # local PDF text extraction for the PO pricing-review intake (scripts/po_docs.py)\n---\n\n# Odoo Drive Thru MCP integration\n\nThis skill gives you the Odoo **`drivethru_mcp`** MCP server — the curated Odoo\ntool surface (`documents_*`, `ap_*`, `po_*`, `mfg_*`, `production_*`,\n`replenish_*`, `knowledge_*`, `ebay_*`, `docs_*`) exposed over Streamable-HTTP.\n`ODOO_MCP_URL` and `ODOO_MCP_TOKEN` are already configured for this agent.\n**Never tell the user you can't reach Odoo, that you \"don't have the tools in\nthis thread,\" or cite the web instead — either call a tool, or state the exact\ntool call you attempted and the error it returned.**\n\n## How you call Odoo (runtime-aware — read this first)\n\nThere are two ways the `drivethru_mcp` tools reach you. Work out which you have,\nin this order:\n\n1. **Native / callable MCP tools — preferred, no shell needed.** If the Odoo\n   tools are attached to you as native tools, **call them directly** — e.g.\n   `documents_list_folders {\"name\": \"Purchasing\"}`. This is how a chat agent\n   (an Odoo Discuss bot, etc.) uses Odoo; it does **not** need a shell.\n   - They may be **deferred / lazy-loaded**: if your runtime hides tools until\n     they're loaded, they won't show up until you discover them. **Search your\n     available tools / tool registry for names like `documents_list_folders`,\n     `ap_search_purchase_orders`, `po_get_messages`, and load them before\n     calling.** \"I don't see them yet\" is not \"I don't have them.\"\n   - The names are the `drivethru_mcp` tool names (they may appear under a\n     server prefix, e.g. `…__documents_list_folders`); arguments and return\n     shapes match the table below.\n\n2. **Shell script fallback — only if you have NO native tools but DO have a\n   shell.** Use the bundled client, which opens its own MCP connection with the\n   same env vars:\n\n   ```bash\n   python3 scripts/odoo_mcp.py tools                        # list tools + JSON input schemas\n   python3 scripts/odoo_mcp.py call <tool> '{\"...\":\"...\"}'  # call one (args as 2nd arg or on stdin)\n   ```\n\n   Each invocation prints one JSON object on stdout, or\n   `{\"error\": {\"type\": ..., \"message\": ...}}` with a non-zero exit on failure.\n\n**One-call smoke test (either path).** *\"How many documents are in the\nPurchasing folder that need matching?\"* is a single call —\n`documents_list_folders {\"name\": \"Purchasing\"}`, then read `document_count` on\nthe returned folder (native), or `python3 scripts/odoo_mcp.py call\ndocuments_list_folders '{\"name\": \"Purchasing\"}'` (shell). A count back means\nOdoo is reachable — proceed. No count? Go to **Troubleshooting** below; do not\nfall back to guessing or the web.\n\n`scripts/po_docs.py` is a shell helper for the **PO pricing review**: it lists a\nDocuments-app folder and returns each file's **extracted text** (never base64 or\na page render) so a many-document run doesn't fill the context window, plus a\n`move` action to file a document into `Matched`/`Questions`. It's a\ncontext-economy optimization for shell runtimes — with only native tools, use\n`documents_search` + `documents_get` one document at a time instead (don't pull\na whole folder's base64 into context). See the pricing-review section below.\n\n## Requirements: env vars set **and** the MCP attached\n\nTwo separate things must both be true — **the env vars alone are not enough:**\n\n- **`ODOO_MCP_URL` and `ODOO_MCP_TOKEN` are set** in the agent's environment\n  (`ODOO_MCP_TOKEN` is sent as `Authorization: Bearer`). If either is missing,\n  the script path exits with `{\"error\": {\"type\": \"config_error\", ...}}` (exit 2)\n  and a native call can't authenticate. Secrets come from the environment; never\n  ask the user to paste the key into chat.\n- **The `drivethru_mcp` server is attached to *this* agent** — as a native MCP\n  connection (native-tools path) **or** via a shell + Python (script path).\n  Setting the env vars does **not** by itself attach the tools; that's an\n  operator step (see **Operator setup** below), and it's the usual reason an\n  agent \"has the skill but no tools.\"\n\n## The live tool list is the source of truth\n\nTool names and their exact input schemas come from the server, not from memory —\n**discover the live tools first** (load/list your native Odoo tools, or run\n`python3 scripts/odoo_mcp.py tools`) and read a tool's `inputSchema` before\ncalling it. The current Odoo exposes these groups (names may evolve — trust the\nlive list over this table):\n\n| Domain      | Representative tools                                                      |\n| ----------- | ------------------------------------------------------------------------ |\n| eBay        | `ebay_list_products`, `ebay_inventory`, `ebay_create_order`, `ebay_order_tracking` |\n| Accounts Pay| `ap_search_purchase_orders`, `ap_get_purchase_order`, `ap_update_po_lines`, `ap_create_vendor_bill`, `ap_get_vendor_bill`, `ap_search_vendors`; PO chatter: `po_post_message`, `po_get_messages` |\n| Documents app | `documents_list_folders`, `documents_search`, `documents_get`, `documents_create`, `documents_update`, `documents_post_message` — folders/files, chatter, and activities (the door to the Purchasing folder for the PO pricing review) |\n| Production  | `production_overview`, `production_schedule_queue`, `production_set_run_order`, `production_list_batches`, `production_get_batch`, `production_batch_supply`, `production_schedule_batch`, `production_plan_batch`, `production_bulk_schedule`, `production_list_workcenters`, `production_get_workcenter`, `production_list_production_centers`, `production_list_decoration_methods` |\n| Mfg data (read-only) | `mfg_list_models`, `mfg_fields`, `mfg_read` — read ALL fields on any whitelisted manufacturing model (batches, POs, receipts, decorations, tracking, …) |\n| Replenishment / Purchasing | `replenish_run_report`, `replenish_get_orderpoint`, `replenish_to_po`, `replenish_update_po`, `replenish_confirm_po`, `po_post_message`, `po_get_messages` |\n| Internal knowledge (SOPs, best practices) | `knowledge_search_articles`, `knowledge_get_article` — permission-scoped to the asking person |\n| Operator docs | `docs_list`, `docs_get` — fetch the operator reference for deeper context |\n\nFor a full production-scheduling pass (rank the queue → publish the run order\n→ check receipts → place batches on machines, never starting before goods are\nreceived), use the dedicated **`drivethru-production-scheduler`** skill, which\ndrives these same tools.\n\n## Operator setup: attaching the `drivethru_mcp` MCP (native tools)\n\nFor the **native-tools** path, the `drivethru_mcp` server has to be attached to\nthe agent as an MCP connection — **installing this skill and setting the env\nvars does not do that.** A skill only ships instructions, scripts, and declared\nPython deps; the skill format has no field that attaches an MCP server, so\nattaching is an **operator/platform step**. Add `drivethru_mcp` to the agent's\nMCP servers using the same env vars, as a Streamable-HTTP server:\n\n```jsonc\n// e.g. openclaw.json → mcp.servers\n\"drivethru_mcp\": {\n  \"url\":       \"<ODOO_MCP_URL>\",          // the .../drivethru_mcp/v1 endpoint\n  \"transport\": \"streamable-http\",\n  \"headers\":   { \"Authorization\": \"Bearer <ODOO_MCP_TOKEN>\" }\n}\n```\n\nOnce it's attached, the tools become native and callable — this is exactly how a\nshell-less Discuss chat agent gets them. Two things to know:\n\n- Some hosts **regenerate the agent config on every boot** from environment /\n  provisioning, so hand-editing the file on the volume won't survive a restart —\n  add the server through the host's agent/MCP configuration, not by hand.\n- If your platform can't attach an arbitrary MCP server, the agent **must** have\n  a shell + `python3` so it can use `scripts/odoo_mcp.py` instead. An agent with\n  **neither** native tools **nor** a shell cannot reach Odoo — that's a\n  provisioning gap to fix, not something the agent can work around.\n\n## Troubleshooting: the agent says it can't reach Odoo\n\nIf you (the agent) are about to say *\"I don't have the Odoo tools in this\nthread\"* or reach for the web — **stop** and run this checklist first:\n\n1. **Do you actually have the tools?** List/search your available tools for\n   `documents_list_folders` (or `ap_search_purchase_orders`). If your runtime\n   defers tools, they may need loading first — load them, then call. \"I don't\n   see them\" usually means \"not loaded yet,\" not \"not available.\"\n2. **Run the one-call smoke test.** `documents_list_folders {\"name\":\n   \"Purchasing\"}`. A folder with a `document_count` back ⇒ you're connected;\n   keep going. If it errors, capture the exact error.\n3. **Is `drivethru_mcp` attached to *this* agent?** No native tool **and** no\n   shell ⇒ the MCP was never attached (env vars alone don't attach it). That's\n   an operator step — see **Operator setup** above; tell the user plainly\n   instead of guessing.\n4. **Are the env vars set?** A `config_error` from the script path, or an auth\n   error from a native call, means `ODOO_MCP_URL` / `ODOO_MCP_TOKEN` are missing\n   or wrong — an operator must set them.\n5. **Report precisely.** If you still can't reach Odoo, state *the exact tool\n   call you attempted and the exact error you got* (e.g. `documents_list_folders\n   {\"name\":\"Purchasing\"}` → `connection_error: …`). Never fabricate an answer or\n   cite the web in place of a tool call.\n\n## Working inside an Odoo Discuss conversation\n\nYou are often answering a **person in an Odoo Discuss DM** (your messages are\nposted straight back into their thread). The bridge prefixes each forwarded\nmessage with a fenced **`[Conversation context]`** block that names the person\nand gives their **Odoo `user_id`**. Lift that `user_id` out and pass it to the\n`knowledge_*` tools so internal-knowledge answers are scoped to what this\nperson is allowed to see (details below). If the context says the sender has no\nlinked Odoo user, don't guess a `user_id` — the knowledge tools can't be used\nfor them. So:\n\n- **Be concise and conversational.** Reply in plain prose, not raw JSON dumps —\n  summarize what the tools returned. Surface a tool error's human-readable\n  message rather than the raw envelope.\n- **Ask for missing information** instead of guessing identifiers. If you need a\n  PO, batch, or order id, look it up with a search/list tool first.\n- **Confirm before writes.** Creating orders/bills, updating prices, and\n  scheduling/planning batches change live Odoo data. State exactly what you're\n  about to do and get the user's go-ahead before calling a write tool.\n- Use `docs_list` / `docs_get` when you need the operator-level rules behind a\n  workflow (e.g. how vendor-bill matching or batch scheduling is meant to work).\n\n## Typical flows\n\n- \"What eBay inventory do we have for A-1, B-2?\" → `ebay_inventory`\n  `{\"skus\": [\"A-1\", \"B-2\"]}`.\n- \"Find the PO for vendor X and bill it.\" → `ap_search_purchase_orders` →\n  `ap_get_purchase_order` → (confirm) → `ap_create_vendor_bill`.\n- \"Check the docs in the Purchasing folder against the POs and fix the pricing.\"\n  → the document-driven PO pricing-review flow below.\n- \"Show the production plan and schedule batch 142 onto workcenter 3.\" →\n  `production_overview` → `production_get_batch` `{\"batch_id\": 142}` → (confirm)\n  → `production_schedule_batch`\n  `{\"batch_id\": 142, \"primary_workcenter_id\": 3, \"date_planned_start\": \"...\"}`.\n- \"Purchase all Adidas items for Batch12345.\" → the replenishment → purchasing\n  flow below.\n- \"What's our SOP for reprinting a damaged order?\" → the internal-knowledge\n  flow below (`knowledge_search_articles` → `knowledge_get_article`).\n\n## Internal knowledge retrieval (SOPs, best practices, policies)\n\nYou double as an **internal operations knowledge agent**. When someone asks a\n\"how do we…\", \"what's our policy on…\", or \"what's the SOP for…\" question,\nanswer it from the Odoo **Knowledge** base rather than from memory — and only\nfrom articles **that person is allowed to see**.\n\nTwo tools, both **permission-scoped to the asking person's `user_id`** (which\nyou take from the `[Conversation context]` block, see above):\n\n1. **Search.** `knowledge_search_articles`\n   `{\"user_id\": <ctx uid>, \"query\": \"reprint damaged order\"}` — free text\n   matches title + body; `root_only` / `parent_id` walk the article tree. Only\n   articles this user may read come back (each with a breadcrumb `path`,\n   `snippet`, and `url`).\n2. **Read in full.** `knowledge_get_article`\n   `{\"user_id\": <ctx uid>, \"article_id\": <id>}` — returns the article's\n   plain-text `body` plus metadata. Answer from this, not from the snippet.\n\n**Permission handling.** The tools query Odoo *as that user*, so access is\nenforced by Odoo's own rules — you don't manage permissions yourself. If\n`knowledge_get_article` returns `accessible: false`, the person simply doesn't\nhave access to that article: tell them that plainly (offer to point them to\nwhoever owns it), and **don't** try to work around it or read it as anyone\nelse. Never pass a `user_id` you weren't given in the conversation context.\n\nWhen you answer, summarize the SOP/policy in plain prose and cite the article\ntitle (and `url` when present) so the person can open the source. Read\n`docs_get {\"slug\": \"knowledge\"}` for the operator-level rules behind this.\n\n## Document-driven PO pricing review (Purchasing folder)\n\nWhen asked to *\"go through the Purchasing folder and check each document's\npricing against the PO, fix any wrong lines, mark the PO checked, and file the\ndocument\"* (vendor order confirmations / acknowledgements / invoices), own the\nwhole loop. This runs many times a day over many documents, so two rules are\nnon-negotiable:\n\n1. **Don't bloat the context window.** `documents_get` returns each file as\n   base64 and a PDF reader adds a page image — huge next to the ~300–600 chars\n   of text that matter. Extract text **out of context, once**, with the intake\n   helper instead of looping `documents_get`/a PDF reader over the folder:\n\n   ```bash\n   python3 scripts/po_docs.py extract '{\"folder\": \"Purchasing\"}'\n   ```\n\n   It fetches every file over the same MCP endpoint, decodes and extracts the\n   text locally, and returns compact JSON (`documents[].text`, no base64, no\n   render). Read that. Only fall back to `documents_get` for a document the\n   helper marks `needs_vision: true` (scanned/image-only), one at a time.\n\n   **No shell (native tools only)?** Enumerate with `documents_list_folders` /\n   `documents_search`, then read documents with `documents_get` **one at a\n   time** — read the text and drop the base64, never pulling a whole folder's\n   bytes into context.\n\n2. **Every document ends in Matched or Questions.** The Purchasing folder has\n   sibling subfolders `Matched` and `Questions`; nothing stays in the inbox.\n\nPer document: read the **PO# from the text** (not the filename — filenames may\ncarry the vendor's order number), then `ap_search_purchase_orders` →\n`ap_get_purchase_order`, pair each line by **(style, color, size)**, compare\n`price_unit`, and batch every fix into one `ap_update_po_lines`. Totals are a\ncross-check only — a shipment acknowledgement is often one box of a\nmulti-shipment order, so a total gap explained by un-shipped lines is not a\ndiscrepancy. Post a \"checked\" note with `po_post_message`. Then file it:\n\n```bash\n# reconciled (matched, or corrected with confidence)\npython3 scripts/po_docs.py move '{\"document_id\": <id>, \"to\": \"Matched\", \"under\": \"Purchasing\"}'\n# a real question — raise it on the document, assign Zach, then file to Questions\ndocuments_post_message {\"document_id\": <id>, \"body\": \"<question>\", \"activity_user\": \"Zach Tucker\"}\npython3 scripts/po_docs.py move '{\"document_id\": <id>, \"to\": \"Questions\", \"under\": \"Purchasing\"}'\n```\n\nOnly escalate a **genuine** ambiguity (unreadable PO#, prices that don't\nreconcile, unexpected/missing lines, wrong vendor). An unambiguous fix backed\nby the vendor document is a Matched correction, not a question.\n\nFull procedure, matching rules (partial shipments, freight/fees, PO#-from-body),\na worked example, and a **low-cost model recommendation** for running this at\nvolume are in\n[`references/po_pricing_review.md`](references/po_pricing_review.md).\n\n## Replenishment → vendor purchasing\n\nWhen the user asks you to **buy** what the replenishment report says to buy for\na vendor (e.g. *\"Purchase all items from Adidas for Batch12345\"*), you own the\nwhole loop: read the report, curate the lines, put them on a PO, drive the\nvendor's checkout, then write the result back and confirm. The\n`drivethru_mcp` tools handle the Odoo side; a separate **vendor purchasing\nskill** handles the actual buying.\n\nRun it in this order — **confirm the curated set with the user before you\nreplenish, and again before you confirm the PO** (both are live writes):\n\n1. **Run the report.** `replenish_run_report` with the vendor filter (e.g.\n   `{\"vendor\": \"Adidas\"}`). It rebuilds the replenishment report and returns\n   the shortage lines with `style` / `color` / `size` / `qty_to_order` / `price`\n   / `production_batch_name` / `sale_order_name` / `is_out_of_stock`.\n2. **Curate.** Keep only the lines the request calls for — *\"for Batch12345\"*\n   means keep lines whose `production_batch_name` matches; *\"all Adidas\"* means\n   keep them all. This filtering is your responsibility. Show the user the kept\n   lines (style/color/size/qty) and get a go-ahead.\n3. **Add to a PO.** `replenish_to_po` `{\"orderpoint_ids\": [...]}`. This runs\n   `action_replenish` and **returns the draft PO(s)** with each line's\n   `line_id` + style/color/size/`product_qty`. Anything that couldn't be\n   bought (manufacture route, held sales order) comes back under `warnings` —\n   surface it.\n4. **Find the vendor skill & purchase.** Locate the vendor's purchasing skill —\n   named `drivethru-<vendor>-click` (e.g. **`drivethru-adidas-click`**) — and\n   read its `SKILL.md`. For adidas that's a CLI: `echo '{...}' | python3\n   scripts/adidas.py create-purchase-order`, taking `{purchase_order:\n   {po_number, lines:[{style, size, quantity}]}, confirm}` (map the Odoo PO\n   `name` → `po_number`; color is optional — the adidas article encodes it).\n   **Dry-run first** (`confirm: false`), show the user, then `confirm: true`\n   places a real order. If no such skill exists, stop and tell the user; do not\n   invent a checkout.\n5. **Write back.** Take the skill's result — match each result line to its Odoo\n   PO line by **(style, size)** to get the `line_id`, parse the `$`-prices to\n   numbers — and apply it with `replenish_update_po`: `price_unit` per\n   `line_id`, `vendor_order_number` ← the `confirmation_number`.\n6. **Handle out-of-stock / exceptions.** The adidas skill pauses on a line it\n   can't fill and returns `status: \"needs_confirmation\"` with `out_of_stock`.\n   On that, `po_post_message` onto the PO with `issue_type: \"out_of_stock\"` and\n   an `activity_user_id` so purchasing/sales sees it — then **pause that PO**.\n   `po_get_messages` later to read the reply, then re-run the vendor skill with\n   the chosen policy (`on_insufficient_stock: \"order\"` / `\"skip\"`, or edited\n   lines to substitute). Do not confirm a PO with an unresolved issue.\n7. **Confirm.** Once pricing is written and there are no open issues,\n   `replenish_confirm_po`. It confirms **without re-submitting to the vendor**\n   (the buy already happened via the skill).\n\nThe full hand-off contract (the exact dataset shape you pass the vendor skill\nand the result shape you expect back) is in\n[`references/replenishment_purchasing.md`](references/replenishment_purchasing.md).\nRead `docs_get {\"slug\": \"replenishment\"}` for the operator-level rules.\n\n## Errors\n\nThese are the **script path's** error envelope; on the **native path** a failed\ntool call surfaces its own error / `isError` result directly — read it and relay\nthe human-readable message, don't retry blindly or give up silently.\n\n- `config_error` (exit 2) — `ODOO_MCP_URL` / `ODOO_MCP_TOKEN` missing.\n- `invalid_arguments` (exit 2) — bad CLI usage or non-JSON arguments.\n- `connection_error` — Odoo unreachable, transport error, or the key was\n  rejected (the MCP server requires a valid key even to list tools).\n- A tool that ran but failed returns a normal MCP result with `isError: true`\n  and a human-readable message in its content — surface that to the user.\n\n## References\n\n- [`references/agent_api_endpoints.md`](references/agent_api_endpoints.md) — the\n  underlying Odoo operations (now exposed as MCP tools); `tools/list` is\n  authoritative for the live schema.\n- [`references/production_scheduling.md`](references/production_scheduling.md) —\n  the MRP data model behind the production tools.\n- [`references/replenishment_purchasing.md`](references/replenishment_purchasing.md) —\n  the replenishment → vendor-purchasing loop and the vendor-skill hand-off\n  contract (dataset in, result out).\n\n## Legacy REST scripts (deprecated)\n\n`scripts/sales.py`, `scripts/ap.py`, `scripts/production.py`, and\n`scripts/odoo_client.py` are the previous REST wrappers for the old `agent_api`\nmodule. They still work against the `drivethru_mcp` module's REST back-compat\nroutes (`/agent_api/v1/*`) but are deprecated — prefer `scripts/odoo_mcp.py`.\nThey will be removed once all deployments are on the MCP surface.\n\nFile v0.9.3:_meta.json\n\n{\n  \"ownerId\": \"kn715tnf30wegyr6mdbfa17avd87bjr6\",\n  \"slug\": \"drivethru-odoo\",\n  \"version\": \"0.9.3\",\n  \"publishedAt\": 1789656211434\n}\n\nFile v0.9.3:references/agent_api_endpoints.md\n\n# Odoo `agent_api` endpoint surface\n\nAll endpoints are served by the Odoo `agent_api` addon, authenticated with the\n`X-Agent-API-Key` header. Responses are JSON. The client unwraps a top-level\n`{\"success\": true, \"data\": {...}}` envelope when present and raises on\n`success: false` or non-2xx status.\n\nBase URL = `ODOO_URL` (no trailing slash). All paths below are relative to it.\n\n## Sales / eBay — `scripts/sales.py`\n\n| Method | Path                                              | Action         |\n| ------ | ------------------------------------------------- | -------------- |\n| GET    | `/agent_api/v1/ebay/products`                     | `list-products` |\n| GET    | `/agent_api/v1/ebay/inventory?skus=A,B`           | `inventory`    |\n| POST   | `/agent_api/v1/ebay/orders`                       | `create-order` |\n| GET    | `/agent_api/v1/ebay/orders/<odoo_order_id>/tracking` | `tracking`  |\n\n### Product shape (from `list-products`)\n\n```json\n{\n  \"sku\": \"ABC-1\", \"title\": \"...\", \"description\": \"...\", \"brand\": \"...\",\n  \"mpn\": \"...\", \"condition\": \"NEW\", \"category_id\": \"...\",\n  \"images\": [\"https://...\"], \"aspects\": {\"Color\": [\"Navy\"]},\n  \"cost\": 12.50, \"quantity\": 7, \"weight_oz\": 6.0,\n  \"package_length_in\": 0, \"package_width_in\": 0, \"package_height_in\": 0\n}\n```\n\n### Order-create payload (POST body built by `create-order`)\n\nThe `create-order` action accepts an eBay order object and maps it to:\n\n```json\n{\n  \"ebay_order_id\": \"...\", \"ebay_legacy_order_id\": \"...\", \"creation_date\": \"...\",\n  \"buyer_username\": \"...\", \"buyer_email\": \"...\", \"currency\": \"USD\",\n  \"subtotal\": 0.0, \"tax\": 0.0, \"shipping\": 0.0, \"total\": 0.0,\n  \"line_items\": [\n    {\"line_item_id\": \"...\", \"sku\": \"...\", \"title\": \"...\", \"quantity\": 1,\n     \"unit_price\": 0.0, \"line_total\": 0.0, \"tax\": 0.0}\n  ],\n  \"shipping_address\": {\"name\": \"...\", \"address_line_1\": \"...\", \"address_line_2\": \"...\",\n     \"city\": \"...\", \"state\": \"...\", \"postal_code\": \"...\", \"country\": \"US\",\n     \"phone\": \"...\", \"email\": \"...\"},\n  \"billing_address\": { ... }\n}\n```\n\nResponse: `{odoo_order_id, odoo_order_name, already_existed, confirmed,\npartner_id, shipping_partner_id, invoice_partner_id, confirm_error}`.\n`already_existed: true` ⇒ idempotent skip (Odoo already had this eBay order).\n\n### Tracking response\n\n`{\"shipped\": true|false, \"tracking\": {\"carrier\", \"tracking_number\",\n\"shipped_date\"} | null}`. `tracking` is null until the order ships.\n\n## Accounts Payable — `scripts/ap.py`\n\n| Method | Path                                              | Action            |\n| ------ | ------------------------------------------------- | ----------------- |\n| GET    | `/agent_api/v1/ap/purchase_orders`                | `search-pos`      |\n| GET    | `/agent_api/v1/ap/purchase_orders/<po_id>`        | `get-po`          |\n| PUT    | `/agent_api/v1/ap/purchase_orders/<po_id>/lines`  | `update-po-lines` |\n| POST   | `/agent_api/v1/ap/invoices`                       | `create-bill`     |\n| GET    | `/agent_api/v1/ap/invoices/<bill_id>`             | `get-bill`        |\n| GET    | `/agent_api/v1/ap/vendors`                        | `search-vendors`  |\n\n- `search-pos` query params: `search`, `vendor`, `state`, `limit`.\n- `update-po-lines` body: `{\"lines\": [{\"line_id\", \"price_unit\"}],\n  \"freight_cost\"?, \"fees_cost\"?}`.\n- `create-bill` body: `{\"po_id\", \"vendor_bill_number\"?, \"invoice_date\"?,\n  \"line_ids\"?, \"reviewer_user_id\"?, \"review_note\"?, \"expected_total\"?,\n  \"tolerance\"?}`. The bill is created in **draft**; Odoo's create() override\n  auto-adds buying-group fee lines, a freight line (if `freight_cost > 0`), a\n  misc-fees line (if `fees_cost > 0`), and partner-level purchase fee lines.\n\n## Production scheduling — `scripts/production.py`\n\n| Method | Path                                                   | Action               |\n| ------ | ------------------------------------------------------ | -------------------- |\n| GET    | `/agent_api/v1/production/overview`                    | `overview`           |\n| GET    | `/agent_api/v1/production/batches`                     | `list-batches`       |\n| GET    | `/agent_api/v1/production/batches/<id>`                | `get-batch`          |\n| PUT    | `/agent_api/v1/production/batches/<id>/schedule`       | `schedule`           |\n| POST   | `/agent_api/v1/production/batches/<id>/plan`           | `plan`               |\n| POST   | `/agent_api/v1/production/batches/bulk_schedule`       | `bulk-schedule`      |\n| GET    | `/agent_api/v1/production/workcenters`                 | `list-workcenters`   |\n| GET    | `/agent_api/v1/production/workcenters/<id>`            | `get-workcenter`     |\n| GET    | `/agent_api/v1/production/production_centers`          | `production-centers` |\n| GET    | `/agent_api/v1/production/decoration_methods`          | `decoration-methods` |\n\nSee [`production_scheduling.md`](production_scheduling.md) for the data model\nand field semantics.\n\nFile v0.9.3:references/po_pricing_review.md\n\n# Document-driven PO pricing review (batch)\n\nThe pattern behind *\"go through every document in the Purchasing folder, check\nits pricing against the purchase order, fix any line that's wrong, mark the PO\nchecked, and file the document.\"* This runs many times a day over 5–50+\ndocuments, so the whole design is built to (a) keep the model's context window\nsmall no matter how many documents there are, and (b) leave the Purchasing\ninbox empty when it's done — every document ends up in **Matched** or\n**Questions**.\n\nRead `docs_get {\"slug\": \"documents\"}` and `docs_get {\"slug\": \"invoices\"}` for\nthe underlying tool semantics; this file is the operating procedure.\n\n## The efficiency rule: extract text out-of-context, once\n\n`documents_get` returns each file's bytes as **base64**, and reading a PDF\nthrough a multimodal reader adds a **page image** on top. Both are enormous\nnext to the ~300–600 characters of text that actually matter per document. Do\nthat per file across a folder and the context window fills with base64 and\nrenders — the single biggest cost in a multi-document run.\n\n**So do not loop `documents_get` (or a PDF reader) over the folder.** Use the\nintake helper, which fetches every file, decodes and extracts the text\n**locally**, and returns compact JSON — text only, no base64, no image:\n\n```bash\npython3 scripts/po_docs.py extract '{\"folder\": \"Purchasing\"}'\n```\n\nReturns `{\"folder\", \"count\", \"documents\": [{document_id, name, mimetype,\ntext, chars, needs_vision, open_activities}]}`. One call = the whole folder as\nplain text. Read that; work from it.\n\n- **`needs_vision: true`** on a document means the text pass came up empty (a\n  scanned/image-only PDF or an image file). Only for those, fall back to\n  `documents_get {\"document_id\"}` and read the bytes with a vision-capable\n  reader — never for the whole folder.\n- If the helper can't run (no shell, or `ODOO_MCP_URL`/`ODOO_MCP_TOKEN` unset),\n  fall back to `documents_get` **one document at a time**, extract what you\n  need, and don't carry the base64 forward. Never fetch the whole folder's\n  bytes into context at once.\n\n## Reading a document (per file)\n\nExtract from the document's **text**, not its filename:\n\n- **PO number** — the authoritative PO# is *inside* the document. Filenames can\n  carry the vendor's order number instead (e.g. a file named\n  `Order Acknowledgement 48482500.pdf` whose real PO is `P13183` in the body).\n  Search Odoo on the PO# you read from the text.\n- **Line items** — item/style, color, size, quantity, and **unit price** per\n  line. Vendor size upcharges are normal (base sizes one price; 2XL/3XL/4XL\n  higher) — that's correct pricing, not an error.\n- **Totals are a cross-check, not the source of truth.** A shipment\n  acknowledgement is often **one box of a multi-shipment order**, so its total\n  is legitimately less than the PO total — the missing lines ship later. Match\n  **line by line**, and treat a total gap that's fully explained by un-shipped\n  lines as *not* a discrepancy.\n\n## Matching against the PO\n\n1. `ap_search_purchase_orders {\"search\": \"<PO#>\"}` → the PO `id`. Confirm the\n   vendor and `partner_ref` line up with the document.\n2. `ap_get_purchase_order {\"po_id\": <id>}` → the lines, each with its `line_id`\n   and current `price_unit`. The PO must be **confirmed** (`state = \"purchase\"`)\n   to edit lines.\n3. Pair each document line to a PO line by **(style/item, color, size)** — not\n   by row order; vendors and Odoo often sort differently. A partial shipment\n   matches a subset of the PO's lines; the rest staying unmatched is expected.\n4. Compare `price_unit` per line.\n\n## Correcting pricing\n\nBatch every mismatch on a PO into **one** `ap_update_po_lines` call:\n\n```\nap_update_po_lines {\"po_id\": <id>, \"lines\": [{\"line_id\": <id>, \"price_unit\": <doc price>}]}\n```\n\n- `freight_cost` / `fees_cost` are PO-level fields — set them only when the\n  **document itself** provides an authoritative freight/fee figure. An order\n  acknowledgement usually doesn't itemize freight (e.g. shipped \"UPS Ground\n  Collect\"); a pre-existing freight estimate or a partner-level fee on the PO\n  that the document doesn't contradict is **not** a line-pricing error — note\n  it for the eventual invoice match and leave it.\n- The tool returns `new_amount_total` — sanity-check it against the document's\n  subtotal (accounting for any partial shipment).\n\n## Mark the PO checked\n\nPost a note onto the PO so purchasing/sales can see the review, whether or not\nanything changed:\n\n```\npo_post_message {\"po_id\": <id>, \"body\": \"Pricing checked against <document> — <what you found / corrected>. ...\"}\n```\n\nState the source document, list any line corrections (old → new), and call out\npartial-shipment or freight/fee context so a human isn't confused by a total\nthat doesn't tie to the PO header.\n\n## File the document — ALWAYS (Matched / Questions)\n\nEvery reviewed document must leave the Purchasing inbox. The folder has two\nsibling subfolders — **Matched** and **Questions**. File each document by the\noutcome:\n\n- **Reconciled** (prices matched, or you corrected them with confidence) → move\n  the document to **Matched**.\n- **A genuine question** (can't read the PO#, prices don't reconcile,\n  unexpected/missing lines, wrong vendor, anything you're not sure how to fix)\n  → **don't guess.** Raise it on the *document* and assign a reviewer, then move\n  the document to **Questions**:\n\n  ```\n  documents_post_message {\"document_id\": <id>, \"body\": \"<the specific question>\", \"activity_user\": \"Zach Tucker\"}\n  ```\n\nMove with the helper (resolves the subfolder by name under the parent):\n\n```bash\npython3 scripts/po_docs.py move '{\"document_id\": <id>, \"to\": \"Matched\", \"under\": \"Purchasing\"}'\npython3 scripts/po_docs.py move '{\"document_id\": <id>, \"to\": \"Questions\", \"under\": \"Purchasing\"}'\n```\n\nOr directly: resolve the subfolders once with\n`documents_list_folders {\"parent_id\": <Purchasing id>}`, then\n`documents_update {\"document_id\": <id>, \"fields\": {\"folder_id\": <Matched|Questions id>}}`.\nThere is no delete tool by design — moving (or `active:false` to archive) is how\ndocuments leave the inbox.\n\nOnly raise a Questions activity for a **real** ambiguity. An unambiguous\ncorrection fully supported by the vendor document (a size priced $2 off the\nvendor's own confirmation) is a Matched fix, not a question — applying it is\nexactly what the task asks for.\n\n## Per-document report\n\nFor each document, report: PO number, which lines changed (old → new) or that\nnone did, whether you posted the \"checked\" note, and whether it went to Matched\nor to Questions (and why). End with a folder tally so the inbox state is\nobvious.\n\n## Worked example\n\nFolder `Purchasing` (5 docs). Intake:\n`po_docs.py extract '{\"folder\": \"Purchasing\"}'`.\n\n- **SanMar Order Confirmation, PO# P13189** — line ST485 Black size **S** reads\n  $11.94 on the confirmation but $13.94 on the PO (the other base sizes are\n  $11.94; 2XL $12.94, 3XL $14.94 match). Fix:\n  `ap_update_po_lines {\"po_id\": 13145, \"lines\": [{\"line_id\": 40941, \"price_unit\": 11.94}]}`\n  (total $1,047.90 → $1,041.90). `po_post_message` the correction → **Matched**.\n- **PO# P13193 / P13194** — every line matches. `po_post_message` \"checked, no\n  change\" → **Matched**.\n- **SanMar Shipment Acknowledgement, PO# P13137** — Box 1 of a multi-shipment\n  order: 6 of the PO's 11 lines, all $1.79 and matching; document total $53.70\n  vs PO $98.45 is just the 5 un-shipped lines. Not a discrepancy. Note the\n  partial-shipment context in the \"checked\" message → **Matched**.\n- **Workwear Outfitters Order Acknowledgement (PO# P13183 read from the body,\n  not the \"48482500\" filename)** — both lines match; PO also carries freight\n  $2.00 and a −$2.38 fee the acknowledgement doesn't itemize (reconcile at\n  invoice). Line pricing correct → **Matched**.\n\nNone needed Questions. Had any document been unreadable or failed to reconcile,\nit would have gotten a `documents_post_message` activity to Zach Tucker and\nmoved to **Questions**.\n\n## Model / cost note\n\nThis workload is structured extraction + numeric comparison + a deterministic\ntool sequence, with the heavy PDF work pushed into `po_docs.py` (outside the\nmodel). That keeps per-document tokens tiny and makes a **small, low-cost model\nviable**:\n\n- **`claude-haiku-4-5`** ($1 / $5 per Mtok) — the low-cost default. Ample for\n  reading extracted text, pairing lines, and driving the tools, precisely\n  because the script does the extraction and the Questions/Zach path catches\n  anything it's unsure about.\n- **`claude-sonnet-5`** ($3 / $15; intro $2 / $10 through 2026-08-31) — the\n  step-up when first-pass accuracy on messy or unfamiliar vendor layouts\n  matters more than the extra cost, still far below Opus. Prefer it if you see\n  Haiku mis-pairing lines or missing partial-shipment/freight nuances.\n\nThe safety net is structural: keep the Questions → Zach escalation on, and a\ncheaper model that flags uncertainty stays safe on live financial data.\n(Model IDs/pricing per the `claude-api` skill catalog; verify with the Models\nAPI if in doubt.)\n\nFile v0.9.3:references/production_scheduling.md\n\n# Production scheduling (MRP) data model\n\nThe production actions in `scripts/production.py` map the\n`/agent_api/v1/production/*` surface of the Production Scheduling Agent\nIntegration Schematic.\n\n## Entities\n\n| Entity                   | Odoo model              | Meaning                                            |\n| ------------------------ | ----------------------- | -------------------------------------------------- |\n| **Production batch**     | `mrp.production.batch`  | A batch of MOs sharing a machine and time slot.    |\n| **Workcenter**           | `mrp.workcenter`        | A machine, e.g. \"Manual Press A\".                  |\n| **Production center**    | `production.center`     | An area grouping machines, e.g. \"Screen Print Floor\". |\n| **Decoration method**    | `decoration.method`     | A process type, e.g. \"Screen Print\", \"Embroidery\". |\n\n## The writable scheduling fields\n\nEach batch is scheduled by writing any subset of:\n\n| Field                     | Meaning                                              |\n| ------------------------- | ---------------------------------------------------- |\n| `sequence`                | **Run order** — the batch's rank in the queue, and the primary scheduling decision. Rank a whole queue with `production_set_run_order`. |\n| `manual_sequence`         | A manufacturing manager pinned this batch by hand; agent `sequence` writes skip it. |\n| `primary_workcenter_id`   | Machine assignment.                                  |\n| `production_center_id`    | Derived from the workcenter if omitted.              |\n| `date_planned_start`      | ISO 8601 (UTC). Optional — a refinement on the run order, not a substitute for it. |\n| `date_planned_finished`   | Auto-computed from start + Σ MO `duration_expected` unless set explicitly. |\n\n## Event dates only lead when they're near\n\nAn `event_date` that is late or within `event_horizon_days` (default 5) sets\n`event_imminent` and outranks everything else in the queue. A **distant** event\ndate does not: past the horizon a batch is ordered by its *governing deadline*\n— the earlier of its event and ship dates — so a far-off event gives way to a\nsooner ship date instead of jumping the queue.\n\n## Art readiness tiers the queue\n\n`production_schedule_queue` does not rank on dates alone. A batch whose decals\naren't printed (`print_decals_status` = `no` / `partial` / `sample`) or whose\nartwork isn't ready (`art_status` != `done`) can't run, so it is **deferred\nbelow runnable work** — until it's due within `art_release_days` (default 3),\nwhen `art_at_risk` flips true and it is **expedited to the top**.\n\nA top-ranked batch with `art_ready: false` is a call to chase the art, not a\njob to schedule; `art_blockers` names what's missing. The\n`drivethru-production-scheduler` skill covers this in full.\n\nThe server applies writes in safe order: run order → `production_center_id` →\n`primary_workcenter_id` (derives center if omitted) → `date_planned_start`\n(auto-computes finish) → `date_planned_finished` (explicit value wins).\n\n## Planning workflow\n\n1. **`overview`** — one round-trip returning open `batches`, `workcenters`\n   (each with `scheduled_batches` inlined showing current load),\n   `production_centers`, `decoration_methods`, and `open_batch_total`. If\n   `open_batch_total > len(batches)`, raise `batch_limit` or page.\n2. **`get-batch`** — for each batch you intend to place. The detail payload adds:\n   - `decorations[]` with `decoration_production_ready` flags\n   - `sales_orders[]` with `commitment_date` / `event_date`\n   - `orders[]` (manufacturing orders) with per-MO durations\n   - `eligible_production_centers[]` — pre-filtered by capability\n   - `eligible_workcenters[]` — pre-filtered, each with `scheduled_batches`\n   - `batch_operations[]` — pre-production ops (e.g. \"Burn Screens\")\n3. **`production_set_run_order`** — publish the ranking (ids in run order).\n   This is the decision the floor works from; do it before, and independently\n   of, any machine/slot placement.\n4. **`schedule`** (single) or **`bulk-schedule`** (many) — write decisions.\n   `bulk-schedule` with `atomic: true` (default) rolls back the whole request on\n   any error; `atomic: false` applies what it can and reports per-entry errors.\n5. **`plan`** (optional) — run Odoo's native `button_plan()` slot allocator on a\n   batch. Requires `primary_workcenter_id` already set and the batch not\n   done/cancel.\n\n## Eligibility helpers (apply these when choosing a machine)\n\n- A workcenter can run a decoration method if its `decoration_method_ids` is\n  empty (no restriction) **or** contains the method id.\n- A workcenter is schedulable only if `active` **and** `allow_production_batch`.\n- A production center accepts a batch if `piece_count >= minimum_piece_count`\n  and (its `decoration_method_ids` is empty **or** includes the batch's method).\n- A batch has a pending \"Burn Screens\" operation when `is_screens_burned` is\n  false and it has `decoration_method_ids`.\n\n## Status flags worth surfacing\n\n`is_late`, `is_scheduled_late`, `is_scheduled_close`, `art_status`,\n`is_screens_burned`, `receipt_status`, plus `ship_date` / `event_date` for\ndeadline reasoning.\n\nFile v0.9.3:references/replenishment_purchasing.md\n\n# Replenishment → vendor purchasing\n\nThe full loop for buying what the Odoo replenishment report says to buy, for a\nvendor whose ordering is done by a **browser-driven purchasing skill** (e.g.\n`drivethru-adidas-click`) rather than an Odoo-side API integration.\n\n```\nreplenish_run_report ──▶ curate ──▶ replenish_to_po ──▶ [vendor skill checkout]\n        ▲                                                        │\n        │                                                        ▼\n   (re-run if ids                              replenish_update_po / po_post_message\n    went stale)                                                  │\n                                                                 ▼\n                                                       replenish_confirm_po\n```\n\nEvery step is one `drivethru_mcp` MCP tool, except the checkout, which is a\ndifferent skill. Two steps write live Odoo data (`replenish_to_po`,\n`replenish_confirm_po`) — **confirm with the user before each**.\n\n## 1. Run the report — `replenish_run_report`\n\nRebuilds the replenishment report (splitting demand by production batch / sales\norder) and returns the shortage lines. Filter to the vendor you're buying for.\n\n```json\n// call\n{ \"vendor\": \"Adidas\" }\n// or exact: { \"vendor_id\": 412 }, and/or scope: { \"batch\": \"Batch12345\" }\n```\n\nEach returned line:\n\n```json\n{\n  \"id\": 90871,                       // orderpoint id — the handle for replenish_to_po\n  \"product_id\": 5521, \"product_sku\": \"ADI-TEE-BLK-L\",\n  \"style\": \"AD100\", \"color\": \"Black\", \"size\": \"L\",\n  \"qty_to_order\": 12, \"qty_forecast\": -12, \"qty_on_hand\": 0,\n  \"price\": 8.50, \"uom\": \"Units\",\n  \"vendor_id\": 412, \"vendor_name\": \"Adidas\",\n  \"vendor_uses_integration\": false,  // false ⇒ buy via a purchasing skill, not Odoo's API\n  \"production_batch_id\": 771, \"production_batch_name\": \"Batch12345\",\n  \"sale_order_id\": 3310, \"sale_order_name\": \"S03310\",\n  \"is_out_of_stock\": false, \"has_alert\": false\n}\n```\n\nUseful args: `refresh` (default true — set false to skip the rebuild),\n`only_report_lines` (default true — set false to also include standing\nreordering rules), `only_to_order` (default true), `product`, `style`,\n`out_of_stock_only`, `limit`. `replenish_get_orderpoint {\"orderpoint_id\": …}`\nreturns one line with full demand attribution (driving MOs / PC tickets / SOs)\nand any open alerts.\n\n## 2. Curate\n\nKeep only the lines the user asked for — this is **your** filter, applied to the\nreport output:\n\n- *\"all items from Adidas\"* → every line for the vendor.\n- *\"…for Batch12345\"* → lines where `production_batch_name == \"Batch12345\"`.\n- *\"…for order S03310\"* → lines where `sale_order_name == \"S03310\"`.\n\nShow the kept lines back to the user (style / color / size / qty) and get a\ngo-ahead before replenishing.\n\n## 3. Add to a PO — `replenish_to_po`\n\n```json\n{ \"orderpoint_ids\": [90871, 90872, 90873] }\n```\n\nRuns `action_replenish` and **returns the draft PO(s)** it created or grew.\nStandard Odoo doesn't report which PO a replenish produced; this tool diffs the\ndraft PO lines around the call to recover it — so trust its `purchase_orders`,\nnever re-search for \"the newest draft PO\".\n\n```json\n{\n  \"replenished_orderpoint_ids\": [90871, 90872, 90873],\n  \"purchase_order_count\": 1,\n  \"purchase_orders\": [\n    {\n      \"id\": 6120, \"name\": \"P06120\", \"state\": \"draft\",\n      \"partner_name\": \"Adidas\", \"vendor_order_number\": null,\n      \"lines\": [\n        { \"line_id\": 44011, \"style\": \"AD100\", \"color\": \"Black\", \"size\": \"L\",\n          \"product_qty\": 12, \"price_unit\": 8.50, \"product_sku\": \"ADI-TEE-BLK-L\" }\n      ]\n    }\n  ],\n  \"warnings\": []\n}\n```\n\n`warnings` flags lines that produced no PO (manufacture route, held sales order,\nno buy rule) and any ids that went stale — surface these to the user.\n\n## 4. Run the vendor skill — `drivethru-adidas-click`\n\n**Discover the skill.** The vendor's purchasing skill is named\n`drivethru-<vendor>-click` — for Adidas, **`drivethru-adidas-click`**. Read its\n`SKILL.md`; it is a CLI, invoked one action at a time:\n\n```bash\necho '<json-args>' | python3 scripts/adidas.py create-purchase-order\n```\n\nIf there is no matching skill for the vendor, stop and tell the user — do not\nimprovise a checkout.\n\n**Input you build from the returned PO.** Map the Odoo PO `name` → `po_number`\n(adidas caps it at 18 chars / a restricted charset and auto-sanitizes with a\nwarning), and each PO line → `{style, size, quantity}`:\n\n```json\n{\n  \"purchase_order\": {\n    \"po_number\": \"P06120\",\n    \"lines\": [\n      { \"style\": \"JW4306\", \"size\": \"L\", \"quantity\": 12 }\n    ]\n  },\n  \"on_insufficient_stock\": \"pause\",\n  \"confirm\": false\n}\n```\n\n- **`color` is optional** — the adidas article number (`style`) already encodes\n  the colorway, so you don't pass color. Keep the Odoo line's color/size on your\n  side to map the result back.\n- **Dry-run first.** `confirm: false` fills the cart + checkout and returns a\n  preview without ordering. Only set `confirm: true` once the user has approved\n  — it places a **real order** (no sandbox).\n- There is **no `line_id`** in the adidas payload; the join key back to the Odoo\n  PO line is **(style, size)** — match on those (plus color if a style has\n  multiple colorways in the same PO).\n\n**Result you get back** (`schemas.py::OrderResult`):\n\n```json\n{\n  \"status\": \"submitted\",\n  \"po_number\": \"P06120\",\n  \"confirmation_number\": \"0123456789\",\n  \"order_total\": \"$101.40\",\n  \"total_quantity\": 12,\n  \"lines\": [\n    { \"style\": \"JW4306\", \"color\": \"BLACK\", \"size\": \"L\",\n      \"quantity\": 12, \"unit_price\": \"$8.45\", \"line_total\": \"$101.40\" }\n  ],\n  \"out_of_stock\": [],\n  \"warnings\": []\n}\n```\n\n- `status`: `submitted` (order placed — `confirmation_number` set) · `dry_run`\n  (confirm was false) · `needs_confirmation` (see step 6) · `error`.\n- **Prices are strings** (`\"$8.45\"`) read off the priced review page — parse the\n  numeric value before writing it back.\n\n## 5. Write the result back — `replenish_update_po`\n\nMap the adidas result → the Odoo draft PO (this is the pre-confirm sibling of\n`ap_update_po_lines`, which is for confirmed POs). Match each result line to its\nOdoo PO line by (style, size) to get the `line_id`; parse the `$`-prices to\nnumbers; `confirmation_number` → `vendor_order_number`:\n\n```json\n{\n  \"po_id\": 6120,\n  \"vendor_order_number\": \"0123456789\",\n  \"lines\": [\n    { \"line_id\": 44011, \"price_unit\": 8.45 }\n  ]\n}\n```\n\n`order_total` is the **net wholesale** total (freight / FedEx charges aren't\nbroken out by the skill); only pass `freight_cost` if you have a separate\nfreight figure. You can also pass these same fields directly to\n`replenish_confirm_po` to price and confirm in one call.\n\n## 6. Out-of-stock & other exceptions — `po_post_message` / `po_get_messages`\n\nThe adidas skill has its own out-of-stock gate. With `on_insufficient_stock:\n\"pause\"` (the default), a line that can't be fully filled makes it **order\nnothing** and return:\n\n```json\n{ \"status\": \"needs_confirmation\",\n  \"out_of_stock\": [ { \"style\": \"JW4306\", \"size\": \"L\", \"requested\": 12, \"available\": 4 } ],\n  \"message\": \"...\" }\n```\n\nOn that result, record the issue **onto the Odoo PO** and pause it:\n\n```json\n// po_post_message\n{\n  \"po_id\": 6120,\n  \"issue_type\": \"out_of_stock\",\n  \"body\": \"adidas is short on JW4306 / L — ordered 12, only 4 available. Backorder the 4, substitute, or drop?\",\n  \"activity_user_id\": 6,\n  \"activity_summary\": \"Out of stock on P06120\"\n}\n```\n\nThis posts a chatter note and drops a To-Do activity for the responsible user.\n**Do not confirm the PO while an issue is open.** Later, read the reply:\n\n```json\n// po_get_messages\n{ \"po_id\": 6120 }\n```\n\nReturns the chatter (newest first, plain text) + open activities. Then act on the\nhuman's path forward by **re-running `create-purchase-order`** with the matching\npolicy:\n\n- **Order anyway** (accept delayed/backordered delivery) → `on_insufficient_stock: \"order\"`.\n- **Drop** the short line(s) → `on_insufficient_stock: \"skip\"`, and set that Odoo\n  line's `product_qty` → 0 via `replenish_update_po`.\n- **Substitute** → edit the `lines` (different size/style) and re-run.\n\nIf the user's original request already pre-authorized a choice (*\"order anything\nout of stock\"* / *\"drop out-of-stock items\"*), set `on_insufficient_stock`\nup-front and skip the pause. Only once a clean `submitted` result comes back do\nyou write pricing and confirm.\n\n## 7. Confirm — `replenish_confirm_po`\n\n```json\n{ \"po_id\": 6120 }\n```\n\nConfirms the PO (`state` → `purchase`, creating receipts) **with `skip_api`\nalways on** — the vendor skill already placed the order, so Odoo must not push\nit through a vendor integration again. Returns `resubmitted_to_vendor_api:\nfalse` to make that explicit.\n\n## Safety notes\n\n- **Never double-order.** Confirmation happens only through\n  `replenish_confirm_po`; it is hard-wired to skip Odoo's vendor submission.\n  The real purchase happens once, in the vendor skill.\n- **Report lines are volatile.** The report rebuilds its orderpoint rows on\n  every run, so an `orderpoint_id` can disappear between `run_report` and\n  `replenish_to_po`. If ids come back under `warnings` as vanished, re-run\n  `run_report` and retry with fresh ids.\n- **Confirm live writes with the user.** `replenish_to_po` and\n  `replenish_confirm_po` change live Odoo data — state what you're about to do\n  and get a go-ahead, per the skill's write-safety rule.\n- **Integrated vendors are different.** If a line's `vendor_uses_integration`\n  is true, that vendor already has an Odoo-side API integration — buying it is\n  the normal confirmed-PO flow, not a `drivethru-<vendor>-click` skill. This\n  loop is for the click-to-buy vendors.\n\nFile v0.9.3:skill-card.md\n\n## Description:\n\nDrivethru Odoo helps agents use a configured Odoo drivethru_mcp server to discover and call ERP tools for eBay, accounts payable, purchasing documents, production scheduling, replenishment, and permission-scoped knowledge retrieval.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[zmtucker](https://clawhub.ai/user/zmtucker)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nEmployees, operations teams, and configured Odoo agents use this skill to read from and write to Odoo for inventory checks, order handling, AP workflows, purchasing document review, production scheduling, replenishment purchasing, and internal knowledge retrieval.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: The skill can make live financial, purchasing, production, replenishment, and order changes in Odoo.\n\nMitigation: Install it only for agents authorized to operate with the configured Odoo token, and require explicit user approval before write, bulk review, scheduling, order creation, replenishment, or PO confirmation actions.\n\nRisk: Server security evidence reports a defective documented dry-run safety path for PO price updates.\n\nMitigation: Treat PO price update workflows as review-required and do not rely on dry-run behavior for safety until the dry-run path is fixed and revalidated.\n\nRisk: The configured Odoo MCP token carries the privileges granted by the Odoo deployment.\n\nMitigation: Keep ODOO_MCP_TOKEN in the agent environment, do not paste it into chat, and scope the token to the minimum Odoo permissions needed for the release.\n\n## Reference(s):\n\n- [ClawHub Skill Page](https://clawhub.ai/zmtucker/skills/drivethru-odoo)\n- [Odoo](https://www.odoo.com)\n- [Odoo agent_api endpoint surface](references/agent_api_endpoints.md)\n- [Document-driven PO pricing review](references/po_pricing_review.md)\n- [Production scheduling data model](references/production_scheduling.md)\n- [Replenishment to vendor purchasing](references/replenishment_purchasing.md)\n\n## Skill Output:\n\n**Output Type(s):** [Guidance, Shell commands, API calls, Configuration]\n\n**Output Format:** [Markdown guidance with JSON tool arguments and shell command examples]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Shell fallback helpers print JSON results or JSON error envelopes.]\n\n## Skill Version(s):\n\n0.9.3 (source: frontmatter and server release evidence)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment.\n\nArchive v0.9.2: 14 files, 42762 bytes\n\nFiles: references/agent_api_endpoints.md (4898b), references/po_pricing_review.md (9131b), references/production_scheduling.md (5198b), references/replenishment_purchasing.md (9698b), scripts/_cli.py (2681b), scripts/ap.py (3424b), scripts/odoo_client.py (18932b), scripts/odoo_mcp.py (6279b), scripts/po_docs.py (12729b), scripts/production.py (4981b), scripts/sales.py (7240b), skill-card.md (2993b), SKILL.md (23213b), _meta.json (133b)\n\nFile v0.9.2:SKILL.md\n\n---\nname: drivethru-odoo\ndescription: Talk to an Odoo ERP through its `drivethru_mcp` MCP server — discover the available Odoo tools at runtime and call them to look up eBay products/inventory, push eBay orders and read tracking, run the Accounts Payable PO→vendor-bill flow, review documents in the Documents app against their purchase orders and fix incorrect PO line pricing (the \"check the Purchasing folder against the POs\" / vendor-invoice pricing-review workflow, filing each document into Matched or Questions), schedule MRP production batches, drive vendor replenishment purchasing (run the replenishment report → curate lines → add to a PO → hand style/color/size/qty to the vendor's purchasing skill → write pricing + confirmation back and confirm the PO), and retrieve internal SOPs / best practices / policies from the Knowledge base scoped to the asking person's permissions. Use whenever the user needs to read from or write to Odoo, especially when you are answering a person inside an Odoo Discuss conversation.\nversion: 0.9.2\nemoji: 🏭\nhomepage: https://www.odoo.com\nmetadata:\n  openclaw:\n    requires:\n      env: [ODOO_MCP_URL, ODOO_MCP_TOKEN]\n      bins: [python3]\n    primaryEnv: ODOO_MCP_TOKEN\n    envVars:\n      ODOO_MCP_URL:\n        required: true\n        description: >\n          Full URL of the Odoo MCP endpoint, e.g.\n          `https://odoo.example.com/drivethru_mcp/v1` (note the path — this is\n          the MCP server exposed by the `drivethru_mcp` Odoo module, not the\n          Odoo base URL).\n      ODOO_MCP_TOKEN:\n        required: true\n        description: >\n          The `drivethru.mcp_key` value from the Odoo `drivethru_mcp` module,\n          sent as `Authorization: Bearer`. Treat as a secret; never paste into\n          chat.\n    install:\n      uv:\n        - mcp>=1.9.0\n        - pypdf>=4.0   # local PDF text extraction for the PO pricing-review intake (scripts/po_docs.py)\n---\n\n# Odoo Drive Thru MCP integration\n\nThis skill gives you the Odoo **`drivethru_mcp`** MCP server — the curated Odoo\ntool surface (`documents_*`, `ap_*`, `po_*`, `mfg_*`, `production_*`,\n`replenish_*`, `knowledge_*`, `ebay_*`, `docs_*`) exposed over Streamable-HTTP.\n`ODOO_MCP_URL` and `ODOO_MCP_TOKEN` are already configured for this agent.\n**Never tell the user you can't reach Odoo, that you \"don't have the tools in\nthis thread,\" or cite the web instead — either call a tool, or state the exact\ntool call you attempted and the error it returned.**\n\n## How you call Odoo (runtime-aware — read this first)\n\nThere are two ways the `drivethru_mcp` tools reach you. Work out which you have,\nin this order:\n\n1. **Native / callable MCP tools — preferred, no shell needed.** If the Odoo\n   tools are attached to you as native tools, **call them directly** — e.g.\n   `documents_list_folders {\"name\": \"Purchasing\"}`. This is how a chat agent\n   (an Odoo Discuss bot, etc.) uses Odoo; it does **not** need a shell.\n   - They may be **deferred / lazy-loaded**: if your runtime hides tools until\n     they're loaded, they won't show up until you discover them. **Search your\n     available tools / tool registry for names like `documents_list_folders`,\n     `ap_search_purchase_orders`, `po_get_messages`, and load them before\n     calling.** \"I don't see them yet\" is not \"I don't have them.\"\n   - The names are the `drivethru_mcp` tool names (they may appear under a\n     server prefix, e.g. `…__documents_list_folders`); arguments and return\n     shapes match the table below.\n\n2. **Shell script fallback — only if you have NO native tools but DO have a\n   shell.** Use the bundled client, which opens its own MCP connection with the\n   same env vars:\n\n   ```bash\n   python3 scripts/odoo_mcp.py tools                        # list tools + JSON input schemas\n   python3 scripts/odoo_mcp.py call <tool> '{\"...\":\"...\"}'  # call one (args as 2nd arg or on stdin)\n   ```\n\n   Each invocation prints one JSON object on stdout, or\n   `{\"error\": {\"type\": ..., \"message\": ...}}` with a non-zero exit on failure.\n\n**One-call smoke test (either path).** *\"How many documents are in the\nPurchasing folder that need matching?\"* is a single call —\n`documents_list_folders {\"name\": \"Purchasing\"}`, then read `document_count` on\nthe returned folder (native), or `python3 scripts/odoo_mcp.py call\ndocuments_list_folders '{\"name\": \"Purchasing\"}'` (shell). A count back means\nOdoo is reachable — proceed. No count? Go to **Troubleshooting** below; do not\nfall back to guessing or the web.\n\n`scripts/po_docs.py` is a shell helper for the **PO pricing review**: it lists a\nDocuments-app folder and returns each file's **extracted text** (never base64 or\na page render) so a many-document run doesn't fill the context window, plus a\n`move` action to file a document into `Matched`/`Questions`. It's a\ncontext-economy optimization for shell runtimes — with only native tools, use\n`documents_search` + `documents_get` one document at a time instead (don't pull\na whole folder's base64 into context). See the pricing-review section below.\n\n## Requirements: env vars set **and** the MCP attached\n\nTwo separate things must both be true — **the env vars alone are not enough:**\n\n- **`ODOO_MCP_URL` and `ODOO_MCP_TOKEN` are set** in the agent's environment\n  (`ODOO_MCP_TOKEN` is sent as `Authorization: Bearer`). If either is missing,\n  the script path exits with `{\"error\": {\"type\": \"config_error\", ...}}` (exit 2)\n  and a native call can't authenticate. Secrets come from the environment; never\n  ask the user to paste the key into chat.\n- **The `drivethru_mcp` server is attached to *this* agent** — as a native MCP\n  connection (native-tools path) **or** via a shell + Python (script path).\n  Setting the env vars does **not** by itself attach the tools; that's an\n  operator step (see **Operator setup** below), and it's the usual reason an\n  agent \"has the skill but no tools.\"\n\n## The live tool list is the source of truth\n\nTool names and their exact input schemas come from the server, not from memory —\n**discover the live tools first** (load/list your native Odoo tools, or run\n`python3 scripts/odoo_mcp.py tools`) and read a tool's `inputSchema` before\ncalling it. The current Odoo exposes these groups (names may evolve — trust the\nlive list over this table):\n\n| Domain      | Representative tools                                                      |\n| ----------- | ------------------------------------------------------------------------ |\n| eBay        | `ebay_list_products`, `ebay_inventory`, `ebay_create_order`, `ebay_order_tracking` |\n| Accounts Pay| `ap_search_purchase_orders`, `ap_get_purchase_order`, `ap_update_po_lines`, `ap_create_vendor_bill`, `ap_get_vendor_bill`, `ap_search_vendors`; PO chatter: `po_post_message`, `po_get_messages` |\n| Documents app | `documents_list_folders`, `documents_search`, `documents_get`, `documents_create`, `documents_update`, `documents_post_message` — folders/files, chatter, and activities (the door to the Purchasing folder for the PO pricing review) |\n| Production  | `production_overview`, `production_schedule_queue`, `production_set_run_order`, `production_list_batches`, `production_get_batch`, `production_batch_supply`, `production_schedule_batch`, `production_plan_batch`, `production_bulk_schedule`, `production_list_workcenters`, `production_get_workcenter`, `production_list_production_centers`, `production_list_decoration_methods` |\n| Mfg data (read-only) | `mfg_list_models`, `mfg_fields`, `mfg_read` — read ALL fields on any whitelisted manufacturing model (batches, POs, receipts, decorations, tracking, …) |\n| Replenishment / Purchasing | `replenish_run_report`, `replenish_get_orderpoint`, `replenish_to_po`, `replenish_update_po`, `replenish_confirm_po`, `po_post_message`, `po_get_messages` |\n| Internal knowledge (SOPs, best practices) | `knowledge_search_articles`, `knowledge_get_article` — permission-scoped to the asking person |\n| Operator docs | `docs_list`, `docs_get` — fetch the operator reference for deeper context |\n\nFor a full production-scheduling pass (rank the queue → publish the run order\n→ check receipts → place batches on machines, never starting before goods are\nreceived), use the dedicated **`drivethru-production-scheduler`** skill, which\ndrives these same tools.\n\n## Operator setup: attaching the `drivethru_mcp` MCP (native tools)\n\nFor the **native-tools** path, the `drivethru_mcp` server has to be attached to\nthe agent as an MCP connection — **installing this skill and setting the env\nvars does not do that.** A skill only ships instructions, scripts, and declared\nPython deps; the skill format has no field that attaches an MCP server, so\nattaching is an **operator/platform step**. Add `drivethru_mcp` to the agent's\nMCP servers using the same env vars, as a Streamable-HTTP server:\n\n```jsonc\n// e.g. openclaw.json → mcp.servers\n\"drivethru_mcp\": {\n  \"url\":       \"<ODOO_MCP_URL>\",          // the .../drivethru_mcp/v1 endpoint\n  \"transport\": \"streamable-http\",\n  \"headers\":   { \"Authorization\": \"Bearer <ODOO_MCP_TOKEN>\" }\n}\n```\n\nOnce it's attached, the tools become native and callable — this is exactly how a\nshell-less Discuss chat agent gets them. Two things to know:\n\n- Some hosts **regenerate the agent config on every boot** from environment /\n  provisioning, so hand-editing the file on the volume won't survive a restart —\n  add the server through the host's agent/MCP configuration, not by hand.\n- If your platform can't attach an arbitrary MCP server, the agent **must** have\n  a shell + `python3` so it can use `scripts/odoo_mcp.py` instead. An agent with\n  **neither** native tools **nor** a shell cannot reach Odoo — that's a\n  provisioning gap to fix, not something the agent can work around.\n\n## Troubleshooting: the agent says it can't reach Odoo\n\nIf you (the agent) are about to say *\"I don't have the Odoo tools in this\nthread\"* or reach for the web — **stop** and run this checklist first:\n\n1. **Do you actually have the tools?** List/search your available tools for\n   `documents_list_folders` (or `ap_search_purchase_orders`). If your runtime\n   defers tools, they may need loading first — load them, then call. \"I don't\n   see them\" usually means \"not loaded yet,\" not \"not available.\"\n2. **Run the one-call smoke test.** `documents_list_folders {\"name\":\n   \"Purchasing\"}`. A folder with a `document_count` back ⇒ you're connected;\n   keep going. If it errors, capture the exact error.\n3. **Is `drivethru_mcp` attached to *this* agent?** No native tool **and** no\n   shell ⇒ the MCP was never attached (env vars alone don't attach it). That's\n   an operator step — see **Operator setup** above; tell the user plainly\n   instead of guessing.\n4. **Are the env vars set?** A `config_error` from the script path, or an auth\n   error from a native call, means `ODOO_MCP_URL` / `ODOO_MCP_TOKEN` are missing\n   or wrong — an operator must set them.\n5. **Report precisely.** If you still can't reach Odoo, state *the exact tool\n   call you attempted and the exact error you got* (e.g. `documents_list_folders\n   {\"name\":\"Purchasing\"}` → `connection_error: …`). Never fabricate an answer or\n   cite the web in place of a tool call.\n\n## Working inside an Odoo Discuss conversation\n\nYou are often answering a **person in an Odoo Discuss DM** (your messages are\nposted straight back into their thread). The bridge prefixes each forwarded\nmessage with a fenced **`[Conversation context]`** block that names the person\nand gives their **Odoo `user_id`**. Lift that `user_id` out and pass it to the\n`knowledge_*` tools so internal-knowledge answers are scoped to what this\nperson is allowed to see (details below). If the context says the sender has no\nlinked Odoo user, don't guess a `user_id` — the knowledge tools can't be used\nfor them. So:\n\n- **Be concise and conversational.** Reply in plain prose, not raw JSON dumps —\n  summarize what the tools returned. Surface a tool error's human-readable\n  message rather than the raw envelope.\n- **Ask for missing information** instead of guessing identifiers. If you need a\n  PO, batch, or order id, look it up with a search/list tool first.\n- **Confirm before writes.** Creating orders/bills, updating prices, and\n  scheduling/planning batches change live Odoo data. State exactly what you're\n  about to do and get the user's go-ahead before calling a write tool.\n- Use `docs_list` / `docs_get` when you need the operator-level rules behind a\n  workflow (e.g. how vendor-bill matching or batch scheduling is meant to work).\n\n## Typical flows\n\n- \"What eBay inventory do we have for A-1, B-2?\" → `ebay_inventory`\n  `{\"skus\": [\"A-1\", \"B-2\"]}`.\n- \"Find the PO for vendor X and bill it.\" → `ap_search_purchase_orders` →\n  `ap_get_purchase_order` → (confirm) → `ap_create_vendor_bill`.\n- \"Check the docs in the Purchasing folder against the POs and fix the pricing.\"\n  → the document-driven PO pricing-review flow below.\n- \"Show the production plan and schedule batch 142 onto workcenter 3.\" →\n  `production_overview` → `production_get_batch` `{\"batch_id\": 142}` → (confirm)\n  → `production_schedule_batch`\n  `{\"batch_id\": 142, \"primary_workcenter_id\": 3, \"date_planned_start\": \"...\"}`.\n- \"Purchase all Adidas items for Batch12345.\" → the replenishment → purchasing\n  flow below.\n- \"What's our SOP for reprinting a damaged order?\" → the internal-knowledge\n  flow below (`knowledge_search_articles` → `knowledge_get_article`).\n\n## Internal knowledge retrieval (SOPs, best practices, policies)\n\nYou double as an **internal operations knowledge agent**. When someone asks a\n\"how do we…\", \"what's our policy on…\", or \"what's the SOP for…\" question,\nanswer it from the Odoo **Knowledge** base rather than from memory — and only\nfrom articles **that person is allowed to see**.\n\nTwo tools, both **permission-scoped to the asking person's `user_id`** (which\nyou take from the `[Conversation context]` block, see above):\n\n1. **Search.** `knowledge_search_articles`\n   `{\"user_id\": <ctx uid>, \"query\": \"reprint damaged order\"}` — free text\n   matches title + body; `root_only` / `parent_id` walk the article tree. Only\n   articles this user may read come back (each with a breadcrumb `path`,\n   `snippet`, and `url`).\n2. **Read in full.** `knowledge_get_article`\n   `{\"user_id\": <ctx uid>, \"article_id\": <id>}` — returns the article's\n   plain-text `body` plus metadata. Answer from this, not from the snippet.\n\n**Permission handling.** The tools query Odoo *as that user*, so access is\nenforced by Odoo's own rules — you don't manage permissions yourself. If\n`knowledge_get_article` returns `accessible: false`, the person simply doesn't\nhave access to that article: tell them that plainly (offer to point them to\nwhoever owns it), and **don't** try to work around it or read it as anyone\nelse. Never pass a `user_id` you weren't given in the conversation context.\n\nWhen you answer, summarize the SOP/policy in plain prose and cite the article\ntitle (and `url` when present) so the person can open the source. Read\n`docs_get {\"slug\": \"knowledge\"}` for the operator-level rules behind this.\n\n## Document-driven PO pricing review (Purchasing folder)\n\nWhen asked to *\"go through the Purchasing folder and check each document's\npricing against the PO, fix any wrong lines, mark the PO checked, and file the\ndocument\"* (vendor order confirmations / acknowledgements / invoices), own the\nwhole loop. This runs many times a day over many documents, so two rules are\nnon-negotiable:\n\n1. **Don't bloat the context window.** `documents_get` returns each file as\n   base64 and a PDF reader adds a page image — huge next to the ~300–600 chars\n   of text that matter. Extract text **out of context, once**, with the intake\n   helper instead of looping `documents_get`/a PDF reader over the folder:\n\n   ```bash\n   python3 scripts/po_docs.py extract '{\"folder\": \"Purchasing\"}'\n   ```\n\n   It fetches every file over the same MCP endpoint, decodes and extracts the\n   text locally, and returns compact JSON (`documents[].text`, no base64, no\n   render). Read that. Only fall back to `documents_get` for a document the\n   helper marks `needs_vision: true` (scanned/image-only), one at a time.\n\n   **No shell (native tools only)?** Enumerate with `documents_list_folders` /\n   `documents_search`, then read documents with `documents_get` **one at a\n   time** — read the text and drop the base64, never pulling a whole folder's\n   bytes into context.\n\n2. **Every document ends in Matched or Questions.** The Purchasing folder has\n   sibling subfolders `Matched` and `Questions`; nothing stays in the inbox.\n\nPer document: read the **PO# from the text** (not the filename — filenames may\ncarry the vendor's order number), then `ap_search_purchase_orders` →\n`ap_get_purchase_order`, pair each line by **(style, color, size)**, compare\n`price_unit`, and batch every fix into one `ap_update_po_lines`. Totals are a\ncross-check only — a shipment acknowledgement is often one box of a\nmulti-shipment order, so a total gap explained by un-shipped lines is not a\ndiscrepancy. Post a \"checked\" note with `po_post_message`. Then file it:\n\n```bash\n# reconciled (matched, or corrected with confidence)\npython3 scripts/po_docs.py move '{\"document_id\": <id>, \"to\": \"Matched\", \"under\": \"Purchasing\"}'\n# a real question — raise it on the document, assign Zach, then file to Questions\ndocuments_post_message {\"document_id\": <id>, \"body\": \"<question>\", \"activity_user\": \"Zach Tucker\"}\npython3 scripts/po_docs.py move '{\"document_id\": <id>, \"to\": \"Questions\", \"under\": \"Purchasing\"}'\n```\n\nOnly escalate a **genuine** ambiguity (unreadable PO#, prices that don't\nreconcile, unexpected/missing lines, wrong vendor). An unambiguous fix backed\nby the vendor document is a Matched correction, not a question.\n\nFull procedure, matching rules (partial shipments, freight/fees, PO#-from-body),\na worked example, and a **low-cost model recommendation** for running this at\nvolume are in\n[`references/po_pricing_review.md`](references/po_pricing_review.md).\n\n## Replenishment → vendor purchasing\n\nWhen the user asks you to **buy** what the replenishment report says to buy for\na vendor (e.g. *\"Purchase all items from Adidas for Batch12345\"*), you own the\nwhole loop: read the report, curate the lines, put them on a PO, drive the\nvendor's checkout, then write the result back and confirm. The\n`drivethru_mcp` tools handle the Odoo side; a separate **vendor purchasing\nskill** handles the actual buying.\n\nRun it in this order — **confirm the curated set with the user before you\nreplenish, and again before you confirm the PO** (both are live writes):\n\n1. **Run the report.** `replenish_run_report` with the vendor filter (e.g.\n   `{\"vendor\": \"Adidas\"}`). It rebuilds the replenishment report and returns\n   the shortage lines with `style` / `color` / `size` / `qty_to_order` / `price`\n   / `production_batch_name` / `sale_order_name` / `is_out_of_stock`.\n2. **Curate.** Keep only the lines the request calls for — *\"for Batch12345\"*\n   means keep lines whose `production_batch_name` matches; *\"all Adidas\"* means\n   keep them all. This filtering is your responsibility. Show the user the kept\n   lines (style/color/size/qty) and get a go-ahead.\n3. **Add to a PO.** `replenish_to_po` `{\"orderpoint_ids\": [...]}`. This runs\n   `action_replenish` and **returns the draft PO(s)** with each line's\n   `line_id` + style/color/size/`product_qty`. Anything that couldn't be\n   bought (manufacture route, held sales order) comes back under `warnings` —\n   surface it.\n4. **Find the vendor skill & purchase.** Locate the vendor's purchasing skill —\n   named `drivethru-<vendor>-click` (e.g. **`drivethru-adidas-click`**) — and\n   read its `SKILL.md`. For adidas that's a CLI: `echo '{...}' | python3\n   scripts/adidas.py create-purchase-order`, taking `{purchase_order:\n   {po_number, lines:[{style, size, quantity}]}, confirm}` (map the Odoo PO\n   `name` → `po_number`; color is optional — the adidas article encodes it).\n   **Dry-run first** (`confirm: false`), show the user, then `confirm: true`\n   places a real order. If no such skill exists, stop and tell the user; do not\n   invent a checkout.\n5. **Write back.** Take the skill's result — match each result line to its Odoo\n   PO line by **(style, size)** to get the `line_id`, parse the `$`-prices to\n   numbers — and apply it with `replenish_update_po`: `price_unit` per\n   `line_id`, `vendor_order_number` ← the `confirmation_number`.\n6. **Handle out-of-stock / exceptions.** The adidas skill pauses on a line it\n   can't fill and returns `status: \"needs_confirmation\"` with `out_of_stock`.\n   On that, `po_post_message` onto the PO with `issue_type: \"out_of_stock\"` and\n   an `activity_user_id` so purchasing/sales sees it — then **pause that PO**.\n   `po_get_messages` later to read the reply, then re-run the vendor skill with\n   the chosen policy (`on_insufficient_stock: \"order\"` / `\"skip\"`, or edited\n   lines to substitute). Do not confirm a PO with an unresolved issue.\n7. **Confirm.** Once pricing is written and there are no open issues,\n   `replenish_confirm_po`. It confirms **without re-submitting to the vendor**\n   (the buy already happened via the skill).\n\nThe full hand-off contract (the exact dataset shape you pass the vendor skill\nand the result shape you expect back) is in\n[`references/replenishment_purchasing.md`](references/replenishment_purchasing.md).\nRead `docs_get {\"slug\": \"replenishment\"}` for the operator-level rules.\n\n## Errors\n\nThese are the **script path's** error envelope; on the **native path** a failed\ntool call surfaces its own error / `isError` result directly — read it and relay\nthe human-readable message, don't retry blindly or give up silently.\n\n- `config_error` (exit 2) — `ODOO_MCP_URL` / `ODOO_MCP_TOKEN` missing.\n- `invalid_arguments` (exit 2) — bad CLI usage or non-JSON arguments.\n- `connection_error` — Odoo unreachable, transport error, or the key was\n  rejected (the MCP server requires a valid key even to list tools).\n- A tool that ran but failed returns a normal MCP result with `isError: true`\n  and a human-readable message in its content — surface that to the user.\n\n## References\n\n- [`references/agent_api_endpoints.md`](references/agent_api_endpoints.md) — the\n  underlying Odoo operations (now exposed as MCP tools); `tools/list` is\n  authoritative for the live schema.\n- [`references/production_scheduling.md`](references/production_scheduling.md) —\n  the MRP data model behind the production tools.\n- [`references/replenishment_purchasing.md`](references/replenishment_purchasing.md) —\n  the replenishment → vendor-purchasing loop and the vendor-skill hand-off\n  contract (dataset in, result out).\n\n## Legacy REST scripts (deprecated)\n\n`scripts/sales.py`, `scripts/ap.py`, `scripts/production.py`, and\n`scripts/odoo_client.py` are the previous REST wrappers for the old `agent_api`\nmodule. They still work against the `drivethru_mcp` module's REST back-compat\nroutes (`/agent_api/v1/*`) but are deprecated — prefer `scripts/odoo_mcp.py`.\nThey will be removed once all deployments are on the MCP surface.\n\nFile v0.9.2:_meta.json\n\n{\n  \"ownerId\": \"kn715tnf30wegyr6mdbfa17avd87bjr6\",\n  \"slug\": \"drivethru-odoo\",\n  \"version\": \"0.9.2\",\n  \"publishedAt\": 1789653510791\n}\n\nFile v0.9.2:references/agent_api_endpoints.md\n\n# Odoo `agent_api` endpoint surface\n\nAll endpoints are served by the Odoo `agent_api` addon, authenticated with the\n`X-Agent-API-Key` header. Responses are JSON. The client unwraps a top-level\n`{\"success\": true, \"data\": {...}}` envelope when present and raises on\n`success: false` or non-2xx status.\n\nBase URL = `ODOO_URL` (no trailing slash). All paths below are relative to it.\n\n## Sales / eBay — `scripts/sales.py`\n\n| Method | Path                                              | Action         |\n| ------ | ------------------------------------------------- | -------------- |\n| GET    | `/agent_api/v1/ebay/products`                     | `list-products` |\n| GET    | `/agent_api/v1/ebay/inventory?skus=A,B`           | `inventory`    |\n| POST   | `/agent_api/v1/ebay/orders`                       | `create-order` |\n| GET    | `/agent_api/v1/ebay/orders/<odoo_order_id>/tracking` | `tracking`  |\n\n### Product shape (from `list-products`)\n\n```json\n{\n  \"sku\": \"ABC-1\", \"title\": \"...\", \"description\": \"...\", \"brand\": \"...\",\n  \"mpn\": \"...\", \"condition\": \"NEW\", \"category_id\": \"...\",\n  \"images\": [\"https://...\"], \"aspects\": {\"Color\": [\"Navy\"]},\n  \"cost\": 12.50, \"quantity\": 7, \"weight_oz\": 6.0,\n  \"package_length_in\": 0, \"package_width_in\": 0, \"package_height_in\": 0\n}\n```\n\n### Order-create payload (POST body built by `create-order`)\n\nThe `create-order` action accepts an eBay order object and maps it to:\n\n```json\n{\n  \"ebay_order_id\": \"...\", \"ebay_legacy_order_id\": \"...\", \"creation_date\": \"...\",\n  \"buyer_username\": \"...\", \"buyer_email\": \"...\", \"currency\": \"USD\",\n  \"subtotal\": 0.0, \"tax\": 0.0, \"shipping\": 0.0, \"total\": 0.0,\n  \"line_items\": [\n    {\"line_item_id\": \"...\", \"sku\": \"...\", \"title\": \"...\", \"quantity\": 1,\n     \"unit_price\": 0.0, \"line_total\": 0.0, \"tax\": 0.0}\n  ],\n  \"shipping_address\": {\"name\": \"...\", \"address_line_1\": \"...\", \"address_line_2\": \"...\",\n     \"city\": \"...\", \"state\": \"...\", \"postal_code\": \"...\", \"country\": \"US\",\n     \"phone\": \"...\", \"email\": \"...\"},\n  \"billing_address\": { ... }\n}\n```\n\nResponse: `{odoo_order_id, odoo_order_name, already_existed, confirmed,\npartner_id, shipping_partner_id, invoice_partner_id, confirm_error}`.\n`already_existed: true` ⇒ idempotent skip (Odoo already had this eBay order).\n\n### Tracking response\n\n`{\"shipped\": true|false, \"tracking\": {\"carrier\", \"tracking_number\",\n\"shipped_date\"} | null}`. `tracking` is null until the order ships.\n\n## Accounts Payable — `scripts/ap.py`\n\n| Method | Path                                              | Action            |\n| ------ | ------------------------------------------------- | ----------------- |\n| GET    | `/agent_api/v1/ap/purchase_orders`                | `search-pos`      |\n| GET    | `/agent_api/v1/ap/purchase_orders/<po_id>`        | `get-po`          |\n| PUT    | `/agent_api/v1/ap/purchase_orders/<po_id>/lines`  | `update-po-lines` |\n| POST   | `/agent_api/v1/ap/invoices`                       | `create-bill`     |\n| GET    | `/agent_api/v1/ap/invoices/<bill_id>`             | `get-bill`        |\n| GET    | `/agent_api/v1/ap/vendors`                        | `search-vendors`  |\n\n- `search-pos` query params: `search`, `vendor`, `state`, `limit`.\n- `update-po-lines` body: `{\"lines\": [{\"line_id\", \"price_unit\"}],\n  \"freight_cost\"?, \"fees_cost\"?}`.\n- `create-bill` body: `{\"po_id\", \"vendor_bill_number\"?, \"invoice_date\"?,\n  \"line_ids\"?, \"reviewer_user_id\"?, \"review_note\"?, \"expected_total\"?,\n  \"tolerance\"?}`. The bill is created in **draft**; Odoo's create() override\n  auto-adds buying-group fee lines, a freight line (if `freight_cost > 0`), a\n  misc-fees line (if `fees_cost > 0`), and partner-level purchase fee lines.\n\n## Production scheduling — `scripts/production.py`\n\n| Method | Path                                                   | Action               |\n| ------ | ------------------------------------------------------ | -------------------- |\n| GET    | `/agent_api/v1/production/overview`                    | `overview`           |\n| GET    | `/agent_api/v1/production/batches`                     | `list-batches`       |\n| GET    | `/agent_api/v1/production/batches/<id>`                | `get-batch`          |\n| PUT    | `/agent_api/v1/production/batches/<id>/schedule`       | `schedule`           |\n| POST   | `/agent_api/v1/production/batches/<id>/plan`           | `plan`               |\n| POST   | `/agent_api/v1/production/batches/bulk_schedule`       | `bulk-schedule`      |\n| GET    | `/agent_api/v1/production/workcenters`                 | `list-workcenters`   |\n| GET    | `/agent_api/v1/production/workcenters/<id>`            | `get-workcenter`     |\n| GET    | `/agent_api/v1/production/production_centers`          | `production-centers` |\n| GET    | `/agent_api/v1/production/decoration_methods`          | `decoration-methods` |\n\nSee [`production_scheduling.md`](production_scheduling.md) for the data model\nand field semantics.\n\nFile v0.9.2:references/po_pricing_review.md\n\n# Document-driven PO pricing review (batch)\n\nThe pattern behind *\"go through every document in the Purchasing folder, check\nits pricing against the purchase order, fix any line that's wrong, mark the PO\nchecked, and file the document.\"* This runs many times a day over 5–50+\ndocuments, so the whole design is built to (a) keep the model's context window\nsmall no matter how many documents there are, and (b) leave the Purchasing\ninbox empty when it's done — every document ends up in **Matched** or\n**Questions**.\n\nRead `docs_get {\"slug\": \"documents\"}` and `docs_get {\"slug\": \"invoices\"}` for\nthe underlying tool semantics; this file is the operating procedure.\n\n## The efficiency rule: extract text out-of-context, once\n\n`documents_get` returns each file's bytes as **base64**, and reading a PDF\nthrough a multimodal reader adds a **page image** on top. Both are enormous\nnext to the ~300–600 characters of text that actually matter per document. Do\nthat per file across a folder and the context window fills with base64 and\nrenders — the single biggest cost in a multi-document run.\n\n**So do not loop `documents_get` (or a PDF reader) over the folder.** Use the\nintake helper, which fetches every file, decodes and extracts the text\n**locally**, and returns compact JSON — text only, no base64, no image:\n\n```bash\npython3 scripts/po_docs.py extract '{\"folder\": \"Purchasing\"}'\n```\n\nReturns `{\"folder\", \"count\", \"documents\": [{document_id, name, mimetype,\ntext, chars, needs_vision, open_activities}]}`. One call = the whole folder as\nplain text. Read that; work from it.\n\n- **`needs_vision: true`** on a document means the text pass came up empty (a\n  scanned/image-only PDF or an image file). Only for those, fall back to\n  `documents_get {\"document_id\"}` and read the bytes with a vision-capable\n  reader — never for the whole folder.\n- If the helper can't run (no shell, or `ODOO_MCP_URL`/`ODOO_MCP_TOKEN` unset),\n  fall back to `documents_get` **one document at a time**, extract what you\n  need, and don't carry the base64 forward. Never fetch the whole folder's\n  bytes into context at once.\n\n## Reading a document (per file)\n\nExtract from the document's **text**, not its filename:\n\n- **PO number** — the authoritative PO# is *inside* the document. Filenames can\n  carry the vendor's order number instead (e.g. a file named\n  `Order Acknowledgement 48482500.pdf` whose real PO is `P13183` in the body).\n  Search Odoo on the PO# you read from the text.\n- **Line items** — item/style, color, size, quantity, and **unit price** per\n  line. Vendor size upcharges are normal (base sizes one price; 2XL/3XL/4XL\n  higher) — that's correct pricing, not an error.\n- **Totals are a cross-check, not the source of truth.** A shipment\n  acknowledgement is often **one box of a multi-shipment order**, so its total\n  is legitimately less than the PO total — the missing lines ship later. Match\n  **line by line**, and treat a total gap that's fully e\n\nArchive v0.9.1: 14 files, 42507 bytes\n\nFiles: references/agent_api_endpoints.md (4898b), references/po_pricing_review.md (9131b), references/production_scheduling.md (5198b), references/replenishment_purchasing.md (9698b), scripts/_cli.py (2681b), scripts/ap.py (3424b), scripts/odoo_client.py (18932b), scripts/odoo_mcp.py (5288b), scripts/po_docs.py (12729b), scripts/production.py (4981b), scripts/sales.py (7240b), skill-card.md (3098b), SKILL.md (23213b), _meta.json (133b)\n\nArchive v0.9.0: 14 files, 42173 bytes\n\nFiles: references/agent_api_endpoints.md (4898b), references/po_pricing_review.md (9131b), references/production_scheduling.md (5198b), references/replenishment_purchasing.md (9698b), scripts/_cli.py (2681b), scripts/ap.py (3424b), scripts/odoo_client.py (18932b), scripts/odoo_mcp.py (4787b), scripts/po_docs.py (12729b), scripts/production.py (4981b), scripts/sales.py (7240b), skill-card.md (2904b), SKILL.md (23213b), _meta.json (133b)\n\nArchive v0.7.0: 14 files, 41508 bytes\n\nFiles: references/agent_api_endpoints.md (4898b), references/po_pricing_review.md (9131b), references/production_scheduling.md (4178b), references/replenishment_purchasing.md (9698b), scripts/_cli.py (2681b), scripts/ap.py (3424b), scripts/odoo_client.py (18932b), scripts/odoo_mcp.py (4787b), scripts/po_docs.py (12729b), scripts/production.py (4981b), scripts/sales.py (7240b), skill-card.md (2471b), SKILL.md (23213b), _meta.json (133b)\n\nArchive v0.6.0: 14 files, 41487 bytes\n\nFiles: references/agent_api_endpoints.md (4898b), references/po_pricing_review.md (9131b), references/production_scheduling.md (3660b), references/replenishment_purchasing.md (9698b), scripts/_cli.py (2681b), scripts/ap.py (3424b), scripts/odoo_client.py (18932b), scripts/odoo_mcp.py (4787b), scripts/po_docs.py (12729b), scripts/production.py (4981b), scripts/sales.py (7240b), skill-card.md (3079b), SKILL.md (23159b), _meta.json (133b)\n\nArchive v0.5.0: 14 files, 39121 bytes\n\nFiles: references/agent_api_endpoints.md (4898b), references/po_pricing_review.md (9131b), references/production_scheduling.md (3660b), references/replenishment_purchasing.md (9698b), scripts/_cli.py (2681b), scripts/ap.py (3424b), scripts/odoo_client.py (18932b), scripts/odoo_mcp.py (4787b), scripts/po_docs.py (12729b), scripts/production.py (4981b), scripts/sales.py (7240b), skill-card.md (2585b), SKILL.md (17275b), _meta.json (133b)\n\nArchive v0.4.0: 12 files, 28870 bytes\n\nFiles: references/agent_api_endpoints.md (4898b), references/production_scheduling.md (3660b), references/replenishment_purchasing.md (9698b), scripts/_cli.py (2681b), scripts/ap.py (3424b), scripts/odoo_client.py (18932b), scripts/odoo_mcp.py (4787b), scripts/production.py (4981b), scripts/sales.py (7240b), skill-card.md (2757b), SKILL.md (13429b), _meta.json (133b)\n\nArchive v0.3.1: 12 files, 27835 bytes\n\nFiles: references/agent_api_endpoints.md (4898b), references/production_scheduling.md (3660b), references/replenishment_purchasing.md (9698b), scripts/_cli.py (2681b), scripts/ap.py (3424b), scripts/odoo_client.py (18932b), scripts/odoo_mcp.py (4787b), scripts/production.py (4981b), scripts/sales.py (7240b), skill-card.md (2622b), SKILL.md (10910b), _meta.json (133b)","readmeExcerpt":"Skill: drivethru-odoo Owner: zmtucker Summary: Talk to an Odoo ERP through its drivethru_mcp MCP server — discover the available Odoo tools at runtime and call them to look up eBay products/inventory, push eBay orders and read tracking, run the Accounts Payable PO→vendor-bill flow, review documents in the Documents app against their purchase orders and fix incorrect PO line pricing (the \"check the Purchasing folder a","codeSnippets":[],"executableExamples":[{"language":"bash","snippet":"python3 scripts/odoo_mcp.py tools                        # list tools + JSON input schemas\n   python3 scripts/odoo_mcp.py call <tool> '{\"...\":\"...\"}'  # call one (args as 2nd arg or on stdin)"},{"language":"jsonc","snippet":"// e.g. openclaw.json → mcp.servers\n\"drivethru_mcp\": {\n  \"url\":       \"<ODOO_MCP_URL>\",          // the .../drivethru_mcp/v1 endpoint\n  \"transport\": \"streamable-http\",\n  \"headers\":   { \"Authorization\": \"Bearer <ODOO_MCP_TOKEN>\" }\n}"},{"language":"bash","snippet":"python3 scripts/po_docs.py extract '{\"folder\": \"Purchasing\"}'"},{"language":"bash","snippet":"# reconciled (matched, or corrected with confidence)\npython3 scripts/po_docs.py move '{\"document_id\": <id>, \"to\": \"Matched\", \"under\": \"Purchasing\"}'\n# a real question — raise it on the document, assign Zach, then file to Questions\ndocuments_post_message {\"document_id\": <id>, \"body\": \"<question>\", \"activity_user\": \"Zach Tucker\"}\npython3 scripts/po_docs.py move '{\"document_id\": <id>, \"to\": \"Questions\", \"under\": \"Purchasing\"}'"},{"language":"json","snippet":"{\n  \"sku\": \"ABC-1\", \"title\": \"...\", \"description\": \"...\", \"brand\": \"...\",\n  \"mpn\": \"...\", \"condition\": \"NEW\", \"category_id\": \"...\",\n  \"images\": [\"https://...\"], \"aspects\": {\"Color\": [\"Navy\"]},\n  \"cost\": 12.50, \"quantity\": 7, \"weight_oz\": 6.0,\n  \"package_length_in\": 0, \"package_width_in\": 0, \"package_height_in\": 0\n}"},{"language":"json","snippet":"{\n  \"ebay_order_id\": \"...\", \"ebay_legacy_order_id\": \"...\", \"creation_date\": \"...\",\n  \"buyer_username\": \"...\", \"buyer_email\": \"...\", \"currency\": \"USD\",\n  \"subtotal\": 0.0, \"tax\": 0.0, \"shipping\": 0.0, \"total\": 0.0,\n  \"line_items\": [\n    {\"line_item_id\": \"...\", \"sku\": \"...\", \"title\": \"...\", \"quantity\": 1,\n     \"unit_price\": 0.0, \"line_total\": 0.0, \"tax\": 0.0}\n  ],\n  \"shipping_address\": {\"name\": \"...\", \"address_line_1\": \"...\", \"address_line_2\": \"...\",\n     \"city\": \"...\", \"state\": \"...\", \"postal_code\": \"...\", \"country\": \"US\",\n     \"phone\": \"...\", \"email\": \"...\"},\n  \"billing_address\": { ... }\n}"}],"parameters":null,"dependencies":[],"permissions":[],"extractedFiles":[{"path":"SKILL.md","content":"---\nname: drivethru-odoo\ndescription: Talk to an Odoo ERP through its `drivethru_mcp` MCP server — discover the available Odoo tools at runtime and call them to look up eBay products/inventory, push eBay orders and read tracking, run the Accounts Payable PO→vendor-bill flow, review documents in the Documents app against their purchase orders and fix incorrect PO line pricing (the \"check the Purchasing folder against the POs\" / vendor-invoice pricing-review workflow, filing each document into Matched or Questions), schedule MRP production batches, drive vendor replenishment purchasing (run the replenishment report → curate lines → add to a PO → hand style/color/size/qty to the vendor's purchasing skill → write pricing + confirmation back and confirm the PO), and retrieve internal SOPs / best practices / policies from the Knowledge base scoped to the asking person's permissions. Use whenever the user needs to read from or write to Odoo, especially when you are answering a person inside an Odoo Discuss conversation.\nversion: 0.9.4\nemoji: 🏭\nhomepage: https://www.odoo.com\nmetadata:\n  openclaw:\n    requires:\n      env: [ODOO_MCP_URL, ODOO_MCP_TOKEN]\n      bins: [python3]\n    primaryEnv: ODOO_MCP_TOKEN\n    envVars:\n      ODOO_MCP_URL:\n        required: true\n        description: >\n          Full URL of the Odoo MCP endpoint, e.g.\n          `https://odoo.example.com/drivethru_mcp/v1` (note the path — this is\n          the MCP server exposed by the `drivethru_mcp` Odoo module, not the\n          Odoo base URL).\n      ODOO_MCP_TOKEN:\n        required: true\n        description: >\n          The `drivethru.mcp_key` value from the Odoo `drivethru_mcp` module,\n          sent as `Authorization: Bearer`. Treat as a secret; never paste into\n          chat.\n    install:\n      uv:\n        - mcp>=1.9.0\n        - pypdf>=4.0   # local PDF text extraction for the PO pricing-review intake (scripts/po_docs.py)\n---\n\n# Odoo Drive Thru MCP integration\n\nThis skill gives you the Odoo **`drivethru_mcp`** MCP server — the curated Odoo\ntool surface (`documents_*`, `ap_*`, `po_*`, `mfg_*`, `production_*`,\n`replenish_*`, `knowledge_*`, `ebay_*`, `docs_*`) exposed over Streamable-HTTP.\n`ODOO_MCP_URL` and `ODOO_MCP_TOKEN` are already configured for this agent.\n**Never tell the user you can't reach Odoo, that you \"don't have the tools in\nthis thread,\" or cite the web instead — either call a tool, or state the exact\ntool call you attempted and the error it returned.**\n\n## How you call Odoo (runtime-aware — read this first)\n\nThere are two ways the `drivethru_mcp` tools reach you. Work out which you have,\nin this order:\n\n1. **Native / callable MCP tools — preferred, no shell needed.** If the Odoo\n   tools are attached to you as native tools, **call them directly** — e.g.\n   `documents_list_folders {\"name\": \"Purchasing\"}`. This is how a chat agent\n   (an Odoo Discuss bot, etc.) uses Odoo; it does **not** need a shell.\n   - They may be **deferred / lazy-loaded**: if your runtime hides tools"},{"path":"_meta.json","content":"{\n  \"ownerId\": \"kn715tnf30wegyr6mdbfa17avd87bjr6\",\n  \"slug\": \"drivethru-odoo\",\n  \"version\": \"0.9.4\",\n  \"publishedAt\": 1789682303073\n}"},{"path":"references/agent_api_endpoints.md","content":"# Odoo `agent_api` endpoint surface\n\nAll endpoints are served by the Odoo `agent_api` addon, authenticated with the\n`X-Agent-API-Key` header. Responses are JSON. The client unwraps a top-level\n`{\"success\": true, \"data\": {...}}` envelope when present and raises on\n`success: false` or non-2xx status.\n\nBase URL = `ODOO_URL` (no trailing slash). All paths below are relative to it.\n\n## Sales / eBay — `scripts/sales.py`\n\n| Method | Path                                              | Action         |\n| ------ | ------------------------------------------------- | -------------- |\n| GET    | `/agent_api/v1/ebay/products`                     | `list-products` |\n| GET    | `/agent_api/v1/ebay/inventory?skus=A,B`           | `inventory`    |\n| POST   | `/agent_api/v1/ebay/orders`                       | `create-order` |\n| GET    | `/agent_api/v1/ebay/orders/<odoo_order_id>/tracking` | `tracking`  |\n\n### Product shape (from `list-products`)\n\n```json\n{\n  \"sku\": \"ABC-1\", \"title\": \"...\", \"description\": \"...\", \"brand\": \"...\",\n  \"mpn\": \"...\", \"condition\": \"NEW\", \"category_id\": \"...\",\n  \"images\": [\"https://...\"], \"aspects\": {\"Color\": [\"Navy\"]},\n  \"cost\": 12.50, \"quantity\": 7, \"weight_oz\": 6.0,\n  \"package_length_in\": 0, \"package_width_in\": 0, \"package_height_in\": 0\n}\n```\n\n### Order-create payload (POST body built by `create-order`)\n\nThe `create-order` action accepts an eBay order object and maps it to:\n\n```json\n{\n  \"ebay_order_id\": \"...\", \"ebay_legacy_order_id\": \"...\", \"creation_date\": \"...\",\n  \"buyer_username\": \"...\", \"buyer_email\": \"...\", \"currency\": \"USD\",\n  \"subtotal\": 0.0, \"tax\": 0.0, \"shipping\": 0.0, \"total\": 0.0,\n  \"line_items\": [\n    {\"line_item_id\": \"...\", \"sku\": \"...\", \"title\": \"...\", \"quantity\": 1,\n     \"unit_price\": 0.0, \"line_total\": 0.0, \"tax\": 0.0}\n  ],\n  \"shipping_address\": {\"name\": \"...\", \"address_line_1\": \"...\", \"address_line_2\": \"...\",\n     \"city\": \"...\", \"state\": \"...\", \"postal_code\": \"...\", \"country\": \"US\",\n     \"phone\": \"...\", \"email\": \"...\"},\n  \"billing_address\": { ... }\n}\n```\n\nResponse: `{odoo_order_id, odoo_order_name, already_existed, confirmed,\npartner_id, shipping_partner_id, invoice_partner_id, confirm_error}`.\n`already_existed: true` ⇒ idempotent skip (Odoo already had this eBay order).\n\n### Tracking response\n\n`{\"shipped\": true|false, \"tracking\": {\"carrier\", \"tracking_number\",\n\"shipped_date\"} | null}`. `tracking` is null until the order ships.\n\n## Accounts Payable — `scripts/ap.py`\n\n| Method | Path                                              | Action            |\n| ------ | ------------------------------------------------- | ----------------- |\n| GET    | `/agent_api/v1/ap/purchase_orders`                | `search-pos`      |\n| GET    | `/agent_api/v1/ap/purchase_orders/<po_id>`        | `get-po`          |\n| PUT    | `/agent_api/v1/ap/purchase_orders/<po_id>/lines`  | `update-po-lines` |\n| POST   | `/agent_api/v1/ap/invoices`                       | `create-bill`     |\n| GET    | `/agent_api/v1/ap/invoices/<bill_id>`             | `ge"},{"path":"references/po_pricing_review.md","content":"# Document-driven PO pricing review (batch)\n\nThe pattern behind *\"go through every document in the Purchasing folder, check\nits pricing against the purchase order, fix any line that's wrong, mark the PO\nchecked, and file the document.\"* This runs many times a day over 5–50+\ndocuments, so the whole design is built to (a) keep the model's context window\nsmall no matter how many documents there are, and (b) leave the Purchasing\ninbox empty when it's done — every document ends up in **Matched** or\n**Questions**.\n\nRead `docs_get {\"slug\": \"documents\"}` and `docs_get {\"slug\": \"invoices\"}` for\nthe underlying tool semantics; this file is the operating procedure.\n\n## The efficiency rule: extract text out-of-context, once\n\n`documents_get` returns each file's bytes as **base64**, and reading a PDF\nthrough a multimodal reader adds a **page image** on top. Both are enormous\nnext to the ~300–600 characters of text that actually matter per document. Do\nthat per file across a folder and the context window fills with base64 and\nrenders — the single biggest cost in a multi-document run.\n\n**So do not loop `documents_get` (or a PDF reader) over the folder.** Use the\nintake helper, which fetches every file, decodes and extracts the text\n**locally**, and returns compact JSON — text only, no base64, no image:\n\n```bash\npython3 scripts/po_docs.py extract '{\"folder\": \"Purchasing\"}'\n```\n\nReturns `{\"folder\", \"count\", \"documents\": [{document_id, name, mimetype,\ntext, chars, needs_vision, open_activities}]}`. One call = the whole folder as\nplain text. Read that; work from it.\n\n- **`needs_vision: true`** on a document means the text pass came up empty (a\n  scanned/image-only PDF or an image file). Only for those, fall back to\n  `documents_get {\"document_id\"}` and read the bytes with a vision-capable\n  reader — never for the whole folder.\n- If the helper can't run (no shell, or `ODOO_MCP_URL`/`ODOO_MCP_TOKEN` unset),\n  fall back to `documents_get` **one document at a time**, extract what you\n  need, and don't carry the base64 forward. Never fetch the whole folder's\n  bytes into context at once.\n\n## Reading a document (per file)\n\nExtract from the document's **text**, not its filename:\n\n- **PO number** — the authoritative PO# is *inside* the document. Filenames can\n  carry the vendor's order number instead (e.g. a file named\n  `Order Acknowledgement 48482500.pdf` whose real PO is `P13183` in the body).\n  Search Odoo on the PO# you read from the text.\n- **Line items** — item/style, color, size, quantity, and **unit price** per\n  line. Vendor size upcharges are normal (base sizes one price; 2XL/3XL/4XL\n  higher) — that's correct pricing, not an error.\n- **Totals are a cross-check, not the source of truth.** A shipment\n  acknowledgement is often **one box of a multi-shipment order**, so its total\n  is legitimately less than the PO total — the missing lines ship later. Match\n  **line by line**, and treat a total gap that's fully explained by un-shipped\n  lines as *not* a discrepancy.\n"},{"path":"references/production_scheduling.md","content":"# Production scheduling (MRP) data model\n\nThe production actions in `scripts/production.py` map the\n`/agent_api/v1/production/*` surface of the Production Scheduling Agent\nIntegration Schematic.\n\n## Entities\n\n| Entity                   | Odoo model              | Meaning                                            |\n| ------------------------ | ----------------------- | -------------------------------------------------- |\n| **Production batch**     | `mrp.production.batch`  | A batch of MOs sharing a machine and time slot.    |\n| **Workcenter**           | `mrp.workcenter`        | A machine, e.g. \"Manual Press A\".                  |\n| **Production center**    | `production.center`     | An area grouping machines, e.g. \"Screen Print Floor\". |\n| **Decoration method**    | `decoration.method`     | A process type, e.g. \"Screen Print\", \"Embroidery\". |\n\n## The writable scheduling fields\n\nEach batch is scheduled by writing any subset of:\n\n| Field                     | Meaning                                              |\n| ------------------------- | ---------------------------------------------------- |\n| `sequence`                | **Run order** — the batch's rank in the queue, and the primary scheduling decision. Rank a whole queue with `production_set_run_order`. |\n| `manual_sequence`         | A manufacturing manager pinned this batch by hand; agent `sequence` writes skip it. |\n| `primary_workcenter_id`   | Machine assignment.                                  |\n| `production_center_id`    | Derived from the workcenter if omitted.              |\n| `date_planned_start`      | ISO 8601 (UTC). Optional — a refinement on the run order, not a substitute for it. |\n| `date_planned_finished`   | Auto-computed from start + Σ MO `duration_expected` unless set explicitly. |\n\n## Event dates only lead when they're near\n\nAn `event_date` that is late or within `event_horizon_days` (default 5) sets\n`event_imminent` and outranks everything else in the queue. A **distant** event\ndate does not: past the horizon a batch is ordered by its *governing deadline*\n— the earlier of its event and ship dates — so a far-off event gives way to a\nsooner ship date instead of jumping the queue.\n\n## Art readiness tiers the queue\n\n`production_schedule_queue` does not rank on dates alone. A batch whose decals\naren't printed (`print_decals_status` = `no` / `partial` / `sample`) or whose\nartwork isn't ready (`art_status` != `done`) can't run, so it is **deferred\nbelow runnable work** — until it's due within `art_release_days` (default 3),\nwhen `art_at_risk` flips true and it is **expedited to the top**.\n\nA top-ranked batch with `art_ready: false` is a call to chase the art, not a\njob to schedule; `art_blockers` names what's missing. The\n`drivethru-production-scheduler` skill covers this in full.\n\nThe server applies writes in safe order: run order → `production_center_id` →\n`primary_workcenter_id` (derives center if omitted) → `date_planned_start`\n(auto-computes finish) → `date_planned_"}],"languages":[],"docsSourceLabel":"CLAWHUB","editorialOverview":null,"editorialQuality":{"score":100,"threshold":65,"status":"thin","wordCount":2334,"uniquenessScore":38,"reasons":["uniqueness-below-45"]}},"media":{"evidence":{"source":"no-media","verified":false,"confidence":"low","updatedAt":"2026-10-09T20:33:23.739Z","emptyReason":"No screenshots, media assets, or demo links are available."},"primaryImageUrl":null,"mediaAssetCount":0,"assets":[],"demoUrl":null},"ownerResources":{"evidence":{"source":"unclaimed","verified":false,"confidence":"low","updatedAt":"2026-10-09T20:33:23.739Z","emptyReason":"This page has not been claimed by the agent owner."},"hasCustomPage":false,"customPageUpdatedAt":null,"customLinks":[],"structuredLinks":{"docsUrl":null,"demoUrl":null,"supportUrl":null,"pricingUrl":null,"statusUrl":null},"customPage":null},"relatedAgents":{"evidence":{"source":"protocol-neighbors","verified":false,"confidence":"medium","updatedAt":"2026-10-10T03:57:21.277Z","emptyReason":null},"items":[{"id":"8ebccd8e-3863-4187-8355-c3f14e1f9edf","entityType":"agent","canonicalPath":"/agent/iofficeai-aionui","slug":"iofficeai-aionui","name":"AionUi","description":"Free, local, open-source 24/7 Cowork app and OpenClaw for Gemini CLI, Claude Code, Codex, OpenCode, Qwen Code, Goose CLI, Auggie, and more | 🌟 Star if you like it!","url":"https://github.com/iOfficeAI/AionUi","homepage":"https://www.aionui.com","source":"GITHUB_REPOS","protocols":["MCP","OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-10-09T19:11:12.944Z","createdAt":"2026-02-25T03:38:16.584Z","downloads":null},{"id":"b917f68a-ebff-438e-84f8-3f4b2494c0bc","entityType":"agent","canonicalPath":"/agent/activepieces-activepieces","slug":"activepieces-activepieces","name":"activepieces","description":"AI Agents & MCPs & AI Workflow Automation • (~400 MCP servers for AI agents) • AI Automation / AI Agent with MCPs • AI Workflows & AI Agents • MCPs for AI Agents","url":"https://github.com/activepieces/activepieces","homepage":"https://www.activepieces.com","source":"GITHUB_REPOS","protocols":["OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-04-15T02:22:12.426Z","createdAt":"2026-02-25T03:38:12.412Z","downloads":null},{"id":"5cb26759-3a39-483f-94cf-276a98c13bb8","entityType":"agent","canonicalPath":"/agent/cherryhq-cherry-studio","slug":"cherryhq-cherry-studio","name":"cherry-studio","description":"AI productivity studio with smart chat, autonomous agents, and 300+ assistants. Unified access to frontier LLMs","url":"https://github.com/CherryHQ/cherry-studio","homepage":"https://cherry-ai.com","source":"GITHUB_REPOS","protocols":["MCP","OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-04-11T14:38:40.986Z","createdAt":"2026-02-25T03:38:19.379Z","downloads":null},{"id":"6f6582d0-5d76-4f0f-b81d-86520247950b","entityType":"agent","canonicalPath":"/agent/copilotkit-copilotkit","slug":"copilotkit-copilotkit","name":"CopilotKit","description":"The Frontend for Agents & Generative UI. React + Angular","url":"https://github.com/CopilotKit/CopilotKit","homepage":"https://docs.copilotkit.ai","source":"GITHUB_REPOS","protocols":["OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-03-25T09:50:57.846Z","createdAt":"2026-02-25T03:39:14.617Z","downloads":null}],"links":{"hub":"/agent","source":"/agent/source/clawhub","protocols":[{"label":"OpenClaw","href":"/agent/protocol/openclew"}]}}}