From 50452504c109e773e709744230eae59690aa39e2 Mon Sep 17 00:00:00 2001 From: MCMartella Date: Tue, 5 May 2026 10:21:14 +0800 Subject: [PATCH 1/5] feat: trigger-driven skill metadata for improved activation Refactor SKILL.md descriptions across all domains to include 'MANDATORY' usage language and technical model names (e.g. sale.order, res.partner). This ensures the LLM recognizes the dependency between user requests and domain-specific expertise during the planning phase. --- skills/odoo-crm/SKILL.md | 2 +- skills/odoo-finance/SKILL.md | 2 +- skills/odoo-get-started/SKILL.md | 2 +- skills/odoo-helpdesk/SKILL.md | 2 +- skills/odoo-hr/SKILL.md | 2 +- skills/odoo-introspector/SKILL.md | 2 +- skills/odoo-inventory/SKILL.md | 2 +- skills/odoo-mrp/SKILL.md | 2 +- skills/odoo-products/SKILL.md | 2 +- skills/odoo-projects/SKILL.md | 2 +- skills/odoo-purchasing/SKILL.md | 2 +- skills/odoo-relations/SKILL.md | 2 +- skills/odoo-sales/SKILL.md | 2 +- skills/odoo-ux/SKILL.md | 2 +- 14 files changed, 14 insertions(+), 14 deletions(-) diff --git a/skills/odoo-crm/SKILL.md b/skills/odoo-crm/SKILL.md index d026a8d..e65ec37 100644 --- a/skills/odoo-crm/SKILL.md +++ b/skills/odoo-crm/SKILL.md @@ -1,6 +1,6 @@ --- name: odoo-crm -description: High-level functional expertise in Odoo's CRM engine, covering Lead qualification, Opportunity management, and Activity tracking. +description: MANDATORY for Leads and Opportunities (crm.lead). Expertise in Odoo's CRM pipeline, stages, and activity tracking. --- # Skill: Odoo CRM & Pipeline Management diff --git a/skills/odoo-finance/SKILL.md b/skills/odoo-finance/SKILL.md index 002aae1..4e57ec5 100644 --- a/skills/odoo-finance/SKILL.md +++ b/skills/odoo-finance/SKILL.md @@ -1,6 +1,6 @@ --- name: odoo-finance -description: High-level business expertise in Odoo's unified accounting engine, covering Invoicing, Billing, Banking, Consolidation, and Analytics. +description: MANDATORY for Customer Invoices (out_invoice), Vendor Bills (in_invoice), and Payments (account.payment). High-level expertise in Odoo's unified ledger and banking. --- # Skill: Odoo Finance & Accounting diff --git a/skills/odoo-get-started/SKILL.md b/skills/odoo-get-started/SKILL.md index c25663e..9d6d9cc 100644 --- a/skills/odoo-get-started/SKILL.md +++ b/skills/odoo-get-started/SKILL.md @@ -1,6 +1,6 @@ --- name: odoo-get-started -description: Orientation skill to establish context immediately upon connecting to an Odoo instance. +description: MANDATORY FIRST STEP. Orientation skill to establish the Odoo World Map context (Version, Apps, Company, Permissions) BEFORE executing any other tools. --- # Skill: Odoo Orientation (The Mandatory Start) diff --git a/skills/odoo-helpdesk/SKILL.md b/skills/odoo-helpdesk/SKILL.md index 8629f1c..839e34b 100644 --- a/skills/odoo-helpdesk/SKILL.md +++ b/skills/odoo-helpdesk/SKILL.md @@ -1,6 +1,6 @@ --- name: odoo-helpdesk -description: High-level functional expertise in Odoo's Customer Support engine, covering Ticket management, SLA compliance, and multi-channel integration. +description: MANDATORY for Tickets (helpdesk.ticket) and SLAs (helpdesk.sla). Expertise in Odoo's support engine and customer service. --- # Skill: Odoo Helpdesk & Customer Support diff --git a/skills/odoo-hr/SKILL.md b/skills/odoo-hr/SKILL.md index 77c81a5..832605a 100644 --- a/skills/odoo-hr/SKILL.md +++ b/skills/odoo-hr/SKILL.md @@ -1,6 +1,6 @@ --- name: odoo-hr -description: Functional expertise in Odoo's HR module, covering Employee management, Departmental structures, and Employment contracts. +description: MANDATORY for Employees (hr.employee) and Departments (hr.department). Expertise in Odoo's human resources and org structure. --- # Skill: Odoo Human Resources (HR) diff --git a/skills/odoo-introspector/SKILL.md b/skills/odoo-introspector/SKILL.md index e431687..2790afd 100644 --- a/skills/odoo-introspector/SKILL.md +++ b/skills/odoo-introspector/SKILL.md @@ -1,6 +1,6 @@ --- name: odoo-introspector -description: Expertise to examine, understand, and interpret Odoo's internal ORM structures and metadata. +description: MANDATORY for technical discovery. Expertise in Odoo's internal ORM structures, field types, and metadata interpretation. --- # Skill: Odoo Introspector diff --git a/skills/odoo-inventory/SKILL.md b/skills/odoo-inventory/SKILL.md index 3764aa3..8a63128 100644 --- a/skills/odoo-inventory/SKILL.md +++ b/skills/odoo-inventory/SKILL.md @@ -1,6 +1,6 @@ --- name: odoo-inventory -description: Expertise required to manage Odoo's inventory operations, covering transfers, stock moves, and physical warehouse structure. +description: MANDATORY for Transfers (stock.picking), Stock Moves (stock.move), and Locations (stock.location). Expertise in Odoo's logistics and warehouse structure. --- # Skill: Odoo Inventory & Logistics diff --git a/skills/odoo-mrp/SKILL.md b/skills/odoo-mrp/SKILL.md index 54f8977..5a1df48 100644 --- a/skills/odoo-mrp/SKILL.md +++ b/skills/odoo-mrp/SKILL.md @@ -1,6 +1,6 @@ --- name: odoo-mrp -description: High-level functional expertise in Odoo's Manufacturing and Product Lifecycle Management engines, covering production planning, execution, and engineering changes. +description: MANDATORY for Manufacturing Orders (mrp.production), BoMs (mrp.bom), and Work Orders (mrp.workorder). Expertise in Odoo's production engine. --- # Skill: Odoo MRP (Manufacturing) & PLM diff --git a/skills/odoo-products/SKILL.md b/skills/odoo-products/SKILL.md index 659498f..0f29cca 100644 --- a/skills/odoo-products/SKILL.md +++ b/skills/odoo-products/SKILL.md @@ -1,6 +1,6 @@ --- name: odoo-products -description: Expertise required to manage Odoo's product catalog, covering both general templates and specific product variants. +description: MANDATORY for Product Templates (product.template) and Variants (product.product). Expertise in Odoo's catalog and attribute management. --- # Skill: Odoo Product Management diff --git a/skills/odoo-projects/SKILL.md b/skills/odoo-projects/SKILL.md index 08a72f2..396b921 100644 --- a/skills/odoo-projects/SKILL.md +++ b/skills/odoo-projects/SKILL.md @@ -1,6 +1,6 @@ --- name: odoo-projects -description: High-level functional expertise in Odoo's Project Management engine, covering Tasks, Milestones, and integration with Sales and Accounting. +description: MANDATORY for Projects (project.project) and Tasks (project.task). Expertise in Odoo's project management and milestone tracking. --- # Skill: Odoo Projects, Milestones & Timesheets diff --git a/skills/odoo-purchasing/SKILL.md b/skills/odoo-purchasing/SKILL.md index 65f41eb..3dc167e 100644 --- a/skills/odoo-purchasing/SKILL.md +++ b/skills/odoo-purchasing/SKILL.md @@ -1,6 +1,6 @@ --- name: odoo-purchasing -description: High-level functional expertise in Odoo's Purchasing engine, covering RFQs, Purchase Orders, and Vendor Price management. +description: MANDATORY for RFQs and Purchase Orders (purchase.order). Expertise in Odoo's procurement engine and vendor pricing. --- # Skill: Odoo Purchasing & Procurement diff --git a/skills/odoo-relations/SKILL.md b/skills/odoo-relations/SKILL.md index 944af36..0aaae09 100644 --- a/skills/odoo-relations/SKILL.md +++ b/skills/odoo-relations/SKILL.md @@ -1,6 +1,6 @@ --- name: odoo-relations -description: Expertise required to manage the res.partner ecosystem, the foundational directory for all people and organizations in Odoo. +description: MANDATORY for Contacts, Customers, and Vendors (res.partner). Expertise in Odoo's central directory and relationship hierarchies. --- # Skill: Odoo Relations (Partners & Contacts) diff --git a/skills/odoo-sales/SKILL.md b/skills/odoo-sales/SKILL.md index 4814415..f5be94f 100644 --- a/skills/odoo-sales/SKILL.md +++ b/skills/odoo-sales/SKILL.md @@ -1,6 +1,6 @@ --- name: odoo-sales -description: Functional expertise in Odoo's Sales engine, covering the entire lifecycle from Quotation to Order Fulfillment. +description: MANDATORY for Sales Orders (sale.order), Quotations, and Order Fulfillment. Functional expertise in Odoo's Sales engine and lifecycle states. --- # Skill: Odoo Sales & Order Fulfillment diff --git a/skills/odoo-ux/SKILL.md b/skills/odoo-ux/SKILL.md index 0866d28..4e86777 100644 --- a/skills/odoo-ux/SKILL.md +++ b/skills/odoo-ux/SKILL.md @@ -1,6 +1,6 @@ --- name: odoo-ux -description: Expertise to navigate Odoo's user interface architecture and interpret frontend metadata. +description: MANDATORY for UI navigation. Expertise in Odoo's interface architecture (Menus, Actions, Views) and frontend metadata. --- # Skill: Odoo UX & Navigation From 3b06ad7f6d864632fab1f919e96aacb55cc5e5d8 Mon Sep 17 00:00:00 2001 From: MCMartella Date: Tue, 5 May 2026 12:42:32 +0800 Subject: [PATCH 2/5] fix: prevent HTML escaping in Odoo chatter Add 'body_is_html: true' to message_post calls in AuditService and update skill documentation to ensure HTML content is rendered correctly in the Odoo chatter. --- skills/odoo-dev/SKILL.md | 3 ++- skills/odoo-helpdesk/SKILL.md | 4 ++-- src/services/audit-service.ts | 1 + 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/skills/odoo-dev/SKILL.md b/skills/odoo-dev/SKILL.md index bd9afcd..5e7db30 100644 --- a/skills/odoo-dev/SKILL.md +++ b/skills/odoo-dev/SKILL.md @@ -1,6 +1,6 @@ --- name: odoo-dev -description: High-level technical expertise in Odoo's customization engine, covering Server Actions, Automated Rules, and Odoo Studio modifications. +description: MANDATORY for customizations. High-level technical expertise in Odoo's customization engine, covering Server Actions, Automated Rules, and Odoo Studio modifications. --- # Skill: Odoo Development & Customization @@ -21,6 +21,7 @@ Before troubleshooting or proposing changes, the agent must identify existing lo ### 2. Server Actions (`ir.actions.server`) - **Purpose:** Execute Python code, update records, or trigger multi-step workflows via the UI. - **Mandate:** When writing Python code in a Server Action, ensure all variables (`env`, `model`, `record`, `records`, `time`) are used correctly. Avoid long-running loops or operations that could cause database locks. +- **HTML Communication:** If using `message_post` within a Server Action to send HTML, remember to pass `body_is_html=True` to prevent escaping. ### 3. Automated Rules (`base.automation`) - **Purpose:** Automatically trigger Server Actions based on specific events. diff --git a/skills/odoo-helpdesk/SKILL.md b/skills/odoo-helpdesk/SKILL.md index 839e34b..c5f45c4 100644 --- a/skills/odoo-helpdesk/SKILL.md +++ b/skills/odoo-helpdesk/SKILL.md @@ -24,8 +24,8 @@ Helpdesk is the bridge between a customer problem and a technical solution: - **Sales/Invoicing:** Reference the original `sale.order_id` to verify warranty status or bill for support time via `sale_line_id`. ### 4. Communication Protocol -- **Public Reply:** Use `message_post` with `message_type: 'comment'` for messages sent directly to the customer via email/portal. -- **Internal Note:** Use `message_post` with `subtype_xmlid: 'mail.mt_note'` for internal technical updates. +- **Public Reply:** Use `message_post` with `message_type: 'comment'` and `body_is_html: true` (if sending HTML) for messages sent directly to the customer. +- **Internal Note:** Use `message_post` with `subtype_xmlid: 'mail.mt_note'` and `body_is_html: true` (if sending HTML) for internal technical updates. - **Mandate:** Clearly distinguish between the two. Never leak internal technical jargon to the customer. ## Available Resources diff --git a/src/services/audit-service.ts b/src/services/audit-service.ts index bd3368b..a874389 100644 --- a/src/services/audit-service.ts +++ b/src/services/audit-service.ts @@ -81,6 +81,7 @@ export class AuditService { body: `
🤖 AI Agent Action:
${body}
`, message_type: 'comment', subtype_xmlid: 'mail.mt_note', + body_is_html: true, }); } catch (error) { // Odoo models without 'mail.thread' inheritance will fail here. From d94f96a8839c197ce9c1ae93ff2d68699f4b96cc Mon Sep 17 00:00:00 2001 From: MCMartella Date: Tue, 5 May 2026 12:44:04 +0800 Subject: [PATCH 3/5] chore: release 1.3.3 - skill optimization and chatter fix --- gemini-extension.json | 4 ++-- package.json | 10 +++++++--- src/mcp-server.ts | 2 +- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/gemini-extension.json b/gemini-extension.json index 78fc2a3..4c5bef0 100644 --- a/gemini-extension.json +++ b/gemini-extension.json @@ -1,7 +1,7 @@ { "name": "brass-monkey", - "version": "1.3.2", - "description": "Secure, intelligent bridge between AI agents and Odoo instances.", + "version": "1.3.3", + "description": "A high-fidelity Gemini CLI extension and MCP bridge for Odoo ERP/CRM.", "main": "dist/index.js", "types": "dist/index.d.ts", "author": "Matthew Martella", diff --git a/package.json b/package.json index 812f4d7..f00961b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "brass-monkey", - "version": "1.3.2", + "version": "1.3.3", "type": "module", "main": "dist/index.js", "scripts": { @@ -13,14 +13,18 @@ "keywords": [ "odoo", "gemini", + "gemini-cli", + "cli", "extension", + "mcp", + "ai-agent", "xml-rpc", "erp", "crm" ], - "author": "", + "author": "Actinon", "license": "MIT", - "description": "Secure, intelligent bridge between AI agents and Odoo instances.", + "description": "A high-fidelity Gemini CLI extension and MCP bridge for Odoo ERP/CRM.", "dependencies": { "@modelcontextprotocol/sdk": "^1.29.0", "keytar": "^7.9.0", diff --git a/src/mcp-server.ts b/src/mcp-server.ts index f3b4aab..12cf5d8 100644 --- a/src/mcp-server.ts +++ b/src/mcp-server.ts @@ -19,7 +19,7 @@ import { fileURLToPath } from 'url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); // Read package.json for metadata -let version = "1.3.2"; +let version = "1.3.3"; try { const pkgPath = path.resolve(__dirname, "../package.json"); const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8")); From c5aca58f853ce4e46a3d59ef56eba1568ae75551 Mon Sep 17 00:00:00 2001 From: MCMartella Date: Tue, 5 May 2026 12:58:37 +0800 Subject: [PATCH 4/5] feat: automatic HTML detection for Odoo message_post Implement a core interceptor in OdooClient to automatically detect HTML content and inject 'body_is_html: true'. Includes regression tests to verify injection for HTML and preservation of plain text. --- src/services/odoo-client.ts | 8 ++++++++ tests/odoo-client.test.ts | 40 +++++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/src/services/odoo-client.ts b/src/services/odoo-client.ts index 9507b6c..5b740cb 100644 --- a/src/services/odoo-client.ts +++ b/src/services/odoo-client.ts @@ -120,6 +120,14 @@ export class OdooClient { const { db, api_key } = this.config; + // Safety Interceptor: Auto-detect HTML in message_post calls + if (method === 'message_post' && kwargs && typeof kwargs.body === 'string') { + const containsHtml = /<[a-z][\s\S]*>/i.test(kwargs.body); + if (containsHtml && kwargs.body_is_html === undefined) { + kwargs.body_is_html = true; + } + } + return new Promise((resolve, reject) => { this.objectClient.methodCall( 'execute_kw', diff --git a/tests/odoo-client.test.ts b/tests/odoo-client.test.ts index 1fa1890..dbe4af6 100644 --- a/tests/odoo-client.test.ts +++ b/tests/odoo-client.test.ts @@ -85,4 +85,44 @@ describe('OdooClient', () => { expect.any(Function) ); }); + + it('should automatically inject body_is_html for message_post with HTML', async () => { + const commonClient = (xmlrpc.createSecureClient as any).mock.results[0].value; + const objectClient = (xmlrpc.createSecureClient as any).mock.results[1].value; + + commonClient.methodCall + .mockImplementationOnce((method, params, callback) => callback(null, { server_version: '15.0' })) + .mockImplementationOnce((method, params, callback) => callback(null, 1)); + + objectClient.methodCall.mockImplementation((method, params, callback) => callback(null, true)); + + const htmlBody = '
Test
'; + await client.executeKw('res.partner', 'message_post', [1], { body: htmlBody }); + + expect(objectClient.methodCall).toHaveBeenCalledWith( + 'execute_kw', + ['test-db', 1, 'password', 'res.partner', 'message_post', [1], { body: htmlBody, body_is_html: true }], + expect.any(Function) + ); + }); + + it('should NOT inject body_is_html for plain text message_post', async () => { + const commonClient = (xmlrpc.createSecureClient as any).mock.results[0].value; + const objectClient = (xmlrpc.createSecureClient as any).mock.results[1].value; + + commonClient.methodCall + .mockImplementationOnce((method, params, callback) => callback(null, { server_version: '15.0' })) + .mockImplementationOnce((method, params, callback) => callback(null, 1)); + + objectClient.methodCall.mockImplementation((method, params, callback) => callback(null, true)); + + const plainBody = 'Just a plain text message'; + await client.executeKw('res.partner', 'message_post', [1], { body: plainBody }); + + expect(objectClient.methodCall).toHaveBeenCalledWith( + 'execute_kw', + ['test-db', 1, 'password', 'res.partner', 'message_post', [1], { body: plainBody }], + expect.any(Function) + ); + }); }); From a3b59275e372502553288a75aed11c4920ca5cb2 Mon Sep 17 00:00:00 2001 From: MCMartella Date: Tue, 5 May 2026 13:39:43 +0800 Subject: [PATCH 5/5] feat: multi-company context injection and diagnostic transparency 1. Automatically fetch and inject allowed_company_ids into RPC context for cross-company visibility.\n2. Expose ValueError and KeyError in formatError to enable agent self-correction for missing fields.\n3. Update odoo-data-ops skill with schema strictness recovery guidance. --- skills/odoo-data-ops/SKILL.md | 8 +++- src/services/odoo-client.ts | 26 ++++++++++- tests/odoo-client.test.ts | 84 +++++++++++++++++++++++++++++++++++ 3 files changed, 115 insertions(+), 3 deletions(-) diff --git a/skills/odoo-data-ops/SKILL.md b/skills/odoo-data-ops/SKILL.md index 68b970b..c8946a9 100644 --- a/skills/odoo-data-ops/SKILL.md +++ b/skills/odoo-data-ops/SKILL.md @@ -29,7 +29,13 @@ Every CRUD tool supports an `instance_alias` parameter. - **Field Categorization:** By default, `search_read` only returns "Base" fields to save context. If you need more, use `include_extended: true` (for extra modules) or `include_computed: true` (for calculated fields). - **Aggregations:** Use `aggregate_records` for BI-style queries (grouping, counting, summing). This is much more context-efficient than reading thousands of records to perform local math. -### 5. Agent-Driven Undo Workflow +### 5. Schema Strictness & Error Recovery +Odoo v18+ is strict about field lists in `search_read`. +- **The Trigger:** If you receive a `ValueError` or `KeyError` stating a field does not exist. +- **The Mandate:** You must STOP and call `inspect_model` to verify the current live schema. Do NOT guess field names. +- **Action:** After finding the correct field, retry the operation with the updated field list. + +### 6. Agent-Driven Undo Workflow If you make a mistake or are asked to "undo" a change: 1. **Locate:** Use `search_read` on the `mail.message` model for the target record to find the "Before Snapshot" you previously posted. 2. **Analyze:** Verify the current record state. Do not attempt a rollback if it violates Odoo's business logic (e.g., trying to revert an invoice that has since been paid). diff --git a/src/services/odoo-client.ts b/src/services/odoo-client.ts index 5b740cb..924e358 100644 --- a/src/services/odoo-client.ts +++ b/src/services/odoo-client.ts @@ -10,6 +10,7 @@ export class OdooClient { private objectClient: any; private uid: number | null = null; private versionInfo: any = null; + private companyIds: number[] = []; constructor(private config: OdooConfig) { const commonUrl = new URL('/xmlrpc/2/common', config.url).toString(); @@ -62,7 +63,18 @@ export class OdooClient { } this.uid = uid as number; - resolve(this.uid); + + // Fetch allowed companies for cross-company visibility + this.objectClient.methodCall( + 'execute_kw', + [db, this.uid, api_key, 'res.users', 'read', [[this.uid]], { fields: ['company_ids'] }], + (companyError: any, userRecords: any) => { + if (!companyError && userRecords && userRecords.length > 0) { + this.companyIds = userRecords[0].company_ids || []; + } + resolve(this.uid as number); + } + ); } ); } @@ -128,6 +140,14 @@ export class OdooClient { } } + // Context Injection: Enable cross-company visibility by default + if (this.companyIds.length > 0) { + kwargs.context = kwargs.context || {}; + if (kwargs.context.allowed_company_ids === undefined) { + kwargs.context.allowed_company_ids = this.companyIds; + } + } + return new Promise((resolve, reject) => { this.objectClient.methodCall( 'execute_kw', @@ -153,7 +173,9 @@ export class OdooClient { /odoo\.exceptions\.UserError: (.*)/, /odoo\.exceptions\.ValidationError: (.*)/, /odoo\.exceptions\.AccessError: (.*)/, - /odoo\.exceptions\.MissingError: (.*)/ + /odoo\.exceptions\.MissingError: (.*)/, + /ValueError: (.*)/, + /KeyError: (.*)/ ]; for (const pattern of businessErrors) { diff --git a/tests/odoo-client.test.ts b/tests/odoo-client.test.ts index dbe4af6..121d809 100644 --- a/tests/odoo-client.test.ts +++ b/tests/odoo-client.test.ts @@ -27,6 +27,7 @@ describe('OdooClient', () => { it('should authenticate successfully', async () => { const commonClient = (xmlrpc.createSecureClient as any).mock.results[0].value; + const objectClient = (xmlrpc.createSecureClient as any).mock.results[1].value; commonClient.methodCall .mockImplementationOnce((method, params, callback) => { @@ -36,6 +37,10 @@ describe('OdooClient', () => { callback(null, 1); // UID response }); + objectClient.methodCall.mockImplementationOnce((method, params, callback) => { + callback(null, [{ company_ids: [1] }]); // res.users read + }); + const uid = await client.authenticate(); expect(uid).toBe(1); expect(client.majorVersion).toBe(15); @@ -45,6 +50,11 @@ describe('OdooClient', () => { ['test-db', 'admin', 'password', {}], expect.any(Function) ); + expect(objectClient.methodCall).toHaveBeenCalledWith( + 'execute_kw', + ['test-db', 1, 'password', 'res.users', 'read', [[1]], { fields: ['company_ids'] }], + expect.any(Function) + ); }); it('should throw error on authentication failure', async () => { @@ -125,4 +135,78 @@ describe('OdooClient', () => { expect.any(Function) ); }); + + it('should inject allowed_company_ids into context if companyIds are available', async () => { + const commonClient = (xmlrpc.createSecureClient as any).mock.results[0].value; + const objectClient = (xmlrpc.createSecureClient as any).mock.results[1].value; + + commonClient.methodCall + .mockImplementationOnce((method, params, callback) => callback(null, { server_version: '15.0' })) + .mockImplementationOnce((method, params, callback) => callback(null, 1)); + + // Mock the user read to return company IDs + objectClient.methodCall.mockImplementationOnce((method, params, callback) => { + callback(null, [{ company_ids: [1, 26] }]); + }); + + await client.authenticate(); + + // Now call executeKw and check context + objectClient.methodCall.mockImplementationOnce((method, params, callback) => callback(null, [])); + await client.executeKw('res.partner', 'search', [[]]); + + expect(objectClient.methodCall).toHaveBeenCalledWith( + 'execute_kw', + ['test-db', 1, 'password', 'res.partner', 'search', [[]], { context: { allowed_company_ids: [1, 26] } }], + expect.any(Function) + ); + }); + + it('should preserve existing context when injecting allowed_company_ids', async () => { + const commonClient = (xmlrpc.createSecureClient as any).mock.results[0].value; + const objectClient = (xmlrpc.createSecureClient as any).mock.results[1].value; + + commonClient.methodCall + .mockImplementationOnce((method, params, callback) => callback(null, { server_version: '15.0' })) + .mockImplementationOnce((method, params, callback) => callback(null, 1)); + + objectClient.methodCall.mockImplementationOnce((method, params, callback) => { + callback(null, [{ company_ids: [1, 26] }]); + }); + + await client.authenticate(); + + objectClient.methodCall.mockImplementationOnce((method, params, callback) => callback(null, [])); + await client.executeKw('res.partner', 'search', [[]], { context: { lang: 'en_US' } }); + + expect(objectClient.methodCall).toHaveBeenCalledWith( + 'execute_kw', + ['test-db', 1, 'password', 'res.partner', 'search', [[]], { context: { lang: 'en_US', allowed_company_ids: [1, 26] } }], + expect.any(Function) + ); + }); + + it('should NOT overwrite allowed_company_ids if explicitly provided', async () => { + const commonClient = (xmlrpc.createSecureClient as any).mock.results[0].value; + const objectClient = (xmlrpc.createSecureClient as any).mock.results[1].value; + + commonClient.methodCall + .mockImplementationOnce((method, params, callback) => callback(null, { server_version: '15.0' })) + .mockImplementationOnce((method, params, callback) => callback(null, 1)); + + objectClient.methodCall.mockImplementationOnce((method, params, callback) => { + callback(null, [{ company_ids: [1, 26] }]); + }); + + await client.authenticate(); + + objectClient.methodCall.mockImplementationOnce((method, params, callback) => callback(null, [])); + await client.executeKw('res.partner', 'search', [[]], { context: { allowed_company_ids: [26] } }); + + expect(objectClient.methodCall).toHaveBeenCalledWith( + 'execute_kw', + ['test-db', 1, 'password', 'res.partner', 'search', [[]], { context: { allowed_company_ids: [26] } }], + expect.any(Function) + ); + }); });