| name | ipai-platform |
| description | Master skill bundle for InsightPulseAI. Covers Azure resource constants (ipai-resource-map), Odoo 18 CE development conventions (ipai-odoo-platform), Pulser agent platform patterns (ipai-agent-platform), and Odoo docs enhancement. Load for any IPAI Azure, Odoo, Foundry, or agent work. Triggers on: IPAI, InsightPulseAI, Pulser, Odoo 18 CE, ipai_*, BIR compliance, ACA deployment, Foundry, rg-ipai, pg-ipai-odoo, ipai-copilot-resource.
|
| version | 0.8.0 |
| updated | 2026-04-18 |
| scope | repo |
| parts | ["index","ipai-resource-map","ipai-odoo-platform","ipai-agent-platform","odoo-docs-enhance"] |
Part 1: Index
name: pulser-constitution
description: Establish or update Pulser for Odoo governing principles. Use when starting a new feature, onboarding a new contributor, or when IPAI doctrine needs to be re-anchored. Triggers on "constitution", "governing principles", "architecture doctrine", or "platform rules".
disable-model-invocation: false
user-invocable: true
Pulser for Odoo — constitution enforcement
You are anchoring all work to the Pulser for Odoo governing principles.
Read spec/pulser-odoo/constitution.md if it exists. If it does not exist, create it from the template below.
Then confirm all active work items are consistent with these principles.
Non-negotiables to enforce
Naming
- Product name: Pulser for Odoo
- Technical addon:
ipai_odoo_copilot
- Spec slug:
pulser-odoo
- Tenant terminology: a Pulser tenant is a customer organization — never an Odoo company, branch, or Entra tenant
System-of-action rule
Odoo CE/OCA 18 is the system of action. Pulser does not replace Odoo transactional truth.
Pulser may: assist, route, summarize, validate, prepare, generate artifacts, enforce policy gates.
Pulser may not bypass: Odoo record truth, RBAC, approval banding, evidence controls, mutation safety.
Ingress rule
Canonical public ingress:
- Azure DNS
- Direct custom-domain binding to Azure Container Apps origins
- Certificate binding at the app edge
Not canonical: Cloudflare, proxy-based ingress, Azure Front Door as main ingress layer.
Front Door may exist only as a legacy or migration artifact.
Runtime topology rule
Pulser core runtime converges to three apps only:
pulser-odoo-web
pulser-agent-api
pulser-worker
Additional apps require written justification (OCR, MCP bridge, public website portfolio).
ERP must remain separate from public website runtime.
Deployment stamp rule
Pulser scales through deployment stamps. Each stamp:
- Is independently deployable and recoverable
- Has a dedicated Azure Database for PostgreSQL Flexible Server (no shared PG across stamps)
- Is bounded in blast radius
IaC rule
All infrastructure converges into infra/azure/ as Bicep modules.
No critical infrastructure left as manual Azure-only state.
Repo authority chain
Azure Boards → Spec Kit → GitHub → IaC/CI-CD → Azure runtime → observability/evals → feedback
RBAC rule
Pulser behavior is resolved from: surface × domain × role groups × approval band × evidence scope × expertise mode × task type × risk level.
Never infer RBAC from contact lists, email directories, or ad hoc naming conventions.
Self-improvement bounds
Allowed: structured run tracing, domain evaluation, replay-based optimization, narrow small-model training.
Prohibited: uncontrolled online RL for finance, tax, approval, or operational mutation workflows.
Template for spec/pulser-odoo/constitution.md
If the file does not exist, write it verbatim from $CLAUDE_SKILL_DIR/templates/constitution-template.md.
Verification checklist
After creating or updating the constitution, confirm:
Part 2: IPAI Resource Map (Azure constants)
name: ipai-resource-map
description: >
InsightPulseAI Azure resource constants and topology. Load whenever working
with IPAI infrastructure, Odoo deployments, Foundry, agents, or any Azure
resource. Provides exact subscription IDs, resource names, resource group
topology, MI names, and IPAI-specific conventions — eliminating lookup
round-trips and preventing wrong-version or wrong-resource errors.
ALWAYS load this skill for any IPAI Azure, Odoo, Foundry, or agent work.
Pairs with official azure-container-apps, microsoft-foundry, and
azure-database-for-postgresql skills for implementation patterns.
IPAI Azure Resource Map
Canonical reference for all InsightPulseAI Azure resources.
Last updated: 2026-04-13. Source: Azure portal CSV export (116 resources).
Subscriptions
| Name | ID | Purpose |
|---|
| IPAI ISV Sponsored (canonical) | eba824fb-332d-4623-9dfb-2c9f7ee83f4e | All workloads (dev/staging/prod) |
Entra tenant: 402de71a-87ec-4302-a609-fb76098d1da7 (insightpulseai.com)
Default region: Southeast Asia (southeastasia)
Foundry region: East US 2 (eastus2) — Foundry resource must be EUS2
Resource Group Topology
rg-ipai-dev-odoo-runtime (SEA) — PRIMARY runtime RG
ACA environment, all Container Apps/Jobs, ACR, AFD, WAF,
alerts, Redis, DNS zones, private endpoints, NSGs, VNet,
Log Analytics, App Insights, Recovery Services vault,
Function App, workbook, action groups
rg-ipai-dev-odoo-data (SEA) — Data tier
pg-ipai-odoo (PG Flex), stipaiodoodev (storage),
private endpoint for PG
rg-ipai-dev-platform (SEA) — Identity + secrets
kv-ipai-dev-sea (canonical KV)
kv-ipai-dev (STALE — consolidate and delete)
id-ipai-agent-* (6 per-agent managed identities)
rg-data-intel-ph (EUS2) — AI/data stack
ipai-copilot-resource (Foundry)
ipai-copilot project
cosmos-ipai-dev (Cosmos DB NoSQL, serverless)
srch-ipai-dev (AI Search, Basic)
stipaiagentdev (Storage, ZRS — DEDICATED Foundry Agent Service)
docai-ipai-dev (Document Intelligence)
bing-ipai-grounding (Bing Resource)
rg-ipai-financial-intel (EUS2) — Financial intel
kv-admin845384060711840
stadmin8456a384060711840
rg-ipai-stg-odoo-runtime (SEA) — sponsored sub eba824fb (PrismaLab prod home + staging lane)
la-ipai-stg (Log Analytics)
ipai-odoo-stg-env (ACA env, domain whitedesert-54fce6ca)
ipai-prismalab-web (serves prismalab.insightpulseai.com — moved here from prod sub 2026-04-13)
id-ipai-stg (UAMI for ACA pulls; cross-sub AcrPull on acripaiodoo + Cognitive Services User on ipai-copilot-resource)
rg-ipai-stg-odoo-data (SEA) — sponsored sub eba824fb — empty (PG Flex deferred)
Sponsored-sub legacy/parallel stack (NOT touched, separate work):
rg-ipai-dev-data-sea (SEA) — stdevipai (containers: bir-inbox, odoo-attachments) · stlkipaidev (lakehouse, empty bronze/silver/gold) · pg-ipai-odoo-dev (PG Flex)
rg-ipai-dev-mon-sea (SEA) — log-ipai-dev-sea · appi-ipai-dev
rg-ipai-dev-security-sea (SEA) — id-ipai-dev (DIFFERENT MI from prod — same name, different principalId) · kv-ipai-dev-sea
rg-ipai-dev-odoo-sea (SEA) — sb-ipai-dev-sea (Service Bus) · acae-ipai-dev-sea (ACA env, EMPTY)
rg-ipai-dev-ai-sea (SEA) — dbw-ipai-dev (Databricks)
rg-ipai-dev-dbw-managed (SEA) — Databricks-managed (unity-catalog-access-connector, dbmanagedidentity, dbstorage*, workers-vnet, workers-sg)
Deleted this session (zombies + duplicates): aif-ipai-dev (empty Foundry shell, eus2) · srch-dev-ipai (wrong-named empty Search) · stlkdevipai (wrong-named empty lakehouse) · ipai-prismalab-stg-web (duplicate canary) · rg-ipai-dev-ai-eus2 (RG was holding only aif-ipai-dev).
Container Apps (23 running)
Odoo core (SOR):
ipai-odoo-dev-web — Odoo web tier
ipai-odoo-dev-cron — Odoo cron worker
ipai-odoo-dev-worker — Odoo queue worker
- ACA environment:
ipai-odoo-dev-env-v2
AI/Agent tier:
ipai-copilot-gateway — Foundry proxy / Pulser gateway
ipai-odoo-mcp — Odoo MCP Server (13 tools, FastMCP StreamableHTTP) ✅ LIVE
ipai-release-manager — MAF Release Manager agent ✅ LIVE
ipai-bot-proxy-dev — Bot Framework webhook proxy
ipai-ocr-dev — Document Intelligence OCR
Platform services:
ipai-mcp-dev — General MCP server (→ rename: ipai-pg-mcp-server is separate)
ipai-pg-mcp-server-q7d3v77xqx — PostgreSQL MCP server (own env)
ipai-odoo-connector — Odoo integration connector
Dev tools:
ipai-code-server-dev — VS Code server
ipai-grafana-dev — Grafana
ipai-mailpit-dev — Mail testing
Portals/websites:
ipai-login-dev, ipai-ops-dashboard, ipai-website-dev
ipai-workload-center, ipai-w9studio-dev, w9studio-landing-dev
Research:
ipai-prismalab-dev, ipai-prismalab-gateway
Evaluate for retirement:
ipai-superset-dev — Superset deprecated; replace with Databricks/Fabric
Container App Jobs
KEEP:
ipai-build-agent — CI/CD build agent
oca-audit-job — OCA module audit
oca-full-audit — Full OCA audit
REVIEW before delete:
set-auth-mode — keyless auth migration job (verify completed)
clear-keys-job — key clearing job (verify completed)
DELETE (9 stale cleanup jobs):
for job in asset-deep-fix asset-fix-job oauth-diag-job oauth-fix-job \
oauth-signup-fix oauth-verify-job url-fix-job \
pg-mcp-grant pg-mcp-entra-grant; do
az containerapp job delete -n $job \
-g rg-ipai-dev-odoo-runtime --yes --no-wait
done
Key Resources (exact names)
| Resource | Name | RG | Region |
|---|
| Container registry | acripaiodoo | runtime | SEA |
| Front Door | afd-ipai-dev | runtime | Global |
| WAF policy | wafipaidev | runtime | Global |
| Redis | cache-ipai-dev | runtime | SEA |
| PG Flex | pg-ipai-odoo | data | SEA |
| PG host | pg-ipai-odoo.postgres.database.azure.com | — | — |
| KV (canonical) | kv-ipai-dev-sea | platform | SEA |
| KV (stale) | kv-ipai-dev | platform | SEA |
| Platform MI | id-ipai-dev | runtime | SEA |
| App Insights | appi-ipai-dev | runtime | SEA |
| Log Analytics | la-ipai-odoo-dev | runtime | SEA |
| VNet | vnet-ipai-dev | runtime | SEA |
| Foundry resource | ipai-copilot-resource | rg-data-intel-ph | EUS2 |
| Foundry project | ipai-copilot | rg-data-intel-ph | EUS2 |
| Foundry endpoint | https://ipai-copilot-resource.services.ai.azure.com/api/projects/ipai-copilot | — | — |
| Cosmos DB | cosmos-ipai-dev | rg-data-intel-ph | EUS2 |
| AI Search | srch-ipai-dev | rg-data-intel-ph | SEA (verified 2026-04-13; sponsored-sub srch-dev-ipai was a wrong-named duplicate, deleted) |
| Agent storage | stipaiagentdev | rg-data-intel-ph | EUS2 |
| App storage | stipaidev | runtime | SEA |
| Document Intel | docai-ipai-dev | rg-data-intel-ph | EUS2 |
| DNS zone | insightpulseai.com | runtime | Global |
| ADO org | insightpulseai | — | SEA |
*srch-ipai-dev shows SEA — verify should be EUS2 to match Foundry
Managed Identities
| Name | RG | Purpose |
|---|
id-ipai-dev | runtime | Platform MI — all ACA apps use this |
id-ipai-agent-ap-invoice-dev | platform | AP Invoice agent |
id-ipai-agent-bank-recon-dev | platform | Bank Reconciliation agent |
id-ipai-agent-doc-intel-dev | platform | Document Intelligence agent |
id-ipai-agent-finance-close-dev | platform | Finance Close agent |
id-ipai-agent-pulser-dev | platform | Core Pulser agent |
id-ipai-agent-tax-guru-dev | platform | Tax Guru agent |
Auth pattern (always): DefaultAzureCredential — never API keys in code.
Azure Bots (6 registered — Teams surfaces)
All in rg-ipai-dev-odoo-runtime, routed via ipai-bot-proxy-dev:
ipai-ap-invoice-teams-bot-dev
ipai-bank-recon-teams-bot-dev
ipai-doc-intel-teams-bot-dev
ipai-finance-close-teams-bot-dev
ipai-pulser-teams-bot-dev
ipai-tax-guru-teams-bot-dev
Key Vault — Canonical Reference
Use kv-ipai-dev-sea for all secrets.
kv-ipai-dev is a duplicate from a naming collision — consolidate and delete.
az keyvault show -n kv-ipai-dev-sea -g rg-ipai-dev-platform --query id -o tsv
Microsoft Foundry — Naming (Feb 2026)
Rebranded from "Azure AI Foundry" → "Microsoft Foundry" (Feb 2026)
Resource type: Microsoft.CognitiveServices/account kind AIServices
Portal: https://ai.azure.com (toggle "New Foundry" ON)
Agent API: Responses API (v2) — not Assistants API (v1)
SDK: azure-ai-projects>=2.0.0 (unified — replaces azure-ai-inference, azure-ai-ml)
Terminology: Threads→Conversations, Messages→Items, Runs→Responses
Odoo Stack Constants
Odoo version: 18 CE (NEVER 19 — do not apply v19 patterns)
Databases: odoo (prod) | odoo_staging | odoo_dev
PG host: pg-ipai-odoo.postgres.database.azure.com
RG (data): rg-ipai-dev-odoo-data
RG (runtime): rg-ipai-dev-odoo-runtime
View tag: <list> ONLY — never <tree> (deprecated in 18, error in 19)
view_mode: "list,form"
OCA path: oca_addons/ (vendored, pinned)
Custom path: addons/ipai/
Module prefix: ipai_
Smart Delta order: config → OCA → ipai_*_delta → ipai_*_core
Monitoring Stack
Alerts (metric): 13 rules — ACA CPU/restarts/replicas, PG CPU, HTTP 5xx
Alerts (log search): 3 rules — agent latency P95, agent error rate, content safety blocks
Action groups: ag-ipai-ops-email, ag-ipai-platform
Workbook: Odoo Platform Health (in rg-ipai-dev-odoo-runtime)
Missing alerts to add after Week 1 deploy:
- Cosmos DB RU consumption threshold
- AI Search query latency
- Foundry token usage
ISV + Partner Program
Partner ID: 7097326
Entity: Dataverse IT Consultancy, PH, Makati
Program: ISV Success (enrolled 3/31/2026 → 3/31/2027)
Sponsored sub: eba824fb (IPAI ISV Sponsored — staging)
Benefits active:
Azure sponsorship $5K → activate at Partner Center → Benefits
M365 E5 developer (25) → activate at Partner Center → Cloud services
D365 Sales/FS/CS (FREE) → https://experience.dynamics.com/requestlicense/
D365 Operations Tier 2 → https://experience.dynamics.com/requestlicense/
AI technical consultation → Partner Center → Benefits → Consultations
ADS session → Partner Center → Benefits → Consultations
GitHub Enterprise Cloud → Partner Center → Benefits (year 1 only)
Renewal deadline: 3/31/2027
Renewal requirement: publish Transactable SaaS offer OR upgrade Contact Me → Transactable
Renewal fee: $1,550 USD
Critical Security Findings (BLOCKER for prod)
- BOLA —
copilot_gateway.py doesn't verify user owns thread_id
- PgBouncer — not enabled on
pg-ipai-odoo (enable: pgbouncer.enabled=on)
- Cosmos single-region — no failover for agent conversation state
- Defender for AI Services — not enabled on
ipai-copilot-resource
- WAF no prompt exclusions — chat content triggers HTTP 403
- Foundry system MI was OFF — enable:
az cognitiveservices account update -n ipai-copilot-resource -g rg-data-intel-ph --assign-identity
- API keys active — disable:
disableLocalAuth: true on Foundry resource
Azure DevOps MCP — Connection Strings
Remote MCP Server (public preview — VS Code only now)
URL: https://mcp.dev.azure.com/insightpulseai
Auth: Microsoft Entra ID (OAuth)
Toolsets: repos, wit, pipelines, wiki, work, testplan, search
Insiders: X-MCP-Insiders: "true" header
Status: Claude Code/Desktop NOT supported (pending OAuth dynamic registration)
Local MCP Server (use for Claude / Pulser)
Package: @microsoft/azure-devops-mcp
Secret: kv-ipai-dev-sea/ado-pat-ipai-platform (PAT)
Org: insightpulseai
Project: ipai-platform
Repo: InsightPulseAI/odoo
Build agent: ipai-build-agent (ACA Job, rg-ipai-dev-odoo-runtime)
azd agent commands (March 2026, v1.23.x)
azd ai agent run --name <agent>
azd ai agent invoke --name <agent>
azd ai agent show --name <agent>
azd ai agent monitor --name <agent>
App Insights connection string (for ACA OTel)
Resource: appi-ipai-dev
Secret: kv-ipai-dev-sea/appi-ipai-dev-connection-string
Env var on ACA: APPLICATIONINSIGHTS_CONNECTION_STRING
Azure MCP Server (official Microsoft, not ADO-specific)
Project: github.com/orgs/microsoft/projects/1976
Repo: monitor for azure resource management MCP tools
-e
Part 3: IPAI Odoo Platform (Odoo 18 CE conventions)
name: ipai-odoo-platform
description: >
InsightPulseAI Odoo 18 CE platform — module development, Extend-First
framework, OCA-first hierarchy, view/widget authoring, BIR tax compliance,
ACA deployment, contributing conventions, and service module patterns.
Single authoritative reference for ALL Odoo work in the IPAI stack.
Replaces: odoo18-ce-development, odoo19-developer, odoo19-oca-first,
odoo19-views-widgets, odoo19-contributing, odoo19-administration,
odoo19-services (all Odoo 19 skills are WRONG VERSION for IPAI).
ALWAYS load for any Odoo module dev, view XML, BIR form, ACA Odoo deploy,
or OCA module evaluation. Pairs with ipai-resource-map for Azure constants.
IPAI Odoo 18 CE Platform
VERSION: Odoo 18 CE — NEVER apply Odoo 19 patterns.
Critical 18 CE facts:
- Use
<list> tag — never <tree> (deprecated in 18, causes warnings)
view_mode="list,form" not "tree,form"
_cr, _context, _uid still work in 18 (deprecated in 19, NOT yet)
osv module still exists in 18 (removed in 19, NOT yet)
- OCA modules: target
18.0 branch, not 16.0 or 17.0
§1 — Development Philosophy: Extend-First
Odoo 18 CE + OCA is already a working ERP. The default assumption is that
the capability exists somewhere in CE or OCA — your job is to find it,
extend it, or repurpose an adjacent module. Writing a new ipai_* module
is always the last option, not the first instinct.
The bias to defeat: reaching for a new module when _inherit on an
existing model or a reconfigured adjacent OCA module would do the same job
with less code and better upgrade safety.
Decision Hierarchy (follow in order, stop at first YES)
1. CONFIGURE
Can CE settings, approval flows, sequences, automated actions,
mail templates, or record rules solve it?
→ YES: configure in the UI or via data XML. Zero Python. Stop.
Examples:
- Period-end locking → account.journal.lock_date (CE setting)
- Approval workflow → base_automation + mail.activity rules
- Sequence numbering → ir.sequence configuration
- Access control → record rules on existing model
2. EXTEND via _inherit
Does a CE or OCA model already own this data/logic — even partially?
→ YES: add _inherit in a thin module. Add fields, override one method,
extend the view with xpath. No forking. No copy-paste of base code.
Examples:
- BIR fields on invoice → _inherit account.move, add 3 fields + 1 compute
- Withholding on partner → _inherit res.partner, add wtax_type selection
- Custom approval state → _inherit account.move, extend state selection
3. REPURPOSE an adjacent module
Is there a CE or OCA module for a neighboring domain that can be
reconfigured or lightly extended (_inherit) to serve this purpose?
→ YES: use it. Rename via _description, remap menus, extend views.
Do not rebuild what already exists in a slightly different form.
Examples:
- BIR compliance tracking → repurpose project.task with custom stages
- Expense liquidation → extend hr.expense before building new
- Document filing workflow → repurpose mail.activity + attachments
- Vendor accreditation → extend res.partner with accreditation fields
4. COMPOSE
Can automated actions, server actions, computed fields, scheduled
actions, or OCA queue_job wire existing modules together to
produce the desired workflow?
→ YES: compose. No new model, no new module — just wiring.
Examples:
- BIR deadline reminder → ir.cron + mail.template on account.move
- Month-end close checklist → server action sequence on account.period
- Multi-step approval → base_automation chain on state field
5. NEW MODULE (last resort only)
Genuinely missing domain — no existing CE/OCA model can be
extended or repurposed without becoming unrecognizable.
→ Write ipai_<domain>_core. Must pass: "would _inherit on an existing
model solve ≥60% of this?" If yes, go back to step 2.
Legitimate new modules (no CE/OCA equivalent):
- ipai_bir_tax_compliance → BIR-specific form generation + XML export
- ipai_agency_seed → client-specific seed data loader
- ipai_dev_studio_base → foundation layer (depends on nothing)
What this prevents
❌ ipai_finance_close as a new module
→ account.period + lock_date + an automated action = done
❌ ipai_expense_portal rebuilt from scratch
→ hr.expense + _inherit + custom stages = done
❌ ipai_bir_task as a new task model
→ project.task with BIR-specific stages and _inherited fields = done
❌ ipai_vendor_accreditation as a new model
→ res.partner + _inherit + accreditation selection + portal = done
✅ ipai_bir_tax_compliance as a new module
→ BIR 2307 XML generation, SLSP/SAWT file format, QR stamping —
nothing in CE/OCA produces Philippine BIR output files
✅ ipai_agency_seed as a new module
→ client-specific demo/seed data has no CE/OCA home
Hard Guardrails
| Guardrail | Rule |
|---|
| No Enterprise | Never import EE modules or IAP. Enforce iap.disabled=True |
| No micro-modules | Never create a module whose entire purpose fits in one _inherit |
| OCA vendoring | Pin OCA modules in oca_addons/ at exact commit hash |
| No runtime git/pip | All dependencies resolved at build time, not runtime |
| Upgrade safety | _inherit beats fork — every fork is a future upgrade liability |
| AI-first foundation | ipai_* modules depend on ipai_dev_studio_base |
Module structure
addons/ipai/
├── ipai_dev_studio_base/ # Foundation — all ipai_* modules depend on this
├── ipai_ce_branding/ # CE hardening, EE lock-out
├── ipai_bir_tax_compliance/ # BIR 2307/SLSP/SAWT — genuinely new domain
├── ipai_agency_seed/ # Client seed data loader
└── (new modules only when steps 1-4 fail)
oca_addons/ # Vendored OCA 18.0 modules (pinned)
├── web_responsive/
├── auditlog/
├── account_financial_report/
├── account_reconcile_oca/
├── account_tax_balance/
├── queue_job/
└── report_xlsx/
Commit tags
[extend] feat(account): inherit account.move, add BIR 2307 fields
[repurpose] feat(compliance): repurpose project.task for BIR deadline tracking
[compose] feat(close): wire month-end checklist via ir.cron + server actions
[configure] feat(approval): configure base_automation for 3-level expense approval
[new-module] feat(bir): ipai_bir_tax_compliance — BIR XML export engine
§2 — OCA-First Module Selection
Priority OCA modules for IPAI (18.0 branch)
Accounting (R2R/BIR):
account_financial_report — balance sheet, P&L, trial balance
account_reconcile_oca — bank reconciliation UI
account_tax_balance — tax balance reports
account_journal_lock_date — journal locking for period close
account_invoice_import — invoice import from PDF/XML
Operations:
queue_job — async job queue (required by MAF workflow agents)
report_xlsx — Excel report exports
web_responsive — mobile-responsive web client
How to vendor:
cd oca_addons
git submodule add -b 18.0 \
https://github.com/OCA/account-financial-reporting \
account-financial-reporting
cd account-financial-reporting && git checkout <commit-hash>
§3 — Module Structure Template
ipai_<domain>_<type>/
├── __manifest__.py # Always version="18.0.1.0.0"
├── __init__.py
├── models/
│ ├── __init__.py
│ └── <model>.py
├── views/
│ └── <model>_views.xml
├── security/
│ ├── ir.model.access.csv
│ └── security.xml
├── data/
│ └── <data>.xml
├── report/
│ └── <report>.xml
└── static/
└── description/
└── icon.png
Manifest template
{
"name": "IPAI <Domain> <Type>",
"version": "18.0.1.0.0",
"category": "IPAI",
"author": "InsightPulseAI",
"license": "AGPL-3",
"depends": ["ipai_dev_studio_base", "<base_module>"],
"data": [
"security/ir.model.access.csv",
"views/<model>_views.xml",
],
"installable": True,
"auto_install": False,
"application": False,
}
§4 — View Authoring (Odoo 18 CE)
Required: <list> not <tree>
<record id="view_partner_tree" model="ir.ui.view">
<field name="arch" type="xml">
<tree>
<field name="name"/>
</tree>
</field>
</record>
<record id="view_partner_list" model="ir.ui.view">
<field name="arch" type="xml">
<list>
<field name="name"/>
<field name="email"/>
</list>
</field>
</record>
view_mode: always list,form not tree,form
<field name="view_mode">list,form</field>
Form view template
<record id="view_<model>_form" model="ir.ui.view">
<field name="name"><module>.<model>.form</field>
<field name="model"><module>.<model></field>
<field name="arch" type="xml">
<form string="<Title>">
<header>
<field name="state" widget="statusbar"
statusbar_visible="draft,confirm,done"/>
</header>
<sheet>
<group>
<group>
<field name="name"/>
<field name="partner_id"/>
</group>
<group>
<field name="date"/>
<field name="amount_total"/>
</group>
</group>
<notebook>
<page string="Lines">
<field name="line_ids">
<list editable="bottom">
<field name="product_id"/>
<field name="quantity"/>
<field name="price_unit"/>
</list>
</field>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
Search view template
<record id="view_<model>_search" model="ir.ui.view">
<field name="name"><module>.<model>.search</field>
<field name="model"><module>.<model></field>
<field name="arch" type="xml">
<search>
<field name="name" string="Name"/>
<filter name="active" string="Active"
domain="[('state', '=', 'active')]"/>
<group expand="0" string="Group By">
<filter name="group_partner" string="Partner"
context="{'group_by': 'partner_id'}"/>
</group>
</search>
</field>
</record>
§5 — ORM + Python Patterns (Odoo 18)
Model template
from odoo import api, fields, models
from odoo.exceptions import UserError, ValidationError
class IpaiExample(models.Model):
_name = 'ipai.example'
_description = 'IPAI Example'
_order = 'date desc, id desc'
name = fields.Char(string='Name', required=True)
date = fields.Date(string='Date', default=fields.Date.context_today)
partner_id = fields.Many2one('res.partner', string='Partner',
ondelete='restrict')
state = fields.Selection([
('draft', 'Draft'),
('confirm', 'Confirmed'),
('done', 'Done'),
], string='Status', default='draft', required=True)
amount_total = fields.Monetary(string='Total',
currency_field='currency_id')
currency_id = fields.Many2one('res.currency',
default=lambda self: self.env.company.currency_id)
@api.constrains('amount_total')
def _check_amount(self):
for rec in self:
if rec.amount_total < 0:
raise ValidationError("Amount cannot be negative.")
def action_confirm(self):
self.ensure_one()
if self.state != 'draft':
raise UserError("Only draft records can be confirmed.")
self.write({'state': 'confirm'})
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
if not vals.get('name'):
vals['name'] = self.env['ir.sequence'].next_by_code('ipai.example')
return super().create(vals_list)
Compute field pattern
total = fields.Float(compute='_compute_total', store=True)
@api.depends('line_ids.price_subtotal')
def _compute_total(self):
for rec in self:
rec.total = sum(rec.line_ids.mapped('price_subtotal'))
Domain filter (correct 18 syntax)
domain = [('state', '=', 'posted'), ('partner_id.country_id.code', '=', 'PH')]
from odoo import expression
domain = expression.AND([
[('state', '=', 'posted')],
[('date', '>=', date_from)],
])
§6 — BIR Philippine Tax Compliance
Key BIR forms implemented
| Form | Description | Module |
|---|
| BIR 2307 | Certificate of Creditable Withholding | ipai_bir_tax_compliance |
| SLSP | Summary List of Sales/Purchases | ipai_bir_tax_compliance |
| SAWT | Summary Alphalist of Withholding Tax | ipai_bir_tax_compliance |
| 1601-C | Monthly Compensation Tax | ipai_finance_ssc |
| 2550Q | Quarterly VAT return | ipai_finance_ssc |
| 1702-RT/EX | Annual income tax | ipai_finance_ssc |
TBWA\SMP Finance team user codes
These are INDIVIDUAL USERS within TBWA\SMP — never separate tenants or agencies.
Tenant = legal entity only.
| Code | Name | Role | Email domain |
|---|
| CKVC | Khalil Vera Cruz | Finance Director | @omc.com |
| RIM | Rey Meran | Senior Finance Manager | @omc.com |
| BOM | Beng Manalo | Finance Supervisor | @omc.com |
| JAP | Jinky Paladin | Finance | @omc.com |
| JPAL | JP Loterte | Finance | @omc.com |
| JLI | Jasmin Ignacio | Finance | @omc.com |
| LAS | Amor Lasaga | Finance | @omc.com |
| JRMO | Jhoee Oliva | Finance | @omc.com |
| JMSM | Joana Mae Maravillas | Finance | @omc.com |
| RMQB | Sally Brillantes | Finance | @omc.com |
| CSD | Cliff Dejecacion | Finance | @omc.com |
BIR month-end freeze window (Release Manager guardrail)
BIR_FREEZE_MONTHS = [3, 6, 9, 12]
BIR_FREEZE_DAYS = range(1, 15)
def is_bir_freeze_window():
today = date.today()
return (today.month in BIR_FREEZE_MONTHS and
today.day in BIR_FREEZE_DAYS)
§7 — ACA Deployment (Odoo on Azure)
Image build + push pattern
VERSION=$(git rev-parse --short HEAD)
IMAGE="acripaiodoo.azurecr.io/odoo:${VERSION}"
docker build -t $IMAGE \
--build-arg ODOO_VERSION=18 \
--no-cache .
az acr login -n acripaiodoo
docker push $IMAGE
ACA deployment pattern (idempotent)
az containerapp update \
-n ipai-odoo-dev-web \
-g rg-ipai-dev-odoo-runtime \
--image acripaiodoo.azurecr.io/odoo:${VERSION} \
--set-env-vars \
DB_HOST="pg-ipai-odoo.postgres.database.azure.com" \
DB_NAME="odoo" \
AZURE_CLIENT_ID="$(az identity show -n id-ipai-dev \
-g rg-ipai-dev-odoo-runtime --query clientId -o tsv)"
PG Flex connection (Entra ID / keyless)
import subprocess
def get_pg_token():
result = subprocess.run([
'az', 'account', 'get-access-token',
'--resource', 'https://ossrdbms-aad.database.windows.net/',
'--query', 'accessToken', '-o', 'tsv'
], capture_output=True, text=True)
return result.stdout.strip()
§8 — Administration (Azure-adapted)
PgBouncer (CRITICAL — enable immediately)
az postgres flexible-server parameter set \
-n pg-ipai-odoo \
-g rg-ipai-dev-odoo-data \
--name pgbouncer.enabled \
--value on
Database backup verification
az postgres flexible-server show \
-n pg-ipai-odoo \
-g rg-ipai-dev-odoo-data \
--query "backup.backupRetentionDays" -o tsv
Module installation (production-safe)
az containerapp exec \
-n ipai-odoo-dev-web \
-g rg-ipai-dev-odoo-runtime \
--command "odoo -d odoo --stop-after-init -i <module_name>"
§9 — Odoo 18 Inheritance Types
Source: https://www.odoo.com/documentation/18.0/developer/tutorials/server_framework_101/12_inheritance.html
Odoo has three distinct inheritance mechanisms. Choosing the wrong one
is the most common extension mistake.
Type 1 — Extension (_inherit, same _name)
Extends an existing model in-place. The original model gains your new
fields/methods. No new table. All existing records immediately have the
new fields. This is the workhorse — use it for 90% of IPAI extensions.
class AccountMove(models.Model):
_inherit = 'account.move'
bir_2307_ref = fields.Char(string='BIR 2307 Reference')
wtax_amount = fields.Monetary(
string='Withholding Tax Amount',
compute='_compute_wtax_amount', store=True
)
@api.depends('line_ids.tax_ids')
def _compute_wtax_amount(self):
for move in self:
move.wtax_amount = sum(
line.balance for line in move.line_ids
if any(t.tax_group_id.name == 'Withholding' for t in line.tax_ids)
)
View extension (xpath — add fields without replacing the whole view):
<record id="view_move_form_bir" model="ir.ui.view">
<field name="name">account.move.form.bir</field>
<field name="model">account.move</field>
<field name="inherit_id" ref="account.view_move_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='ref']" position="after">
<field name="bir_2307_ref"/>
<field name="wtax_amount"/>
</xpath>
</field>
</record>
Type 2 — Prototype / Copy (_inherit + new _name)
Creates a NEW model that copies the definition of the parent. Separate
table, separate records. Use for creating a parallel model that shares
behaviour but must be independent (rare — think twice before using).
class IpaiBirDocument(models.Model):
_name = 'ipai.bir.document'
_inherit = 'account.move'
_description = 'BIR Document'
bir_form_type = fields.Selection([
('2307', 'BIR Form 2307'),
('slsp', 'SLSP'),
], required=True)
When to use: almost never for IPAI. Only if you genuinely need a
separate model that shares the shape of an existing one.
Type 3 — Delegation (_inherits)
Delegates field storage to another model via a foreign key. The child
model has its own table + a Many2one to the parent. Reading parent fields
on the child works transparently. Use when extending a model that should
remain a distinct record type but share identity fields.
class ResPartnerBirProfile(models.Model):
_name = 'res.partner.bir.profile'
_inherits = {'res.partner': 'partner_id'}
_description = 'Partner BIR Profile'
partner_id = fields.Many2one('res.partner', required=True,
ondelete='cascade')
tin = fields.Char(string='TIN', required=True)
bir_registered_name = fields.Char(string='BIR Registered Name')
For IPAI: prefer Type 1 (_inherit account.move) for BIR fields.
Only use Type 3 if BIR profile data must live in a completely separate
record with its own lifecycle.
Inheritance decision quick-ref
Want to ADD FIELDS to an existing model? → Type 1 (_inherit)
Want to OVERRIDE a method on existing model? → Type 1 (_inherit)
Want to EXTEND a VIEW? → Type 1 + xpath
Need a COPY of a model structure (new table)? → Type 2 (rare)
Need SHARED IDENTITY fields across two models? → Type 3 (_inherits)
§10 — OCA Contribution Workflow
Source: https://odoo-community.org/resources/code
Before contributing a module to OCA
- Find the right PSC (Project Steering Committee) and subscribe to its
mailing list at
https://odoo-community.org/groups
- Sign the CLA:
https://odoo-community.org/about/cla
- Fork the relevant OCA repository (e.g.
OCA/account-financial-tools)
- Use OCA tools: Pylint-Odoo, Flake8, and OCA module templates
PR automation gates (all must be green)
When you open a pull request, it triggers: CLA Bot (verifies CLA status),
Travis CI (automated tests), Coveralls (code coverage), Codacy and/or
CodeClimate (code quality score), and Runboat (live Odoo instance of the
contribution).
Merge threshold: 3 positive reviews within 5 days (or 2 reviews after
5 days), at least one from a PSC member or OCA Core Maintainer.
Testing any OCA module via Runboat
Use Runboat to test any OCA module. Access it from the repository README
("Runboat Try me" button), from the module README, or from a PR's Checks
section ("runboat/build" → Details). Login: admin / admin.
Runboat provides two databases per build:
*-baseonly — clean Odoo, no modules installed
*-all — all modules in the repository installed
For IPAI OCA module evaluation: always test on Runboat before
vendoring into oca_addons/. Confirms 18.0 compatibility without
spinning up a local instance.
§11 — OCA Module README Structure
Source: https://odoo-community.org/modules-documentation
Every OCA module (and every ipai_* module contributed upstream) needs a
README.rst following this structure. The oca-maintainer-tools package
auto-generates it from readme/ fragments.
<module>/
├── readme/
│ ├── DESCRIPTION.rst # What the module does
│ ├── INSTALL.rst # Installation notes (optional)
│ ├── CONFIGURE.rst # Configuration steps (optional)
│ ├── USAGE.rst # How to use it
│ ├── ROADMAP.rst # Known limitations, future plans
│ ├── CHANGELOG.rst # Version history
│ └── CONTRIBUTORS.rst # Author list
└── README.rst # Auto-generated by oca-maintainer-tools
Generate README.rst from fragments:
pip install oca-maintainer-tools
oca-gen-addon-readme --addons-dir . --if-source-miss skip <module_name>
§12 — Contributing Conventions
Commit message format
# Extend-First commit tags
[configure] feat(account): enable SLSP report via account.journal settings
[extend] feat(bir): inherit res.partner, add withholding tax type field
[repurpose] feat(compliance): repurpose project.task for BIR deadline tracking
[compose] feat(close): wire month-end checklist via ir.cron + server actions
[new-module] feat(bir): ipai_bir_tax_compliance — BIR XML/SLSP/SAWT engine
# OCA vendoring
[oca] chore: vendor account_financial_report 18.0.1.2.0 (pinned a1b2c3d)
# Breaking change
[extend] feat!: rename fields on account.move _inherit extension
# Fixes
fix(bir): correct 2307 computation for multi-company
PR requirements
Code review checklist (architecture-judge criteria)
- No Enterprise module imports
- No
iap module references
- All custom modules inherit from
ipai_dev_studio_base
- OCA modules in
oca_addons/ not inline
- No hardcoded company IDs — use
self.env.company
- Multi-company aware:
company_ids or company_id field present
- Inheritance type matches use case (Type 1 for extensions, not Type 2)
- View extensions use
xpath — never replace entire base view
- Pylint-Odoo clean:
pylint --load-plugins pylint_odoo <module>/
§13 — Web Services / External API (XML-RPC + JSON-RPC)
Source: https://www.odoo.com/documentation/18.0/developer/howtos/web_services.html
This is how ipai-odoo-mcp calls Odoo. All 13 MCP tools use the
XML-RPC interface under the hood. Understanding this is required for
debugging the MCP server and adding new tools.
Authentication
import xmlrpc.client
url = "https://erp.insightpulseai.com"
db = "odoo"
username = "admin"
password = "..."
common = xmlrpc.client.ServerProxy(f"{url}/xmlrpc/2/common")
uid = common.authenticate(db, username, password, {})
CRUD operations (XML-RPC)
models = xmlrpc.client.ServerProxy(f"{url}/xmlrpc/2/object")
records = models.execute_kw(db, uid, password,
'account.move', 'search_read',
[[['state', '=', 'posted'], ['move_type', '=', 'out_invoice']]],
{'fields': ['name', 'partner_id', 'amount_total', 'invoice_date'],
'limit': 100, 'offset': 0}
)
new_id = models.execute_kw(db, uid, password,
'res.partner', 'create',
[{'name': 'IPAI Test', 'email': 'test@insightpulseai.com'}]
)
models.execute_kw(db, uid, password,
'res.partner', 'write',
[[new_id], {'phone': '+63 968 269 9265'}]
)
models.execute_kw(db, uid, password,
'res.partner', 'unlink', [[new_id]]
)
Calling business methods
models.execute_kw(db, uid, password,
'account.move', 'action_post', [[invoice_id]]
)
result = models.execute_kw(db, uid, password,
'account.move', 'generate_bir_2307', [[invoice_id]], {}
)
In the Odoo MCP server (ipai-odoo-mcp)
The MCP server wraps these XML-RPC calls as FastMCP tools. When adding
a new MCP tool, the pattern is:
@mcp.tool()
async def get_posted_invoices(
company_id: int,
date_from: str,
date_to: str
) -> list[dict]:
"""Get posted customer invoices for a company in a date range."""
return models.execute_kw(
DB, uid, password,
'account.move', 'search_read',
[[
['state', '=', 'posted'],
['move_type', 'in', ['out_invoice', 'out_refund']],
['company_id', '=', company_id],
['invoice_date', '>=', date_from],
['invoice_date', '<=', date_to],
]],
{'fields': ['name', 'partner_id', 'amount_total', 'invoice_date',
'payment_state', 'bir_2307_ref']}
)
§14 — Multi-Company Guidelines
Source: https://www.odoo.com/documentation/18.0/developer/howtos/company.html
IPAI's deployment is multi-company. TBWA\SMP, Dataverse IT Consultancy,
W9 Studio, and PrismaLab are all separate companies in the same Odoo
instance. All custom fields and modules must be multi-company aware.
Company-dependent fields
Use company_dependent=True for fields whose value differs per company:
class ResPartner(models.Model):
_inherit = 'res.partner'
bir_tin = fields.Char(
string='BIR TIN',
company_dependent=True
)
wtax_agent_type = fields.Selection([
('individual', 'Individual'),
('non_individual', 'Non-Individual'),
], company_dependent=True)
Multi-company consistency
When a record is shared across companies, ensure consistency:
class AccountMove(models.Model):
_inherit = 'account.move'
@api.constrains('company_id', 'partner_id')
def _check_company_consistency(self):
for move in self:
if move.partner_id.company_id and \
move.partner_id.company_id != move.company_id:
raise ValidationError(
f"Partner {move.partner_id.name} does not belong "
f"to company {move.company_id.name}"
)
Default company in new records
company = self.env.company
domain = [('company_id', 'in', self.env.companies.ids)]
Security rules (multi-company record rules)
<record id="rule_bir_doc_company" model="ir.rule">
<field name="name">BIR Document: company</field>
<field name="model_id" ref="model_ipai_bir_document"/>
<field name="domain_force">
['|', ('company_id', '=', False),
('company_id', 'in', company_ids)]
</field>
</record>
§15 — SQL View Reports
Source: https://www.odoo.com/documentation/18.0/developer/howtos/create_reports.html
SQL view models expose PostgreSQL views as Odoo models — ideal for
IPAI analytics dashboards (BIR SLSP summary, month-end close status,
agent activity). Avoids computed fields on live models.
SQL view model template
from odoo import fields, models
class IpaiBirSlspReport(models.Model):
_name = 'ipai.bir.slsp.report'
_description = 'BIR SLSP Summary Report'
_rec_name = 'partner_name'
_auto = False
partner_id = fields.Many2one('res.partner', string='Vendor', readonly=True)
partner_name = fields.Char(string='Vendor Name', readonly=True)
company_id = fields.Many2one('res.company', string='Company', readonly=True)
period = fields.Char(string='Period (YYYY-MM)', readonly=True)
total_purchases = fields.Monetary(string='Total Purchases', readonly=True)
total_vat = fields.Monetary(string='Total VAT', readonly=True)
currency_id = fields.Many2one('res.currency', readonly=True)
def init(self):
"""Called once at module install to create/replace the SQL view."""
self.env.cr.execute("""
CREATE OR REPLACE VIEW ipai_bir_slsp_report AS (
SELECT
row_number() OVER () AS id,
aml.partner_id,
rp.name AS partner_name,
am.company_id,
to_char(am.invoice_date, 'YYYY-MM') AS period,
SUM(am.amount_untaxed_signed) AS total_purchases,
SUM(am.amount_tax_signed) AS total_vat,
am.currency_id
FROM account_move am
JOIN account_move_line aml ON aml.move_id = am.id
JOIN res_partner rp ON rp.id = aml.partner_id
WHERE am.state = 'posted'
AND am.move_type IN ('in_invoice', 'in_refund')
GROUP BY
aml.partner_id, rp.name, am.company_id,
to_char(am.invoice_date, 'YYYY-MM'), am.currency_id
)
""")
Key rules:
_auto = False — no ORM table; SQL view only
row_number() OVER () — always required as the id field
- Never use JOINs that duplicate rows without accounting for it (causes wrong pivot/graph aggregations)
- Fields not needed as measures: add
store=False to hide from pivot
§16 — Accounting Localization (Philippines / BIR)
Source: https://www.odoo.com/documentation/18.0/developer/howtos/accounting_localization.html
IPAI's BIR compliance module extends the Philippines fiscal localization
(l10n_ph). Always build on top of the existing localization — never
replace it.
Installation procedure
The localization module depends on account and l10n_ph:
{
'depends': ['account', 'l10n_ph', 'ipai_dev_studio_base'],
'data': [
'data/account_tax_data.xml',
'data/bir_form_data.xml',
'views/account_move_views.xml',
'report/bir_2307_report.xml',
],
}
Chart of Accounts extension
<record id="account_2307_payable" model="account.account">
<field name="name">Withholding Tax Payable (2307)</field>
<field name="code">21400</field>
<field name="account_type">liability_current</field>
<field name="chart_template_ids" eval="[Command.link(ref('l10n_ph.l10n_ph_chart_template'))]"/>
</record>
Withholding tax definition
<record id="tax_ewt_professional_10" model="account.tax">
<field name="name">EWT - Professional (10%)</field>
<field name="type_tax_use">purchase</field>
<field name="amount_type">percent</field>
<field name="amount">-10</field>
<field name="tax_group_id" ref="tax_group_ewt"/>
<field name="chart_template_ids" eval="[Command.link(ref('l10n_ph.l10n_ph_chart_template'))]"/>
</record>
BIR report using QWeb
<report
id="report_bir_2307"
model="account.move"
string="BIR Form 2307"
report_type="qweb-pdf"
name="ipai_bir_tax_compliance.report_bir_2307_document"
file="ipai_bir_tax_compliance.report_bir_2307_document"
attachment_use="False"
/>
<template id="report_bir_2307_document">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="doc">
<div class="page">
<h2>BIR Form 2307</h2>
<table class="table">
<tr>
<td>Payee TIN:</td>
<td><t t-esc="doc.partner_id.vat"/></td>
</tr>
<tr>
<td>Amount of Income Payment:</td>
<td><t t-esc="doc.amount_untaxed"/></td>
</tr>
<tr>
<td>Amount of Tax Withheld:</td>
<td><t t-esc="doc.wtax_amount"/></td>
</tr>
</table>
</div>
</t>
</t>
</template>
§17 — Custom OWL Fields and Client Actions (Pulser UI)
Source: https://www.odoo.com/documentation/18.0/developer/howtos/javascript_field.html
Source: https://www.odoo.com/documentation/18.0/developer/howtos/javascript_client_action.html
These patterns are needed for the Pulser chat widget embedded in Odoo
and for any custom field widgets in the Finance team forms.
Custom field widget (OWL component)
import { registry } from "@web/core/registry";
import { Component } from "@odoo/owl";
import { standardFieldProps } from "@web/views/fields/standard_field_props";
class BirStatusField extends Component {
static template = "ipai_bir.BirStatusField";
static props = { ...standardFieldProps };
get statusClass() {
const status = this.props.record.data[this.props.name];
return {
'draft': 'text-muted',
'filed': 'text-success',
'overdue': 'text-danger',
}[status] || 'text-secondary';
}
}
registry.category("fields").add("bir_status", BirStatusField);
<templates>
<t t-name="ipai_bir.BirStatusField">
<span t-att-class="statusClass">
<t t-esc="props.record.data[props.name]"/>
</span>
</t>
</templates>
Use in view:
<field name="bir_filing_status" widget="bir_status"/>
Pulser client action (chat panel)
import { registry } from "@web/core/registry";
import { Component, useState } from "@odoo/owl";
class PulserChatAction extends Component {
static template = "ipai_odoo_copilot.PulserChatAction";
setup() {
this.state = useState({
messages: [],
input: "",
loading: false,
});
}
async sendMessage() {
this.state.loading = true;
const response = await fetch("/pulser/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
message: this.state.input,
context: { company_id: this.env.company.id },
}),
});
const data = await response.json();
this.state.messages.push({ role: "assistant", content: data.reply });
this.state.input = "";
this.state.loading = false;
}
}
registry.category("actions").add(
"ipai_odoo_copilot.PulserChatAction",
PulserChatAction
);
Register as a client action in XML:
<record model="ir.actions.client" id="action_pulser_chat">
<field name="name">Pulser AI Copilot</field>
<field name="tag">ipai_odoo_copilot.PulserChatAction</field>
</record>
<menuitem
id="menu_pulser_chat"
name="Pulser"
action="action_pulser_chat"
sequence="100"
web_icon="ipai_odoo_copilot,static/description/icon.png"
/>
§18 — OCA/ai Bridge (Odoo 18 CE AI Integration)
Source: https://github.com/OCA/ai (branch: 18.0)
Repo: OCA/ai — 5 modules, all available for Odoo 18 CE
This is the CE path to embedded AI in Odoo. The OCA bridge connects
external LLMs (Claude via ipai-copilot-gateway) to Odoo's native
surfaces (chatter, documents, systray chat). This is how Pulser appears
inside Odoo without requiring Odoo 19 Enterprise.
Module inventory (all on 18.0 branch)
| Module | Version | Purpose |
|---|
ai_oca_bridge | 18.0.2.0.0 | Base configuration — bridge to external AI systems |
ai_oca_bridge_chatter | 18.0.2.0.0 | AI in Odoo chatter (message thread on every record) |
ai_oca_bridge_document_page | 18.0.1.0.0 | AI sync for Knowledge/Document pages |
ai_oca_bridge_extra_parameters | 18.0.x | Extra parameters for bridge configuration |
ai_oca_native_generate_ollama | 18.0.x | Ollama local LLM connector (reference pattern) |
For IPAI: use ai_oca_bridge + ai_oca_bridge_chatter. Skip
ai_oca_native_generate_ollama — IPAI uses Azure Foundry/Claude, not
Ollama. Use it as a code pattern reference for building an
ai_oca_native_generate_foundry connector.
Vendor into oca_addons/
cd oca_addons/
git submodule add -b 18.0 https://github.com/OCA/ai ai
cd ai && git checkout <pinned-commit-hash>
Install: ai_oca_bridge, ai_oca_bridge_chatter
Architecture: OCA bridge + ipai-copilot-gateway = Pulser in Odoo
User in Odoo chatter
→ ai_oca_bridge_chatter (OCA module in Odoo 18 CE)
→ ai_oca_bridge (OCA) → HTTP call to configured AI endpoint
→ ipai-copilot-gateway (ACA, internal)
→ Azure AI Foundry → claude-sonnet-4-6
→ Pulser agent logic (MAF)
← Response → ai_oca_bridge → chatter message posted
Creating a Foundry provider (ipai_ai_foundry_provider)
The OCA bridge is provider-agnostic. Implement a new provider by
inheriting ai.model.provider:
from odoo import fields, models
class AiModelProviderFoundry(models.Model):
_inherit = 'ai.model.provider'
@property
def _provider_key(self):
return 'foundry'
def _call_provider(self, prompt, model, **kwargs):
"""Call ipai-copilot-gateway → Foundry → Claude."""
import requests
from azure.identity import ManagedIdentityCredential
cred = ManagedIdentityCredential(
client_id=self.env['ir.config_parameter'].sudo()
.get_param('ipai.azure_client_id')
)
token = cred.get_token(
"https://cognitiveservices.azure.com/.default"
).token
gateway_url = self.env['ir.config_parameter'].sudo().get_param(
'ipai.copilot_gateway_url',
default='http://ipai-copilot-gateway'
)
response = requests.post(
f"{gateway_url}/pulser/chat",
headers={"Authorization": f"Bearer {token}"},
json={
"messages": [{"role": "user", "content": prompt}],
"model": model or "claude-sonnet-4-6",
},
timeout=30,
)
return response.json().get('content', '')
§19 — Odoo 19 AI Agents (Strategic Context — NOT for 18 CE)
Source: https://www.odoo.com/documentation/19.0/applications/productivity/ai/agents.html
⚠️ Odoo 19 Enterprise only — NOT available in IPAI's Odoo 18 CE.
Document this architecture as:
- The upgrade target when IPAI moves to 19 CE/Enterprise
- The benchmark against which Pulser must be positioned in GTM
- The reference for what Odoo will natively offer to customers
Odoo 19 AI Agent architecture
Agent
├── System Prompt — role, personality, scope, behavior
├── Response Style — Analytical | Balanced | Creative
├── LLM Model — ChatGPT (OpenAI) | Gemini | Claude (partial)
├── Topics — what the agent can DO (actions)
│ ├── Instructions — topic-specific prompt layer
│ └── AI Tools — actual Odoo functions (create record, open view, etc.)
└── Sources — what the agent KNOWS (RAG)
├── Documents — uploaded files
├── Knowledge — Odoo Knowledge articles
└── Weblinks — external URLs (indexed)
Preconfigured topics (out of box in Odoo 19)
- Natural Language Search — interprets user queries, opens correct view with filters
- Information Retrieval — tools to fetch data about any Odoo model
- Create Leads — automated lead creation (if CRM installed)
An agent with no topics assigned = read-only chatbot. Topics = action capability.
AI Server Actions (Manager/Worker pattern)
AI Server Action (Manager)
→ reads the record + context
→ interprets the AI prompt
→ decides which Tool to call + arguments
← calls Tool
Tool (Worker — standard server action with "Use in AI" enabled)
→ contains all execution logic
→ performs record updates, moves, writes
→ enforces business rules in Python
→ executes unconditionally if called (guard logic must be in Tool code)
Critical: The AI does not infer arguments from Python code. Arguments
passed to a Tool are declared explicitly in the tool's AI Schema (Usage tab).
The Name column in AI Schema must match the Python variable name exactly.
IPAI positioning against Odoo 19 AI Agents
| Capability | Odoo 19 EE AI Agent | Pulser (IPAI 18 CE) |
|---|
| Embedded in Odoo | ✅ native | ✅ via OCA bridge |
| Custom LLM | ChatGPT / Gemini only | ✅ Claude (Anthropic) |
| RAG sources | Documents + Knowledge + Weblinks | ✅ AI Search + Odoo MCP |
| Action execution | Topics + Tools | ✅ MAF + policy-gated execution |
| RBAC | Role-based agents | ✅ approval bands + mutation safety |
| BIR compliance | ❌ not Philippine-specific | ✅ native 2307/SLSP/SAWT |
| Multi-company | Varies | ✅ company_id aware |
| Azure-native | ❌ | ✅ Foundry + ACA |
| Audit trail | Limited | ✅ ops.run_events append-only |
GTM message: "Pulser does what Odoo 19 Enterprise AI Agents do — without
the Enterprise license, natively on Azure, with Philippine BIR compliance
built in, and governed by RBAC and approval bands."
Upgrade path (when IPAI moves to 19)
When upgrading from 18 CE to 19 CE/Enterprise:
- OCA bridge modules (
ai_oca_bridge, ai_oca_bridge_chatter) migrate to
native Odoo 19 AI App
ipai_ai_foundry_provider extends native AI model registry with Claude
- Pulser Topics map 1:1 to the Topics architecture in Odoo 19
- Pulser AI Tools map to the Worker pattern (server actions with
Use in AI)
- Sources stay:
srch-ipai-dev (AI Search) indexes → Odoo 19 Sources
The OCA bridge is the bridge to both directions — it works today on 18 CE
and provides the correct abstraction for the 19 upgrade.
§20 — D365 Benchmark Resources (for P2P/R2R Spec Validation)
Source: https://learn.microsoft.com/en-us/dynamics365/guidance/resources/
GitHub: https://github.com/microsoft/Dynamics-365-FastTrack-Implementation-Assets
Purpose for IPAI: Every claim in the P2P and R2R spec bundles needs
validation against actual D365 behavior. IPAI now has D365 Finance + Project
Operations sandbox access (ISV Success → experience.dynamics.com/requestlicense/).
These resources tell you how to use it.
D365 FastTrack Implementation Assets (repo structure)
| Folder | IPAI relevance |
|---|
ERP/Finance | R2R benchmark — GL, AP, AR, period close, tax |
ERP/SCM/SPS | P2P benchmark — procurement, vendor invoices |
Administration/Integration | D365 ↔ Odoo integration patterns (dual-write reference) |
Administration/Analytics | D365 analytics pipeline — compare vs IPAI Fabric/Databricks |
Administration/Dual-write | If any customer needs D365 + Odoo side-by-side |
Success by Design — implementation framework (5 stages)
The D365 implementation methodology that enterprise consultants follow.
IPAI must have an equivalent implementation path for Pulser to be credible
in enterprise sales:
D365 Success by Design → Pulser Implementation Path (to build)
────────────────────────────────────────────────────────────────────
1. Strategize → 1. IPAI Discovery & Fit Assessment
2. Initiate → 2. Environment Setup (ACA + Odoo 18 CE)
3. Implement → 3. Module Configuration + OCA vendoring
4. Prepare → 4. BIR localization + TBWA\SMP seed data
5. Operate → 5. Pulser agent activation + Release Manager gate
D365 data entity → Odoo model mapping (P2P/R2R validation)
D365 uses "data entities" for its data model. Map these to Odoo models
to validate feature parity claims in the spec bundles:
| D365 Data Entity | D365 Module | Odoo Equivalent | Pulser spec |
|---|
VendorInvoiceHeader | Finance / AP | account.move (move_type=in_invoice) | P2P §AP-01 |
VendorInvoiceLine | Finance / AP | account.move.line | P2P §AP-02 |
VendorInvoiceCharges | Finance / AP | account.move.line (product=charges) | P2P §AP-03 |
VendorInvoiceDocumentAttachment | Finance | ir.attachment on account.move | P2P §AP-04 |
| Customer entity | Finance / AR | res.partner (customer_rank > 0) | R2R §AR-01 |
| Sales order header | Finance | sale.order | P2P §SO-01 |
| Product entity | Finance / SCM | product.template | P2P §PRD-01 |
| Journal entry | Finance / GL | account.move (move_type=entry) | R2R §GL-01 |
| Project operations | Project Ops | project.project + project.task | P2P §POps-01 |
Validation workflow:
1. Open D365 Finance sandbox (experience.dynamics.com/requestlicense/)
2. Navigate to the D365 feature (e.g., AP → Vendor invoices)
3. Screenshot the D365 UI and data flow
4. Open Odoo 18 CE → same workflow
5. Screenshot Odoo equivalent
6. Update spec bundle with side-by-side comparison
7. Note gaps → evaluate via §1 Extend-First hierarchy
D365 import entities as Odoo migration reference
The import entities (customers, products, sales orders, vendor invoices)
define D365's canonical data model. When migrating a D365 customer to
Odoo/Pulser, these entities map the fields to transform:
D365_TO_ODOO_VENDOR_INVOICE = {
'InvoiceDate': 'invoice_date',
'DueDate': 'invoice_date_due',
'VendorAccountNumber': 'partner_id',
'CurrencyCode': 'currency_id',
'InvoiceNumber': 'ref',
'Description': 'narration',
'TotalInvoiceAmount': None,
}
Free trial environments (from ISV Success)
D365 Finance + Project Operations Tier 2 sandbox:
→ https://experience.dynamics.com/requestlicense/
→ Request: "Dynamics 365 Operations Application Partner Sandbox (Tier 2)"
→ Prereq: Tier 1 sandbox (request first)
→ Includes: Finance, Supply Chain, Commerce HQ, Project Operations
→ Cost: FREE (retail value $7,500/month)
→ Credentials: use partner@insightpulseai.com (must be in Entra tenant)
D365 Sales/FS/CS (already in Partner Center):
→ Partner Center → Benefits → Cloud services → D365 Partner Sandbox
→ 25 users, free until Apr 30, 2027
Using FastTrack assets for Odoo feature parity proof
When a prospect asks "can Odoo do what D365 does?" — use this workflow:
- Find the D365 FastTrack asset for the feature (GitHub repo)
- Run it against the D365 sandbox
- Find the OCA equivalent or Odoo CE equivalent
- Run Pulser's MCP tool on the Odoo result
- Document the comparison in
spec/pulser-*/benchmarks/
This is the evidence base for the Marketplace listing and co-sell claims.
§21 — D365 Project Operations → Odoo 18 CE Mapping (P2P Spec)
Source: https://learn.microsoft.com/en-us/dynamics365/project-operations/welcome-to-project-operations
For IPAI's Pulser Project-to-Profit spec validation. D365 Project Operations
is what Pulser must match and replace. This section maps every D365 PO concept
to its Odoo 18 CE equivalent so the P2P spec claims can be validated.
D365 Project Operations — 3 deployment modes
| Mode | Description | IPAI relevance |
|---|
| Core (Lite) | Dataverse only, proforma invoicing, no ERP required | Closest to Odoo standalone — validate P2P Core features |
| Integrated with ERP | Full Finance + PO + dual-write for revenue recognition | Full enterprise benchmark for R2R integration |
| Manufacturing | WBS + production orders | Not in IPAI scope |
TBWA\SMP's use case = Core + Integrated. They need time tracking, expense,
invoicing, and revenue recognition — all within the Integrated deployment.
D365 billing methods → Odoo equivalents
Time and Material billing (D365 Core concept):
D365: Hours logged → actuals → proforma → customer invoice → revenue recognized
Odoo: hr.timesheet → sale.order (invoice_policy=timesheet) → account.move
Fixed price billing (D365 billing milestones):
D365: Billing schedule → milestone trigger → invoice → Completed Contract/Percent Completion
Odoo: sale.order.line (invoice_policy=milestone) → account.move
Full entity mapping (D365 PO → Odoo 18 CE)
| D365 Entity | D365 Module | Odoo Model | Notes |
|---|
| Project contract | Sales | sale.order | One contract = one SO |
| Contract line | Sales | sale.order.line | Each deliverable = one SOL |
| Project | Project Ops | project.project | Link via analytic_account_id |
| Work Breakdown Structure | Project Ops | project.task (hierarchy) | Subtasks = WBS levels |
| Time entry | Time tracking | account.analytic.line (timesheet) | so_line links to SOL |
| Expense | Expense Mgmt | hr.expense + hr.expense.sheet | |
| Material usage | Inventory | stock.move (analytic line) | |
| Proforma invoice | Billing | account.move (state=draft, type=out_invoice) | Don't post = proforma |
| Customer invoice | Billing | account.move (state=posted) | |
| Revenue recognition | Finance | account.analytic.account + journal entries | WIP = analytic account |
| WIP account | Finance | account.account (asset type, WIP) | |
| Accrued revenue | Finance | account.account (credit) | Reverse at invoicing |
| Project cost/revenue profile | Finance | Odoo account configuration | |
| Billable/non-billable | Finance | so_line on timesheet | NULL = non-billable |
| Resource | Resource Mgmt | hr.employee + resource.resource | |
| Role pricing | Pricing | product.pricelist + hr.employee.skill | |
| Budget | Project Ops | project.project planned_hours | |
Revenue recognition in Odoo (matching D365 methods)
Time and Material — accrue at transaction (D365 → Odoo):
class ProjectProject(models.Model):
_inherit = 'project.project'
Fixed Price — Completed Contract method:
Fixed Price — Percent Completion method:
@api.depends('timesheet_ids.unit_amount', 'planned_hours')
def _compute_percent_complete(self):
for project in self:
if project.planned_hours > 0:
actual = sum(project.timesheet_ids.mapped('unit_amount'))
project.percent_complete = (actual / project.planned_hours) * 100
else:
project.percent_complete = 0.0
Pulser P2P benchmark validation workflow
1. Open D365 Project Operations sandbox
(Tier 2 from experience.dynamics.com/requestlicense/)
2. Create a project contract:
- Contract type: Time and Material
- Client: TBWA\SMP equivalent
- Line: Creative Services @ ₱5,000/hr
3. Log timesheets → approve → generate proforma → post invoice
4. Screenshot the D365 UI at each step
5. In Odoo:
- sale.order (confirmed) → project.project → analytic timesheets
→ invoice (timesheet-based) → posted
Screenshot each step
6. Document in spec/pulser-project-to-profit/benchmarks/
"D365_PO_vs_Odoo_TM_billing.md"
Key gaps: what D365 PO has that Odoo CE does not
| Feature | D365 | Odoo 18 CE | OCA solution |
|---|
| Microsoft Project for the Web (WBS) | ✅ | project.task hierarchy (limited) | project_wbs OCA |
| Universal Resource Scheduling | ✅ AI-powered | resource.resource + planning.slot | resource_planning OCA |
| Multidimensional pricing (role + org unit + date) | ✅ | product.pricelist (one dimension) | Custom ipai_*_delta |
| Revenue recognition periodic calculation | ✅ | Manual journal entries | account_analytic OCA |
| Receipt OCR (Integrated mode) | ✅ | ✅ account.vendor.bill AI (18 CE) | Native |
| Proforma invoice workflow | ✅ | Draft invoice = proforma | Native |
| Fixed-price % completion | ✅ | Compute from timesheets | [extend] on project.project |
Pulser's advantage over D365 PO:
- BIR 2307/SLSP/SAWT compliance (native, not available in D365)
- Philippine tax localization (D365 PH localization is minimal)
- AI-native (Pulser copilot in Odoo vs D365's separate Copilot licensing)
- Zero licensing cost (Odoo 18 CE vs D365 Project Operations ≈ $120/user/mo)
- Single system for ERP + AI (Odoo unified vs D365 fragmented across Dataverse + Finance)
§22 — D365 Business Process Catalog → Odoo Module Map
Source: https://learn.microsoft.com/en-us/dynamics365/guidance/business-processes/overview
Download: https://aka.ms/BusinessProcessCatalog (updated quarterly)
The canonical framework for all Pulser spec bundles. D365 organizes all
ERP functionality into 15 end-to-end scenarios (catalog IDs 10–99). IPAI's
Pulser spec bundles must map 1:1 to these scenarios. This section provides
the D365 scenario → Odoo module mapping for the 4 scenarios relevant to
TBWA\SMP Finance operations.
D365 End-to-End Scenarios (all 15)
| ID | Scenario | IPAI priority | Odoo coverage |
|---|
| 10 | Acquire to dispose | Low | account.asset |
| 20 | Case to resolution | Low | helpdesk (OCA) |
| 30 | Concept to market | Low | product |
| 40 | Design to retire | Low | mrp |
| 50 | Forecast to plan | Medium | stock.warehouse.orderpoint |
| 55 | Hire to retire | Medium | hr.* + payroll OCA |
| 60 | Inventory to deliver | Medium | stock.* |
| 65 | Order to cash | High | sale.* + account.move (AR) |
| 70 | Plan to produce | Low | mrp.* |
| 75 | Source to pay | High | purchase.* + AP + BIR 2307 |
| 80 | Project to profit | Primary | project.* + analytic + invoicing |
| 85 | Prospect to quote | Medium | crm.* + sale.order |
| 90 | Record to report | High | account.* + period close |
| 95 | Service to deliver | Low | helpdesk + field.service |
| 99 | Administer to operate | Medium | Odoo admin + Azure RBAC |
Project to Profit (ID 80) — 6 areas → Odoo modules
| D365 Business Process Area | Odoo Module | OCA / Custom |
|---|
| Develop project strategy | project.project (settings) | — |
| Manage project contracts | sale.order + sale.order.line | — |
| Plan projects (WBS, schedule, budget) | project.task (hierarchy) + project.project planned_hours | project_wbs (OCA) |
| Manage project delivery (time, expense) | account.analytic.line + hr.expense | hr_timesheet_sheet (OCA) |
| Manage project financials (billing, revenue) | account.move + analytic accounts + milestones | account_analytic_* (OCA) |
| Analyze project performance | SQL view reports + Power BI / Fabric | project_report (OCA) |
Record to Report (ID 90) — 6 areas → Odoo modules
| D365 Business Process Area | Odoo Module | BIR relevance |
|---|
| Define accounting policies | account.chart.template + account.account | Chart of accounts = PH GAAP |
| Manage cash | account.bank.statement + bank reconciliation | — |
| Manage budgets | account.budget (OCA) | — |
| Record financial transactions | account.move + account.journal | All posted moves = BIR audit trail |
| Close financial periods | account.move period locking + year-end | Month-end close = TBWA\SMP workflow |
| Analyze financial performance | SQL views + account.report | FS reports + BIR forms |
TBWA\SMP Finance team month-end close maps to "Close financial periods":
- Lock prior period → Odoo:
account.journal sequence lock date
- Accrue expenses → Odoo: manual journal entries on
account.move
- Reconcile subledgers → Odoo:
account.reconcile.model
- Post BIR returns → IPAI:
ipai_bir_tax_compliance module
- Generate trial balance → Odoo:
account.report (General Ledger)
Source to Pay (ID 75) — 6 areas → Odoo modules
| D365 Business Process Area | Odoo Module | BIR relevance |
|---|
| Develop procurement strategy | purchase.order settings | — |
| Define procurement catalogs | product.template (purchase) | — |
| Manage supplier relationships | res.partner (supplier_rank > 0) | BIR TIN on partner |
| Source and contract goods/services | purchase.requisition (OCA) | — |
| Procure goods and services | purchase.order + stock.picking | — |
| Manage accounts payable | account.move (in_invoice) + account.payment | BIR 2307 = EWT on vendor payment |
Critical AP → BIR link:
D365: Invoice capture → OCR → vendor invoice → payment → tax withheld
Odoo: account.move (in_invoice) → account.payment → wtax_amount → BIR 2307
Order to Cash (ID 65) — 5 areas → Odoo modules
| D365 Business Process Area | Odoo Module | Notes |
|---|
| Develop sales policies | product.pricelist + res.partner credit | — |
| Manage sales orders | sale.order + sale.order.line | |
| Manage accounts receivable | account.move (out_invoice) + payments | |
| Manage credit and collections | account.credit.limit (OCA) | Odoo CE: limited native |
| Analyze sales performance | sale.report SQL view + Power BI | |
Pulser Spec Bundle → D365 Catalog Alignment
Each Pulser spec bundle should reference the D365 catalog ID and area:
spec/
pulser-project-to-profit/ ← D365 Catalog ID 80
benchmarks/
D365_80_vs_Odoo_mapping.md ← this section (§22)
D365_billing_methods.md ← §21
P2P_spec_bundle.md
pulser-record-to-report/ ← D365 Catalog ID 90
benchmarks/
D365_90_month_end_close.md
R2R_spec_bundle.md
pulser-source-to-pay/ ← D365 Catalog ID 75
benchmarks/
D365_75_AP_BIR2307.md
S2P_spec_bundle.md
pulser-order-to-cash/ ← D365 Catalog ID 65
benchmarks/
D365_65_AR_invoicing.md
O2C_spec_bundle.md
Business Process Catalog download and use
The catalog is available as Excel at https://aka.ms/BusinessProcessCatalog
Updated at least 4x/year (latest: December 2025 version).
Use the catalog to:
- Find the exact D365 business process name for any feature claim
- Identify which D365 module(s) support it (Finance, SCM, PO, Sales, etc.)
- Map it to the Odoo equivalent using this section
- Validate in the D365 sandbox
- Document in the spec bundle with catalog ID reference
This is the evidence base that makes Pulser claims defensible in
enterprise sales and in Microsoft co-sell conversations.
§23 — OCA "Must Have" Modules (Curated Baseline)
Source: https://odoo-community.org/page/must-have-oca-modules
OCA volunteer consultants curated this list. Every IPAI Odoo 18 CE instance
should have these installed. Add to oca_addons/ submodule accordingly.
Core / Base (all instances)
| Module | Technical Name | OCA Repo | Purpose | IPAI relevance |
|---|
| Queue Job | queue_job | queue | Async background job execution via JobRunner | BIR form generation, period close, batch invoice |
| Report XLSX | report_xlsx | reporting-engine | Excel report generation base class | SLSP, SAWT, month-end reports |
| Password Security | password_security | server-auth | Company-level password requirements | Security hardening |
| Disable Odoo Online | disable_odoo_online | server-brand | Removes odoo.com links/menus (CE cleanup) | Required for CE |
| Remove Odoo Enterprise | remove_odoo_enterprise | server-brand | Removes Enterprise-only UI elements (CE) | Required for CE |
| Audit Log | auditlog | server-tools | Logs create/read/write/delete per model | BIR audit trail, RR90 record keeping |
| Name Search Improved | base_name_search_improved | server-tools | Fuzzy name search across fields | partner TIN search, product search |
| Date Range | date_range | server-ux | Global date range filters for list views | BIR period filters, month-end |
| Mass Edit | server_action_mass_edit | server-ux | Bulk edit multiple records at once | Bulk withholding tax category updates |
| Advanced Search | web_advanced_search | web | Advanced multi-field search | Cross-model filtering |
| Environment Ribbon | web_environment_ribbon | web | Dev/Staging/Prod ribbon (configurable) | Required — distinguish prod/staging |
| Favicon per Company | web_favicon | web | Company-specific favicons | Multi-company (TBWA\SMP, Dataverse, W9) |
| Mail Debrand | mail_debrand | mail | Removes "Powered by Odoo" from emails | Client-facing professionalism |
| Mail Tracking | mail_tracking | mail | Email delivery status per recipient | Collection follow-up tracking |
| Search Mail Content | base_search_mail_content | social | Search task/record by message content | AR collections search |
Install commands (vendor as submodules)
cd oca_addons/
git submodule add -b 18.0 https://github.com/OCA/server-brand server-brand
git submodule add -b 18.0 https://github.com/OCA/queue queue