odoo-pulse 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. odoo_pulse-1.0.0/.env.example +33 -0
  2. odoo_pulse-1.0.0/.github/workflows/ci.yml +31 -0
  3. odoo_pulse-1.0.0/.github/workflows/playground.yml +21 -0
  4. odoo_pulse-1.0.0/.github/workflows/release.yml +53 -0
  5. odoo_pulse-1.0.0/.gitignore +27 -0
  6. odoo_pulse-1.0.0/CLAUDE.md +50 -0
  7. odoo_pulse-1.0.0/LICENSE +21 -0
  8. odoo_pulse-1.0.0/Makefile +13 -0
  9. odoo_pulse-1.0.0/PKG-INFO +120 -0
  10. odoo_pulse-1.0.0/README.md +91 -0
  11. odoo_pulse-1.0.0/assets/business_pulse.svg +137 -0
  12. odoo_pulse-1.0.0/assets/og-card.png +0 -0
  13. odoo_pulse-1.0.0/docker/seed/seed.py +426 -0
  14. odoo_pulse-1.0.0/docker-compose.playground.yml +58 -0
  15. odoo_pulse-1.0.0/docs/install.md +91 -0
  16. odoo_pulse-1.0.0/docs/playground.md +68 -0
  17. odoo_pulse-1.0.0/docs/tools.md +242 -0
  18. odoo_pulse-1.0.0/llms-install.md +65 -0
  19. odoo_pulse-1.0.0/odoo_pulse/__init__.py +3 -0
  20. odoo_pulse-1.0.0/odoo_pulse/cache.py +45 -0
  21. odoo_pulse-1.0.0/odoo_pulse/domain_tools.py +516 -0
  22. odoo_pulse-1.0.0/odoo_pulse/odoo_client.py +437 -0
  23. odoo_pulse-1.0.0/odoo_pulse/runtime.py +77 -0
  24. odoo_pulse-1.0.0/odoo_pulse/server.py +36 -0
  25. odoo_pulse-1.0.0/odoo_pulse/tool_groups.py +57 -0
  26. odoo_pulse-1.0.0/odoo_pulse/tools_engagement.py +164 -0
  27. odoo_pulse-1.0.0/odoo_pulse/tools_generic.py +220 -0
  28. odoo_pulse-1.0.0/odoo_pulse/tools_hr.py +215 -0
  29. odoo_pulse-1.0.0/odoo_pulse/tools_niche.py +545 -0
  30. odoo_pulse-1.0.0/odoo_pulse/tools_operations.py +268 -0
  31. odoo_pulse-1.0.0/odoo_pulse/tools_projects.py +149 -0
  32. odoo_pulse-1.0.0/odoo_pulse/tools_reports.py +892 -0
  33. odoo_pulse-1.0.0/odoo_pulse/tools_workflows.py +751 -0
  34. odoo_pulse-1.0.0/odoo_pulse/tools_write.py +191 -0
  35. odoo_pulse-1.0.0/odoo_pulse/workflow_helpers.py +108 -0
  36. odoo_pulse-1.0.0/pyproject.toml +58 -0
  37. odoo_pulse-1.0.0/requirements.txt +1 -0
  38. odoo_pulse-1.0.0/scripts/demo_pulse.py +99 -0
  39. odoo_pulse-1.0.0/scripts/make_og.py +96 -0
  40. odoo_pulse-1.0.0/scripts/playground_smoke.sh +54 -0
  41. odoo_pulse-1.0.0/scripts/smoke_live.py +219 -0
  42. odoo_pulse-1.0.0/server.json +55 -0
  43. odoo_pulse-1.0.0/tests/conftest.py +167 -0
  44. odoo_pulse-1.0.0/tests/test_cache.py +41 -0
  45. odoo_pulse-1.0.0/tests/test_client.py +296 -0
  46. odoo_pulse-1.0.0/tests/test_client_aggregate.py +69 -0
  47. odoo_pulse-1.0.0/tests/test_client_cache.py +46 -0
  48. odoo_pulse-1.0.0/tests/test_config.py +133 -0
  49. odoo_pulse-1.0.0/tests/test_domain_tools.py +127 -0
  50. odoo_pulse-1.0.0/tests/test_runtime.py +72 -0
  51. odoo_pulse-1.0.0/tests/test_tool_groups.py +40 -0
  52. odoo_pulse-1.0.0/tests/test_tools_aggregate.py +49 -0
  53. odoo_pulse-1.0.0/tests/test_tools_attachment.py +63 -0
  54. odoo_pulse-1.0.0/tests/test_tools_project_status_report.py +246 -0
  55. odoo_pulse-1.0.0/tests/test_tools_projects.py +71 -0
  56. odoo_pulse-1.0.0/tests/test_tools_reports_absence.py +81 -0
  57. odoo_pulse-1.0.0/tests/test_tools_reports_inventory.py +85 -0
  58. odoo_pulse-1.0.0/tests/test_tools_reports_pipeline.py +111 -0
  59. odoo_pulse-1.0.0/tests/test_tools_reports_pulse.py +79 -0
  60. odoo_pulse-1.0.0/tests/test_tools_reports_receivables.py +78 -0
  61. odoo_pulse-1.0.0/tests/test_tools_reports_sales.py +98 -0
  62. odoo_pulse-1.0.0/tests/test_tools_smoke.py +109 -0
  63. odoo_pulse-1.0.0/tests/test_tools_sprint_health.py +161 -0
  64. odoo_pulse-1.0.0/tests/test_tools_team_workload.py +155 -0
  65. odoo_pulse-1.0.0/tests/test_tools_write.py +172 -0
  66. odoo_pulse-1.0.0/tests/test_workflow_helpers.py +67 -0
@@ -0,0 +1,33 @@
1
+ # Odoo connection (XML-RPC external API)
2
+ # For Odoo Online (SaaS) the URL is https://<your-db>.odoo.com
3
+ ODOO_URL=https://your-instance.odoo.com
4
+ ODOO_DB=your-database-name
5
+ ODOO_USERNAME=you@example.com
6
+ # Generate under: Settings > Users > (your user) > Account Security > New API Key
7
+ ODOO_API_KEY=your-api-key
8
+
9
+ # Safety controls
10
+ # Keep true to block create/write/unlink. Set to false to allow writes later.
11
+ ODOO_READ_ONLY=true
12
+ # Hard cap on records returned by a single search_read call.
13
+ ODOO_MAX_RECORDS=200
14
+ # Verify the server's TLS certificate. Set to false only for on-premise
15
+ # instances using a self-signed / private-CA certificate.
16
+ ODOO_VERIFY_SSL=true
17
+ # Socket timeout (seconds) for each XML-RPC call, so a hung Odoo can't
18
+ # block a tool call forever.
19
+ ODOO_TIMEOUT=30
20
+
21
+ # --- Write access (all default to "no writes") ---
22
+ # Master switch must be false before any write is possible.
23
+ # (ODOO_READ_ONLY above must be set to false.)
24
+ # Comma-separated allow-list of models that may be written. Empty = none.
25
+ # System models (ir.*, res.users, base*, ...) are blocked even if listed here.
26
+ ODOO_WRITABLE_MODELS=
27
+ # Allow record deletion (unlink). Stays blocked unless set to true.
28
+ ODOO_ALLOW_DELETE=false
29
+
30
+ # Which tool groups the server exposes (comma-separated).
31
+ # Default: core,reports (~20 tools). Groups: core, reports, business, hr,
32
+ # projects, operations, engagement, niche — or "all".
33
+ #ODOO_TOOL_GROUPS=core,reports
@@ -0,0 +1,31 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: ["**"]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ strategy:
12
+ fail-fast: false
13
+ matrix:
14
+ python-version: ["3.10", "3.11", "3.12"]
15
+
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+
19
+ - name: Set up Python ${{ matrix.python-version }}
20
+ uses: actions/setup-python@v5
21
+ with:
22
+ python-version: ${{ matrix.python-version }}
23
+ cache: pip
24
+
25
+ - name: Install dependencies
26
+ run: |
27
+ python -m pip install --upgrade pip
28
+ pip install -e ".[dev]"
29
+
30
+ - name: Run tests
31
+ run: pytest -q
@@ -0,0 +1,21 @@
1
+ name: playground-smoke
2
+
3
+ # Manual + nightly only — never on PRs, so normal CI stays fast.
4
+ on:
5
+ workflow_dispatch:
6
+ schedule:
7
+ - cron: "0 3 * * *" # 03:00 UTC daily
8
+
9
+ jobs:
10
+ smoke:
11
+ runs-on: ubuntu-latest
12
+ timeout-minutes: 25
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+ - uses: actions/setup-python@v5
16
+ with:
17
+ python-version: "3.12"
18
+ - name: Install odoo-pulse
19
+ run: pip install -e .
20
+ - name: Run playground smoke
21
+ run: ./scripts/playground_smoke.sh
@@ -0,0 +1,53 @@
1
+ name: release
2
+
3
+ # Build and publish to PyPI when a version tag is pushed.
4
+ # Publishing uses PyPI Trusted Publishing (OIDC) — no API token is stored.
5
+ #
6
+ # One-time setup (owner, on PyPI):
7
+ # PyPI > your project > Publishing > Add a trusted publisher (GitHub Actions)
8
+ # Owner: minhhq-a1
9
+ # Repository: odoo-pulse
10
+ # Workflow name: release.yml
11
+ # Environment name: pypi
12
+ #
13
+ # Then: `git tag v1.0.0 && git push origin v1.0.0` publishes automatically.
14
+
15
+ on:
16
+ push:
17
+ tags:
18
+ - "v*"
19
+
20
+ permissions:
21
+ contents: read
22
+
23
+ jobs:
24
+ build:
25
+ runs-on: ubuntu-latest
26
+ steps:
27
+ - uses: actions/checkout@v4
28
+ - uses: actions/setup-python@v5
29
+ with:
30
+ python-version: "3.12"
31
+ - name: Build sdist and wheel
32
+ run: |
33
+ python -m pip install --upgrade build twine
34
+ python -m build
35
+ python -m twine check dist/*
36
+ - uses: actions/upload-artifact@v4
37
+ with:
38
+ name: dist
39
+ path: dist/
40
+
41
+ publish:
42
+ needs: build
43
+ runs-on: ubuntu-latest
44
+ environment: pypi
45
+ permissions:
46
+ id-token: write # required for OIDC trusted publishing
47
+ steps:
48
+ - uses: actions/download-artifact@v4
49
+ with:
50
+ name: dist
51
+ path: dist/
52
+ - name: Publish to PyPI
53
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,27 @@
1
+ # macOS
2
+ .DS_Store
3
+
4
+ # Secrets
5
+ .env
6
+
7
+ # Python
8
+ __pycache__/
9
+ *.py[cod]
10
+ *.egg-info/
11
+ .eggs/
12
+ build/
13
+ dist/
14
+ .venv/
15
+ venv/
16
+ env/
17
+
18
+ # Tooling
19
+ .pytest_cache/
20
+ .mypy_cache/
21
+ .ruff_cache/
22
+
23
+ # Local design docs / plans (internal); shipped docs are re-included below
24
+ docs/*
25
+ !docs/playground.md
26
+ !docs/install.md
27
+ !docs/tools.md
@@ -0,0 +1,50 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Commands
6
+
7
+ ```bash
8
+ # Install with dev dependencies
9
+ pip install -e ".[dev]"
10
+
11
+ # Run tests (no live Odoo needed)
12
+ pytest
13
+
14
+ # Run a single test file
15
+ pytest tests/test_client.py
16
+
17
+ # Run a single test by name
18
+ pytest tests/test_tools_write.py::test_create_record_preview
19
+
20
+ # Live smoke test against a real Odoo instance (reads only, never writes)
21
+ python scripts/smoke_live.py
22
+ ```
23
+
24
+ ## Architecture
25
+
26
+ This is an MCP server that exposes Odoo's XML-RPC external API as MCP tools. The entry point is `odoo_pulse/server.py`, which imports all tool modules as side effects — each import registers `@mcp.tool()` functions with the shared FastMCP instance.
27
+
28
+ **Module responsibilities:**
29
+
30
+ - `runtime.py` — shared singleton: holds the `mcp` (FastMCP) instance, the lazy `OdooClient` (created on first tool call), and shared helpers used by every tool: `safe()` (runs a lambda and serialises result/error to JSON), `name_domain()`, `date_domain()`, `preview()` (dry-run struct).
31
+ - `odoo_client.py` — thin XML-RPC wrapper. `OdooConfig.from_env()` reads all env vars. `OdooClient._check_write()` enforces the four-layer write safety guard. All XML-RPC faults become `OdooError`. `fields_get` results are cached via a process-local TTL+LRU cache (`cache.py`); `aggregate_records` dispatches between `read_group` (Odoo ≤18) and `formatted_read_group` (19+) based on `major_version()`.
32
+ - `tools_generic.py` — model-agnostic tools: `search_read`, `search_count`, `read_records`, `get_model_fields`, `list_models`, `odoo_version`, `aggregate_records`, `read_attachment`.
33
+ - `tools_write.py` — write tools (`create_record`, `update_records`, `delete_records`) plus domain-specific helpers (`create_lead`, `create_contact`, `create_task`, `confirm_sale_order`). Every tool returns a dry-run preview unless `confirm=True`.
34
+ - `workflow_helpers.py` — shared building blocks for composed workflow tools: `today_in_tz`, `parse_deadline`, archived-aware `resolve_user_names`, and `build_report` (the standard report envelope: `tool`, `as_of`, tool-specific keys, `summary`, `breakdown`, `highlights`, `risks`). Used by `tools_workflows.py` and `standup_digest`.
35
+ - `tools_workflows.py` — composed, opinionated workflow tools that answer a business question in one call (e.g. `sprint_health`, `team_workload`, `project_status_report`, `standup_digest`). Read-only; compose `search_read`/aggregates server-side and return the `build_report` envelope.
36
+ - `tools_reports.py` — cross-department report tools (`pipeline_review`, `sales_snapshot`, `receivables_health`, `inventory_risk`, `absence_overview`, `business_pulse`). Same envelope and composition style as `tools_workflows.py`.
37
+ - `tool_groups.py` — maps `ODOO_TOOL_GROUPS` (default `core,reports`) to the tool modules `server.py` imports; unknown group names fail at startup.
38
+ - `domain_tools.py`, `tools_hr.py`, `tools_projects.py`, `tools_operations.py`, `tools_engagement.py`, `tools_niche.py` — domain-specific read tools wrapping `search_read` with hard-coded fields and domains for common Odoo models.
39
+
40
+ **Write safety chain** (all four must pass for any write to execute):
41
+ 1. `ODOO_READ_ONLY=false` — master switch (default: `true`, blocking all writes)
42
+ 2. `ODOO_WRITABLE_MODELS` — comma-separated allow-list; model must be in it
43
+ 3. `ODOO_ALLOW_DELETE=true` — required for `delete_records` (default: `false`)
44
+ 4. `confirm=True` on the tool call — all write tools return a preview struct by default
45
+
46
+ System models (`ir.*`, `base*`, `res.users`, `res.groups`, etc.) are permanently blocked regardless of `ODOO_WRITABLE_MODELS`.
47
+
48
+ **Testing pattern:** Tests inject a `FakeClient` directly into `runtime._client` (see `conftest.py`). The fake records every call in `fake_client.calls` and returns canned data from `search_responses`/`read_responses` dicts. No real Odoo or network is needed. Tests assert on the model name and domain that a tool built, not on Odoo's actual response.
49
+
50
+ **Adding a new tool module:** Create `odoo_pulse/tools_foo.py`, import `mcp` and `get_client` from `.runtime`, decorate functions with `@mcp.tool()`, then add the module to a group in `tool_groups.GROUP_MODULES` (server.py imports modules per enabled group).
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Minh Hong
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,13 @@
1
+ COMPOSE = docker compose -f docker-compose.playground.yml
2
+
3
+ .PHONY: playground playground-reset playground-smoke
4
+
5
+ playground: ## Boot Odoo + seed the demo story
6
+ $(COMPOSE) up -d
7
+ $(COMPOSE) logs -f seed
8
+
9
+ playground-reset: ## Wipe the playground (drops the database)
10
+ $(COMPOSE) down -v
11
+
12
+ playground-smoke: ## End-to-end: boot, seed, assert reports, tear down
13
+ ./scripts/playground_smoke.sh
@@ -0,0 +1,120 @@
1
+ Metadata-Version: 2.4
2
+ Name: odoo-pulse
3
+ Version: 1.0.0
4
+ Summary: AI business analyst for Odoo ERP — one-call reports with verdicts, over MCP
5
+ Project-URL: Homepage, https://github.com/minhhq-a1/odoo-pulse
6
+ Project-URL: Repository, https://github.com/minhhq-a1/odoo-pulse
7
+ Project-URL: Issues, https://github.com/minhhq-a1/odoo-pulse/issues
8
+ Author-email: Minh Hong <minhhq@arrowhitech.com>
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: ai,analytics,business-intelligence,claude,erp,mcp,model-context-protocol,odoo,xml-rpc
12
+ Classifier: Development Status :: 5 - Production/Stable
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: Information Technology
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Topic :: Office/Business
22
+ Classifier: Topic :: Software Development :: Libraries
23
+ Requires-Python: >=3.10
24
+ Requires-Dist: mcp[cli]>=1.2.0
25
+ Requires-Dist: python-dotenv>=1.0
26
+ Provides-Extra: dev
27
+ Requires-Dist: pytest>=7.0; extra == 'dev'
28
+ Description-Content-Type: text/markdown
29
+
30
+ # odoo-pulse
31
+
32
+ [![CI](https://github.com/minhhq-a1/odoo-pulse/actions/workflows/ci.yml/badge.svg)](https://github.com/minhhq-a1/odoo-pulse/actions/workflows/ci.yml)
33
+ [![PyPI](https://img.shields.io/pypi/v/odoo-pulse)](https://pypi.org/project/odoo-pulse/)
34
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
35
+
36
+ **An AI business analyst for your [Odoo](https://www.odoo.com) ERP.** Ask one
37
+ question, get one answer — numbers, highlights, risks, and a verdict
38
+ (on-track / at-risk / off-track) — over the [Model Context
39
+ Protocol](https://modelcontextprotocol.io). CRUD bridges to Odoo already exist;
40
+ this is the analytics layer that sits on top.
41
+
42
+ ![business_pulse — a one-call company briefing](assets/business_pulse.svg)
43
+
44
+ ## The analyst tools
45
+
46
+ Each tool answers a whole management question in a single call, returning a
47
+ structured report with a verdict — not a raw dump you have to interpret.
48
+
49
+ | Tool | Answers |
50
+ | --- | --- |
51
+ | `business_pulse` ⭐ | The morning briefing: yesterday's sales, new leads, overdue invoices, late tasks, who's off — with a company-wide verdict |
52
+ | `pipeline_review` | CRM funnel by stage, stalled deals, weighted revenue, recent win rate |
53
+ | `sales_snapshot` | Revenue this period vs last (Δ%), top customers/products, stale quotations |
54
+ | `receivables_health` | AR/AP aging buckets, % overdue, top debtors |
55
+ | `inventory_risk` | Shortages (negative forecast) and dead stock |
56
+ | `absence_overview` | Who's off this week, pending approvals, thin-coverage departments |
57
+ | `sprint_health` · `team_workload` · `project_status_report` | Project delivery: completion, overloaded members, at-risk projects |
58
+
59
+ Under the hood it's the standard Odoo XML-RPC external API — nothing to install
60
+ inside Odoo, works on Odoo Online, Odoo.sh, and on-premise.
61
+
62
+ ## Try it in 5 minutes
63
+
64
+ No Odoo account? Boot a demo Odoo pre-seeded with a story to tell (a stalled
65
+ deal, a 90-day-overdue invoice, a stock shortage, someone off today):
66
+
67
+ ```bash
68
+ docker compose -f docker-compose.playground.yml up -d
69
+ ```
70
+
71
+ Then point Claude at it and ask it to **`run business_pulse`**. Full
72
+ walkthrough: [docs/playground.md](docs/playground.md).
73
+
74
+ ## Install & connect
75
+
76
+ Add it to Claude Code (no install step — `uvx` fetches it):
77
+
78
+ ```bash
79
+ claude mcp add odoo-pulse \
80
+ --env ODOO_URL=https://acme.odoo.com \
81
+ --env ODOO_DB=acme \
82
+ --env ODOO_USERNAME=you@example.com \
83
+ --env ODOO_API_KEY=your-api-key \
84
+ --env ODOO_READ_ONLY=true \
85
+ -- uvx odoo-pulse
86
+ ```
87
+
88
+ Generate the API key in Odoo under **Settings → Users → (your user) → Account
89
+ Security → New API Key**. Config for **Claude Desktop** and **Cursor**, plus pip
90
+ and Docker alternatives: [docs/install.md](docs/install.md).
91
+
92
+ ## Read-only by default, safe writes when you want them
93
+
94
+ The server is read-only out of the box. Writes require four independent controls
95
+ to line up (`ODOO_READ_ONLY=false`, a model allow-list, a delete flag, and a
96
+ per-call `confirm=true` after a dry-run preview); system models are never
97
+ writable. Details: [docs/tools.md#write-operations](docs/tools.md#write-operations).
98
+
99
+ ## More tools
100
+
101
+ Beyond the analyst reports, there are ~60 model-aware query tools spanning CRM,
102
+ Sales, Inventory, Accounting, HR, Project, Manufacturing, PoS, and Enterprise
103
+ apps — opt in via `ODOO_TOOL_GROUPS`. Full catalogue and configuration:
104
+ [docs/tools.md](docs/tools.md).
105
+
106
+ ## Testing
107
+
108
+ The suite mocks the XML-RPC layer, so **no real Odoo or network is needed**:
109
+
110
+ ```bash
111
+ pip install -e ".[dev]"
112
+ pytest
113
+ ```
114
+
115
+ For a live check against a real Odoo (read-only), see
116
+ [docs/tools.md#live-smoke-test-against-a-real-odoo](docs/tools.md#live-smoke-test-against-a-real-odoo).
117
+
118
+ ## License
119
+
120
+ [MIT](LICENSE)
@@ -0,0 +1,91 @@
1
+ # odoo-pulse
2
+
3
+ [![CI](https://github.com/minhhq-a1/odoo-pulse/actions/workflows/ci.yml/badge.svg)](https://github.com/minhhq-a1/odoo-pulse/actions/workflows/ci.yml)
4
+ [![PyPI](https://img.shields.io/pypi/v/odoo-pulse)](https://pypi.org/project/odoo-pulse/)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
6
+
7
+ **An AI business analyst for your [Odoo](https://www.odoo.com) ERP.** Ask one
8
+ question, get one answer — numbers, highlights, risks, and a verdict
9
+ (on-track / at-risk / off-track) — over the [Model Context
10
+ Protocol](https://modelcontextprotocol.io). CRUD bridges to Odoo already exist;
11
+ this is the analytics layer that sits on top.
12
+
13
+ ![business_pulse — a one-call company briefing](assets/business_pulse.svg)
14
+
15
+ ## The analyst tools
16
+
17
+ Each tool answers a whole management question in a single call, returning a
18
+ structured report with a verdict — not a raw dump you have to interpret.
19
+
20
+ | Tool | Answers |
21
+ | --- | --- |
22
+ | `business_pulse` ⭐ | The morning briefing: yesterday's sales, new leads, overdue invoices, late tasks, who's off — with a company-wide verdict |
23
+ | `pipeline_review` | CRM funnel by stage, stalled deals, weighted revenue, recent win rate |
24
+ | `sales_snapshot` | Revenue this period vs last (Δ%), top customers/products, stale quotations |
25
+ | `receivables_health` | AR/AP aging buckets, % overdue, top debtors |
26
+ | `inventory_risk` | Shortages (negative forecast) and dead stock |
27
+ | `absence_overview` | Who's off this week, pending approvals, thin-coverage departments |
28
+ | `sprint_health` · `team_workload` · `project_status_report` | Project delivery: completion, overloaded members, at-risk projects |
29
+
30
+ Under the hood it's the standard Odoo XML-RPC external API — nothing to install
31
+ inside Odoo, works on Odoo Online, Odoo.sh, and on-premise.
32
+
33
+ ## Try it in 5 minutes
34
+
35
+ No Odoo account? Boot a demo Odoo pre-seeded with a story to tell (a stalled
36
+ deal, a 90-day-overdue invoice, a stock shortage, someone off today):
37
+
38
+ ```bash
39
+ docker compose -f docker-compose.playground.yml up -d
40
+ ```
41
+
42
+ Then point Claude at it and ask it to **`run business_pulse`**. Full
43
+ walkthrough: [docs/playground.md](docs/playground.md).
44
+
45
+ ## Install & connect
46
+
47
+ Add it to Claude Code (no install step — `uvx` fetches it):
48
+
49
+ ```bash
50
+ claude mcp add odoo-pulse \
51
+ --env ODOO_URL=https://acme.odoo.com \
52
+ --env ODOO_DB=acme \
53
+ --env ODOO_USERNAME=you@example.com \
54
+ --env ODOO_API_KEY=your-api-key \
55
+ --env ODOO_READ_ONLY=true \
56
+ -- uvx odoo-pulse
57
+ ```
58
+
59
+ Generate the API key in Odoo under **Settings → Users → (your user) → Account
60
+ Security → New API Key**. Config for **Claude Desktop** and **Cursor**, plus pip
61
+ and Docker alternatives: [docs/install.md](docs/install.md).
62
+
63
+ ## Read-only by default, safe writes when you want them
64
+
65
+ The server is read-only out of the box. Writes require four independent controls
66
+ to line up (`ODOO_READ_ONLY=false`, a model allow-list, a delete flag, and a
67
+ per-call `confirm=true` after a dry-run preview); system models are never
68
+ writable. Details: [docs/tools.md#write-operations](docs/tools.md#write-operations).
69
+
70
+ ## More tools
71
+
72
+ Beyond the analyst reports, there are ~60 model-aware query tools spanning CRM,
73
+ Sales, Inventory, Accounting, HR, Project, Manufacturing, PoS, and Enterprise
74
+ apps — opt in via `ODOO_TOOL_GROUPS`. Full catalogue and configuration:
75
+ [docs/tools.md](docs/tools.md).
76
+
77
+ ## Testing
78
+
79
+ The suite mocks the XML-RPC layer, so **no real Odoo or network is needed**:
80
+
81
+ ```bash
82
+ pip install -e ".[dev]"
83
+ pytest
84
+ ```
85
+
86
+ For a live check against a real Odoo (read-only), see
87
+ [docs/tools.md#live-smoke-test-against-a-real-odoo](docs/tools.md#live-smoke-test-against-a-real-odoo).
88
+
89
+ ## License
90
+
91
+ [MIT](LICENSE)
@@ -0,0 +1,137 @@
1
+ <svg class="rich-terminal" viewBox="0 0 970 489.2" xmlns="http://www.w3.org/2000/svg">
2
+ <!-- Generated with Rich https://www.textualize.io -->
3
+ <style>
4
+
5
+ @font-face {
6
+ font-family: "Fira Code";
7
+ src: local("FiraCode-Regular"),
8
+ url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Regular.woff2") format("woff2"),
9
+ url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Regular.woff") format("woff");
10
+ font-style: normal;
11
+ font-weight: 400;
12
+ }
13
+ @font-face {
14
+ font-family: "Fira Code";
15
+ src: local("FiraCode-Bold"),
16
+ url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Bold.woff2") format("woff2"),
17
+ url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Bold.woff") format("woff");
18
+ font-style: bold;
19
+ font-weight: 700;
20
+ }
21
+
22
+ .terminal-571639065-matrix {
23
+ font-family: Fira Code, monospace;
24
+ font-size: 20px;
25
+ line-height: 24.4px;
26
+ font-variant-east-asian: full-width;
27
+ }
28
+
29
+ .terminal-571639065-title {
30
+ font-size: 18px;
31
+ font-weight: bold;
32
+ font-family: arial;
33
+ }
34
+
35
+ .terminal-571639065-r1 { fill: #c5c8c6 }
36
+ .terminal-571639065-r2 { fill: #6b7280 }
37
+ .terminal-571639065-r3 { fill: #6b7280;font-weight: bold }
38
+ .terminal-571639065-r4 { fill: #d1d5db }
39
+ .terminal-571639065-r5 { fill: #8b5cf6 }
40
+ .terminal-571639065-r6 { fill: #8b5cf6;font-weight: bold }
41
+ .terminal-571639065-r7 { fill: #e5e7eb;font-weight: bold }
42
+ .terminal-571639065-r8 { fill: #9ca3af }
43
+ .terminal-571639065-r9 { fill: #22c55e }
44
+ .terminal-571639065-r10 { fill: #e5e7eb }
45
+ .terminal-571639065-r11 { fill: #f59e0b;font-weight: bold }
46
+ .terminal-571639065-r12 { fill: #f59e0b }
47
+ </style>
48
+
49
+ <defs>
50
+ <clipPath id="terminal-571639065-clip-terminal">
51
+ <rect x="0" y="0" width="950.5999999999999" height="438.2" />
52
+ </clipPath>
53
+ <clipPath id="terminal-571639065-line-0">
54
+ <rect x="0" y="1.5" width="951.6" height="24.65"/>
55
+ </clipPath>
56
+ <clipPath id="terminal-571639065-line-1">
57
+ <rect x="0" y="25.9" width="951.6" height="24.65"/>
58
+ </clipPath>
59
+ <clipPath id="terminal-571639065-line-2">
60
+ <rect x="0" y="50.3" width="951.6" height="24.65"/>
61
+ </clipPath>
62
+ <clipPath id="terminal-571639065-line-3">
63
+ <rect x="0" y="74.7" width="951.6" height="24.65"/>
64
+ </clipPath>
65
+ <clipPath id="terminal-571639065-line-4">
66
+ <rect x="0" y="99.1" width="951.6" height="24.65"/>
67
+ </clipPath>
68
+ <clipPath id="terminal-571639065-line-5">
69
+ <rect x="0" y="123.5" width="951.6" height="24.65"/>
70
+ </clipPath>
71
+ <clipPath id="terminal-571639065-line-6">
72
+ <rect x="0" y="147.9" width="951.6" height="24.65"/>
73
+ </clipPath>
74
+ <clipPath id="terminal-571639065-line-7">
75
+ <rect x="0" y="172.3" width="951.6" height="24.65"/>
76
+ </clipPath>
77
+ <clipPath id="terminal-571639065-line-8">
78
+ <rect x="0" y="196.7" width="951.6" height="24.65"/>
79
+ </clipPath>
80
+ <clipPath id="terminal-571639065-line-9">
81
+ <rect x="0" y="221.1" width="951.6" height="24.65"/>
82
+ </clipPath>
83
+ <clipPath id="terminal-571639065-line-10">
84
+ <rect x="0" y="245.5" width="951.6" height="24.65"/>
85
+ </clipPath>
86
+ <clipPath id="terminal-571639065-line-11">
87
+ <rect x="0" y="269.9" width="951.6" height="24.65"/>
88
+ </clipPath>
89
+ <clipPath id="terminal-571639065-line-12">
90
+ <rect x="0" y="294.3" width="951.6" height="24.65"/>
91
+ </clipPath>
92
+ <clipPath id="terminal-571639065-line-13">
93
+ <rect x="0" y="318.7" width="951.6" height="24.65"/>
94
+ </clipPath>
95
+ <clipPath id="terminal-571639065-line-14">
96
+ <rect x="0" y="343.1" width="951.6" height="24.65"/>
97
+ </clipPath>
98
+ <clipPath id="terminal-571639065-line-15">
99
+ <rect x="0" y="367.5" width="951.6" height="24.65"/>
100
+ </clipPath>
101
+ <clipPath id="terminal-571639065-line-16">
102
+ <rect x="0" y="391.9" width="951.6" height="24.65"/>
103
+ </clipPath>
104
+ </defs>
105
+
106
+ <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="968" height="487.2" rx="8"/><text class="terminal-571639065-title" fill="#c5c8c6" text-anchor="middle" x="484" y="27">odoo-pulse&#160;·&#160;business_pulse</text>
107
+ <g transform="translate(26,22)">
108
+ <circle cx="0" cy="0" r="7" fill="#ff5f57"/>
109
+ <circle cx="22" cy="0" r="7" fill="#febc2e"/>
110
+ <circle cx="44" cy="0" r="7" fill="#28c840"/>
111
+ </g>
112
+
113
+ <g transform="translate(9, 41)" clip-path="url(#terminal-571639065-clip-terminal)">
114
+
115
+ <g class="terminal-571639065-matrix">
116
+ <text class="terminal-571639065-r1" x="951.6" y="20" textLength="12.2" clip-path="url(#terminal-571639065-line-0)">
117
+ </text><text class="terminal-571639065-r2" x="12.2" y="44.4" textLength="24.4" clip-path="url(#terminal-571639065-line-1)">▎&#160;</text><text class="terminal-571639065-r3" x="36.6" y="44.4" textLength="36.6" clip-path="url(#terminal-571639065-line-1)">You</text><text class="terminal-571639065-r1" x="951.6" y="44.4" textLength="12.2" clip-path="url(#terminal-571639065-line-1)">
118
+ </text><text class="terminal-571639065-r4" x="12.2" y="68.8" textLength="646.6" clip-path="url(#terminal-571639065-line-2)">&#160;&#160;Run&#160;business_pulse&#160;—&#160;how&#x27;s&#160;the&#160;company&#160;doing&#160;today?</text><text class="terminal-571639065-r1" x="951.6" y="68.8" textLength="12.2" clip-path="url(#terminal-571639065-line-2)">
119
+ </text><text class="terminal-571639065-r1" x="951.6" y="93.2" textLength="12.2" clip-path="url(#terminal-571639065-line-3)">
120
+ </text><text class="terminal-571639065-r5" x="12.2" y="117.6" textLength="24.4" clip-path="url(#terminal-571639065-line-4)">▎&#160;</text><text class="terminal-571639065-r6" x="36.6" y="117.6" textLength="73.2" clip-path="url(#terminal-571639065-line-4)">Claude</text><text class="terminal-571639065-r1" x="951.6" y="117.6" textLength="12.2" clip-path="url(#terminal-571639065-line-4)">
121
+ </text><text class="terminal-571639065-r1" x="951.6" y="142" textLength="12.2" clip-path="url(#terminal-571639065-line-5)">
122
+ </text><text class="terminal-571639065-r7" x="12.2" y="166.4" textLength="219.6" clip-path="url(#terminal-571639065-line-6)">&#160;&#160;📊&#160;Business&#160;Pulse</text><text class="terminal-571639065-r8" x="244" y="166.4" textLength="256.2" clip-path="url(#terminal-571639065-line-6)">&#160;&#160;·&#160;&#160;as&#160;of&#160;2026-07-03</text><text class="terminal-571639065-r1" x="951.6" y="166.4" textLength="12.2" clip-path="url(#terminal-571639065-line-6)">
123
+ </text><text class="terminal-571639065-r1" x="951.6" y="190.8" textLength="12.2" clip-path="url(#terminal-571639065-line-7)">
124
+ </text><text class="terminal-571639065-r9" x="12.2" y="215.2" textLength="48.8" clip-path="url(#terminal-571639065-line-8)">&#160;&#160;•&#160;</text><text class="terminal-571639065-r10" x="61" y="215.2" textLength="451.4" clip-path="url(#terminal-571639065-line-8)">yesterday:&#160;1&#160;order(s),&#160;revenue&#160;6900.0</text><text class="terminal-571639065-r1" x="951.6" y="215.2" textLength="12.2" clip-path="url(#terminal-571639065-line-8)">
125
+ </text><text class="terminal-571639065-r9" x="12.2" y="239.6" textLength="48.8" clip-path="url(#terminal-571639065-line-9)">&#160;&#160;•&#160;</text><text class="terminal-571639065-r10" x="61" y="239.6" textLength="280.6" clip-path="url(#terminal-571639065-line-9)">1&#160;new&#160;lead(s)&#160;yesterday</text><text class="terminal-571639065-r1" x="951.6" y="239.6" textLength="12.2" clip-path="url(#terminal-571639065-line-9)">
126
+ </text><text class="terminal-571639065-r9" x="12.2" y="264" textLength="48.8" clip-path="url(#terminal-571639065-line-10)">&#160;&#160;•&#160;</text><text class="terminal-571639065-r10" x="61" y="264" textLength="219.6" clip-path="url(#terminal-571639065-line-10)">2&#160;people&#160;off&#160;today</text><text class="terminal-571639065-r1" x="951.6" y="264" textLength="12.2" clip-path="url(#terminal-571639065-line-10)">
127
+ </text><text class="terminal-571639065-r1" x="951.6" y="288.4" textLength="12.2" clip-path="url(#terminal-571639065-line-11)">
128
+ </text><text class="terminal-571639065-r11" x="12.2" y="312.8" textLength="207.4" clip-path="url(#terminal-571639065-line-12)">&#160;&#160;Needs&#160;attention</text><text class="terminal-571639065-r1" x="951.6" y="312.8" textLength="12.2" clip-path="url(#terminal-571639065-line-12)">
129
+ </text><text class="terminal-571639065-r12" x="12.2" y="337.2" textLength="48.8" clip-path="url(#terminal-571639065-line-13)">&#160;&#160;⚠&#160;</text><text class="terminal-571639065-r10" x="61" y="337.2" textLength="610" clip-path="url(#terminal-571639065-line-13)">2&#160;customer&#160;invoice(s)&#160;overdue,&#160;13225.0&#160;outstanding</text><text class="terminal-571639065-r1" x="951.6" y="337.2" textLength="12.2" clip-path="url(#terminal-571639065-line-13)">
130
+ </text><text class="terminal-571639065-r12" x="12.2" y="361.6" textLength="48.8" clip-path="url(#terminal-571639065-line-14)">&#160;&#160;⚠&#160;</text><text class="terminal-571639065-r10" x="61" y="361.6" textLength="280.6" clip-path="url(#terminal-571639065-line-14)">2&#160;task(s)&#160;past&#160;deadline</text><text class="terminal-571639065-r1" x="951.6" y="361.6" textLength="12.2" clip-path="url(#terminal-571639065-line-14)">
131
+ </text><text class="terminal-571639065-r1" x="951.6" y="386" textLength="12.2" clip-path="url(#terminal-571639065-line-15)">
132
+ </text><text class="terminal-571639065-r8" x="12.2" y="410.4" textLength="134.2" clip-path="url(#terminal-571639065-line-16)">&#160;&#160;Verdict:&#160;</text><text class="terminal-571639065-r11" x="146.4" y="410.4" textLength="134.2" clip-path="url(#terminal-571639065-line-16)">⚠&#160;ATTENTION</text><text class="terminal-571639065-r1" x="951.6" y="410.4" textLength="12.2" clip-path="url(#terminal-571639065-line-16)">
133
+ </text><text class="terminal-571639065-r1" x="951.6" y="434.8" textLength="12.2" clip-path="url(#terminal-571639065-line-17)">
134
+ </text>
135
+ </g>
136
+ </g>
137
+ </svg>
Binary file