diff --git a/CHANGELOG.md b/CHANGELOG.md index 501a625..42cf269 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index 0a38e8d..a1f5818 100644 --- a/README.md +++ b/README.md @@ -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" } } } @@ -205,6 +224,7 @@ 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. @@ -212,6 +232,7 @@ The server requires the following environment variables: **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 @@ -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` diff --git a/mcp_server_odoo/config.py b/mcp_server_odoo/config.py index fcaf917..a4b63bb 100644 --- a/mcp_server_odoo/config.py +++ b/mcp_server_odoo/config.py @@ -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" @@ -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), diff --git a/mcp_server_odoo/odoo_connection.py b/mcp_server_odoo/odoo_connection.py index c4fd902..8fa2aa7 100644 --- a/mcp_server_odoo/odoo_connection.py +++ b/mcp_server_odoo/odoo_connection.py @@ -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}") diff --git a/pyproject.toml b/pyproject.toml index bc93824..878eb1d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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"] @@ -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 = [ diff --git a/tests/test_locale.py b/tests/test_locale.py new file mode 100644 index 0000000..0c46e85 --- /dev/null +++ b/tests/test_locale.py @@ -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