Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added
- **Multi-Language Support**: Added `ODOO_LOCALE` environment variable to request Odoo responses in different languages
- Automatically injects language context into all Odoo API calls
- Supports any locale installed in your Odoo instance (es_ES, es_AR, fr_FR, etc.)
- Returns translated field labels, selection values, and error messages
- Preserves existing context values when locale is added

## [0.3.0] - 2025-09-14

### Added
Expand Down
58 changes: 57 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,26 @@ Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
"env": {
"ODOO_URL": "https://your-odoo-instance.com",
"ODOO_API_KEY": "your-api-key-here",
"ODOO_DB": "your-database-name"
"ODOO_DB": "your-database-name",
"ODOO_LOCALE": "es_ES"
}
}
}
}
```

**For Spanish (Argentina):**
```json
{
"mcpServers": {
"odoo": {
"command": "uvx",
"args": ["mcp-server-odoo"],
"env": {
"ODOO_URL": "https://your-odoo-instance.com",
"ODOO_API_KEY": "your-api-key-here",
"ODOO_DB": "your-database-name",
"ODOO_LOCALE": "es_AR"
}
}
}
Expand Down Expand Up @@ -205,13 +224,15 @@ The server requires the following environment variables:
| `ODOO_USER` | Yes* | Username (if not using API key) | `admin` |
| `ODOO_PASSWORD` | Yes* | Password (if not using API key) | `admin` |
| `ODOO_DB` | No | Database name (auto-detected if not set) | `mycompany` |
| `ODOO_LOCALE` | No | Language/locale for Odoo responses | `es_ES`, `es_AR`, `en_US`, `fr_FR` |
| `ODOO_YOLO` | No | YOLO mode - bypasses MCP security (⚠️ DEV ONLY) | `off`, `read`, `true` |

*Either `ODOO_API_KEY` or both `ODOO_USER` and `ODOO_PASSWORD` are required.

**Notes:**
- If database listing is restricted on your server, you must specify `ODOO_DB`
- API key authentication is recommended for better security
- When `ODOO_LOCALE` is set, Odoo will return translated field labels, selection values, and messages in that language

### Transport Options

Expand Down Expand Up @@ -397,6 +418,41 @@ Once configured, you can ask Claude:
- "Change the status of order SO/2024/001 to confirmed"
- "Delete the test contact we created earlier"

### Multi-Language Support

When you configure `ODOO_LOCALE`, Odoo will return all data in that language:

**With `ODOO_LOCALE=es_ES` (Spanish):**
```
# Field labels will be in Spanish
Cliente: Acme Corporation
Correo electrónico: info@acme.com
Estado: Borrador

# Selection values will be in Spanish
Tipo: Cliente
País: España
```

**With `ODOO_LOCALE=es_AR` (Argentine Spanish):**
```
# Uses Argentine terminology and formats
Razón Social: La Valeria S.A.
CUIT: 30-12345678-9
Provincia: Buenos Aires
```

**Supported locales include:**
- `es_ES` - Spanish (Spain)
- `es_AR` - Spanish (Argentina)
- `es_MX` - Spanish (Mexico)
- `en_US` - English (United States)
- `en_GB` - English (United Kingdom)
- `fr_FR` - French (France)
- `pt_BR` - Portuguese (Brazil)
- `de_DE` - German (Germany)
- Any other locale installed in your Odoo instance

## Available Tools

### `search_records`
Expand Down
2 changes: 2 additions & 0 deletions mcp_server_odoo/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ class OdooConfig:
default_limit: int = 10
max_limit: int = 100
max_smart_fields: int = 15
locale: Optional[str] = None

# MCP transport configuration
transport: Literal["stdio", "streamable-http"] = "stdio"
Expand Down Expand Up @@ -216,6 +217,7 @@ def get_yolo_mode() -> str:
username=os.getenv("ODOO_USER", "").strip() or None,
password=os.getenv("ODOO_PASSWORD", "").strip() or None,
database=os.getenv("ODOO_DB", "").strip() or None,
locale=os.getenv("ODOO_LOCALE", "").strip() or None,
log_level=os.getenv("ODOO_MCP_LOG_LEVEL", "INFO").strip(),
default_limit=get_int_env("ODOO_MCP_DEFAULT_LIMIT", 10),
max_limit=get_int_env("ODOO_MCP_MAX_LIMIT", 100),
Expand Down
8 changes: 8 additions & 0 deletions mcp_server_odoo/odoo_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -835,6 +835,14 @@ def execute_kw(self, model: str, method: str, args: List[Any], kwargs: Dict[str,
self.config.api_key if self._auth_method == "api_key" else self.config.password
)

# Inject locale into context if configured
if self.config.locale:
if "context" not in kwargs:
kwargs["context"] = {}
# Set language in Odoo context
kwargs["context"]["lang"] = self.config.locale
logger.info(f"🌍 Locale injected: {self.config.locale}")

try:
# Log the operation
logger.debug(f"Executing {method} on {model} with args={args}, kwargs={kwargs}")
Expand Down
18 changes: 10 additions & 8 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,18 @@ requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "mcp-server-odoo"
name = "mcp-server-odoo-ei"
version = "0.3.0"
description = "A Model Context Protocol server for Odoo ERP systems"
readme = "README.md"
requires-python = ">=3.10"
authors = [
{ name = "Andrey Ivanov", email = "ivnv.xd@gmail.com" }
{ name = "Andrey Ivanov", email = "ivnv.xd@gmail.com" },
{ name = "Luciano Bustos", email = "luciano.bustos@elevateinformatics.com" }
]
maintainers = [
{ name = "Andrey Ivanov", email = "ivnv.xd@gmail.com" }
{ name = "Andrey Ivanov", email = "ivnv.xd@gmail.com" },
{ name = "Luciano Bustos", email = "luciano.bustos@elevateinformatics.com" }
]
license = { text = "MPL-2.0" }
keywords = ["mcp", "odoo", "erp", "model-context-protocol", "ai", "llm", "claude", "anthropic"]
Expand All @@ -38,11 +40,11 @@ dependencies = [
]

[project.urls]
Homepage = "https://github.com/ivnvxd/mcp-server-odoo"
Documentation = "https://github.com/ivnvxd/mcp-server-odoo#readme"
Repository = "https://github.com/ivnvxd/mcp-server-odoo"
Issues = "https://github.com/ivnvxd/mcp-server-odoo/issues"
Changelog = "https://github.com/ivnvxd/mcp-server-odoo/releases"
Homepage = "https://github.com/elevateinformatics/mcp-server-odoo"
Documentation = "https://github.com/elevateinformatics/mcp-server-odoo#readme"
Repository = "https://github.com/elevateinformatics/mcp-server-odoo"
Issues = "https://github.com/elevateinformatics/mcp-server-odoo/issues"
Changelog = "https://github.com/elevateinformatics/mcp-server-odoo/releases"

[project.optional-dependencies]
dev = [
Expand Down
195 changes: 195 additions & 0 deletions tests/test_locale.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
"""Tests for locale support in Odoo MCP Server.

This module tests that locale configuration is properly applied
to Odoo API calls, allowing responses in different languages.
"""

from unittest.mock import MagicMock, patch

import pytest

from mcp_server_odoo.config import OdooConfig
from mcp_server_odoo.odoo_connection import OdooConnection


class TestLocaleSupport:
"""Test locale/language support."""

@pytest.fixture
def config_with_locale(self):
"""Create test configuration with Spanish locale."""
return OdooConfig(
url="https://test.odoo.com",
api_key="test_key",
username="test",
database="test_db",
locale="es_ES",
yolo_mode="true", # Use YOLO mode for testing
)

@pytest.fixture
def config_with_ar_locale(self):
"""Create test configuration with Argentine Spanish locale."""
return OdooConfig(
url="https://test.odoo.com",
api_key="test_key",
username="test",
database="test_db",
locale="es_AR",
yolo_mode="true",
)

@pytest.fixture
def config_without_locale(self):
"""Create test configuration without locale."""
return OdooConfig(
url="https://test.odoo.com",
api_key="test_key",
username="test",
database="test_db",
yolo_mode="true",
)

def test_locale_injected_in_execute_kw(self, config_with_locale):
"""Test that locale is injected into context when executing operations."""
conn = OdooConnection(config_with_locale)

# Mock the connection
conn._connected = True
conn._authenticated = True
conn._uid = 1
conn._database = "test_db"
conn._auth_method = "api_key"

# Mock the object proxy
mock_proxy = MagicMock()
mock_proxy.execute_kw.return_value = [{"id": 1, "name": "Test"}]
conn._object_proxy = mock_proxy

# Execute a search operation
kwargs = {}
conn.execute_kw("res.partner", "search_read", [[]], kwargs)

# Verify that locale was injected into context
# execute_kw is called with: (database, uid, password, model, method, args, kwargs)
call_args = mock_proxy.execute_kw.call_args
passed_kwargs = call_args[0][6] # kwargs is the 7th positional argument (index 6)

assert "context" in passed_kwargs
assert "lang" in passed_kwargs["context"]
assert passed_kwargs["context"]["lang"] == "es_ES"

def test_argentine_locale_injected(self, config_with_ar_locale):
"""Test that Argentine Spanish locale is properly injected."""
conn = OdooConnection(config_with_ar_locale)

conn._connected = True
conn._authenticated = True
conn._uid = 1
conn._database = "test_db"
conn._auth_method = "api_key"

mock_proxy = MagicMock()
mock_proxy.execute_kw.return_value = []
conn._object_proxy = mock_proxy

kwargs = {}
conn.execute_kw("res.partner", "search", [[]], kwargs)

call_args = mock_proxy.execute_kw.call_args
passed_kwargs = call_args[0][6] # kwargs is the 7th positional argument

assert passed_kwargs["context"]["lang"] == "es_AR"

def test_no_locale_when_not_configured(self, config_without_locale):
"""Test that no locale is injected when not configured."""
conn = OdooConnection(config_without_locale)

conn._connected = True
conn._authenticated = True
conn._uid = 1
conn._database = "test_db"
conn._auth_method = "api_key"

mock_proxy = MagicMock()
mock_proxy.execute_kw.return_value = []
conn._object_proxy = mock_proxy

kwargs = {}
conn.execute_kw("res.partner", "search", [[]], kwargs)

call_args = mock_proxy.execute_kw.call_args
passed_kwargs = call_args[0][6] # kwargs is the 7th positional argument

# Context should not be added if it wasn't there and no locale is set
# OR if context exists, it shouldn't have 'lang'
if "context" in passed_kwargs:
assert "lang" not in passed_kwargs["context"]

def test_locale_preserves_existing_context(self, config_with_locale):
"""Test that locale injection preserves existing context values."""
conn = OdooConnection(config_with_locale)

conn._connected = True
conn._authenticated = True
conn._uid = 1
conn._database = "test_db"
conn._auth_method = "api_key"

mock_proxy = MagicMock()
mock_proxy.execute_kw.return_value = []
conn._object_proxy = mock_proxy

# Pass existing context with some values
kwargs = {"context": {"active_test": False, "tz": "America/Argentina/Buenos_Aires"}}
conn.execute_kw("res.partner", "search_read", [[]], kwargs)

call_args = mock_proxy.execute_kw.call_args
passed_kwargs = call_args[0][6] # kwargs is the 7th positional argument

# Verify existing context values are preserved
assert passed_kwargs["context"]["active_test"] is False
assert passed_kwargs["context"]["tz"] == "America/Argentina/Buenos_Aires"
# And locale was added
assert passed_kwargs["context"]["lang"] == "es_ES"

def test_locale_from_environment_variable(self):
"""Test loading locale from ODOO_LOCALE environment variable."""
with patch.dict("os.environ", {"ODOO_LOCALE": "fr_FR"}):
from mcp_server_odoo.config import load_config

with patch.dict(
"os.environ",
{
"ODOO_URL": "https://test.odoo.com",
"ODOO_USER": "test",
"ODOO_PASSWORD": "test",
"ODOO_YOLO": "true",
},
):
config = load_config()
assert config.locale == "fr_FR"

def test_common_locales_accepted(self):
"""Test that common locale codes are accepted."""
common_locales = [
"es_ES", # Spanish (Spain)
"es_AR", # Spanish (Argentina)
"es_MX", # Spanish (Mexico)
"en_US", # English (US)
"en_GB", # English (UK)
"fr_FR", # French
"pt_BR", # Portuguese (Brazil)
"de_DE", # German
"it_IT", # Italian
]

for locale_code in common_locales:
config = OdooConfig(
url="https://test.odoo.com",
api_key="test_key",
username="test",
locale=locale_code,
yolo_mode="true",
)
assert config.locale == locale_code