biszx-odoo-mcp 1.1.0__py3-none-any.whl → 1.1.2__py3-none-any.whl
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.
- biszx_odoo_mcp/server/__init__.py +0 -0
- biszx_odoo_mcp/server/context.py +19 -0
- biszx_odoo_mcp/server/resources.py +230 -0
- biszx_odoo_mcp/server/response.py +36 -0
- biszx_odoo_mcp/server/tools.py +432 -0
- biszx_odoo_mcp/tools/__init__.py +0 -0
- biszx_odoo_mcp/tools/config.py +98 -0
- biszx_odoo_mcp/tools/odoo_client.py +632 -0
- {biszx_odoo_mcp-1.1.0.dist-info → biszx_odoo_mcp-1.1.2.dist-info}/METADATA +1 -1
- biszx_odoo_mcp-1.1.2.dist-info/RECORD +18 -0
- biszx_odoo_mcp-1.1.0.dist-info/RECORD +0 -10
- {biszx_odoo_mcp-1.1.0.dist-info → biszx_odoo_mcp-1.1.2.dist-info}/WHEEL +0 -0
- {biszx_odoo_mcp-1.1.0.dist-info → biszx_odoo_mcp-1.1.2.dist-info}/entry_points.txt +0 -0
- {biszx_odoo_mcp-1.1.0.dist-info → biszx_odoo_mcp-1.1.2.dist-info}/licenses/LICENSE +0 -0
- {biszx_odoo_mcp-1.1.0.dist-info → biszx_odoo_mcp-1.1.2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,632 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Odoo client for interacting with Odoo via JSON-RPC using OdooRPC library.
|
|
3
|
+
|
|
4
|
+
TABLE OF CONTENTS:
|
|
5
|
+
=================
|
|
6
|
+
|
|
7
|
+
1. INITIALIZATION AND CONNECTION MANAGEMENT
|
|
8
|
+
- __init__()
|
|
9
|
+
- _connect()
|
|
10
|
+
- _ensure_connected()
|
|
11
|
+
|
|
12
|
+
2. MODEL INTROSPECTION
|
|
13
|
+
- get_models()
|
|
14
|
+
- get_model_info()
|
|
15
|
+
- get_model_fields()
|
|
16
|
+
3. SEARCH AND READ OPERATIONS
|
|
17
|
+
- search_ids()
|
|
18
|
+
- search_count()
|
|
19
|
+
- search_read()
|
|
20
|
+
- read_records()
|
|
21
|
+
|
|
22
|
+
4. CRUD OPERATIONS
|
|
23
|
+
- create_record()
|
|
24
|
+
- create_records()
|
|
25
|
+
- write_records()
|
|
26
|
+
- unlink_records()
|
|
27
|
+
- copy_record()
|
|
28
|
+
|
|
29
|
+
5. ACCESS CONTROL
|
|
30
|
+
- check_access_rights()
|
|
31
|
+
|
|
32
|
+
6. GENERIC METHOD EXECUTION
|
|
33
|
+
- execute_method()
|
|
34
|
+
- call_method()
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
import urllib.parse
|
|
38
|
+
from typing import Any, Protocol, cast
|
|
39
|
+
|
|
40
|
+
import odoorpc # type: ignore
|
|
41
|
+
from loguru import logger
|
|
42
|
+
from odoorpc.error import InternalError, RPCError # type: ignore
|
|
43
|
+
from odoorpc.rpc.error import ConnectorError # type: ignore
|
|
44
|
+
|
|
45
|
+
from biszx_odoo_mcp.exceptions import (
|
|
46
|
+
AuthenticationError,
|
|
47
|
+
ConnectionTimeoutError,
|
|
48
|
+
InternalServerError,
|
|
49
|
+
ModelNotFoundError,
|
|
50
|
+
OdooRPCError,
|
|
51
|
+
)
|
|
52
|
+
from biszx_odoo_mcp.tools.config import Config
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class OdooModelProtocol(Protocol):
|
|
56
|
+
"""Protocol defining the interface for Odoo model proxy objects"""
|
|
57
|
+
|
|
58
|
+
def search(self, domain: list[Any], **kwargs: Any) -> list[int]:
|
|
59
|
+
"""Search for record IDs"""
|
|
60
|
+
...
|
|
61
|
+
|
|
62
|
+
def search_count(self, domain: list[Any]) -> int:
|
|
63
|
+
"""Count records matching domain"""
|
|
64
|
+
...
|
|
65
|
+
|
|
66
|
+
def search_read(
|
|
67
|
+
self, domain: list[Any], fields: list[str] | None = None, **kwargs: Any
|
|
68
|
+
) -> list[dict[str, Any]]:
|
|
69
|
+
"""Search and read records"""
|
|
70
|
+
...
|
|
71
|
+
|
|
72
|
+
def browse(self, ids: list[int]) -> Any:
|
|
73
|
+
"""Browse records by IDs"""
|
|
74
|
+
...
|
|
75
|
+
|
|
76
|
+
def create(self, values: list[dict[str, Any]]) -> Any:
|
|
77
|
+
"""Create records"""
|
|
78
|
+
...
|
|
79
|
+
|
|
80
|
+
def fields_get(self) -> dict[str, Any]:
|
|
81
|
+
"""Get field definitions"""
|
|
82
|
+
...
|
|
83
|
+
|
|
84
|
+
def check_access_rights(self, operation: str, raise_exception: bool = True) -> bool:
|
|
85
|
+
"""Check access rights for the given operation"""
|
|
86
|
+
...
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class OdooClient:
|
|
90
|
+
"""
|
|
91
|
+
Client for interacting with Odoo via JSON-RPC
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
# ============================================================================
|
|
95
|
+
# INITIALIZATION AND CONNECTION MANAGEMENT
|
|
96
|
+
# ============================================================================
|
|
97
|
+
|
|
98
|
+
def __init__(
|
|
99
|
+
self,
|
|
100
|
+
config: Config,
|
|
101
|
+
) -> None:
|
|
102
|
+
"""
|
|
103
|
+
Initialize the Odoo client with connection parameters
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
config: Odoo client configuration
|
|
107
|
+
"""
|
|
108
|
+
self.config = config
|
|
109
|
+
parsed_url = urllib.parse.urlparse(self.config.url)
|
|
110
|
+
self.hostname = parsed_url.netloc
|
|
111
|
+
self.odoo: odoorpc.ODOO | None = None # Will be initialized in _connect
|
|
112
|
+
self.uid: int | None = None # Will be set after login
|
|
113
|
+
self._connect()
|
|
114
|
+
|
|
115
|
+
def _ensure_connected(self) -> Any:
|
|
116
|
+
"""Ensure we have a valid connection"""
|
|
117
|
+
if self.odoo is None:
|
|
118
|
+
raise InternalServerError("Not connected to Odoo")
|
|
119
|
+
return self.odoo
|
|
120
|
+
|
|
121
|
+
def _get_model(self, model_name: str) -> OdooModelProtocol:
|
|
122
|
+
"""Get a model proxy with proper typing"""
|
|
123
|
+
odoo_conn = self._ensure_connected()
|
|
124
|
+
return cast(OdooModelProtocol, odoo_conn.env[model_name])
|
|
125
|
+
|
|
126
|
+
def _connect(self) -> None:
|
|
127
|
+
"""Initialize the OdooRPC connection and authenticate"""
|
|
128
|
+
logger.debug(f"Connecting to Odoo at: {self.config.url}")
|
|
129
|
+
logger.debug(f"Database: {self.config.db}, User: {self.config.username}")
|
|
130
|
+
|
|
131
|
+
try:
|
|
132
|
+
# Determine protocol and port based on URL scheme
|
|
133
|
+
is_https = self.config.url.startswith("https://")
|
|
134
|
+
protocol = "jsonrpc+ssl" if is_https else "jsonrpc"
|
|
135
|
+
port = 443 if is_https else 80
|
|
136
|
+
|
|
137
|
+
self.odoo = odoorpc.ODOO(
|
|
138
|
+
self.hostname,
|
|
139
|
+
protocol=protocol,
|
|
140
|
+
port=port,
|
|
141
|
+
timeout=self.config.timeout,
|
|
142
|
+
version=None,
|
|
143
|
+
)
|
|
144
|
+
self.odoo.login(self.config.db, self.config.username, self.config.password)
|
|
145
|
+
|
|
146
|
+
# Get user ID for later use
|
|
147
|
+
user_model = self._get_model("res.users")
|
|
148
|
+
user_records = user_model.search([("login", "=", self.config.username)])
|
|
149
|
+
self.uid = user_records[0] if user_records else None
|
|
150
|
+
|
|
151
|
+
logger.info("✅ Successfully connected to Odoo")
|
|
152
|
+
|
|
153
|
+
except (RPCError, InternalError, ConnectorError) as e:
|
|
154
|
+
# Log connection errors as they're important for debugging
|
|
155
|
+
logger.error(f"🔴 Failed to connect to Odoo: {str(e)}")
|
|
156
|
+
|
|
157
|
+
# Check specific error types and raise appropriate custom exceptions
|
|
158
|
+
error_msg = str(e).lower()
|
|
159
|
+
if isinstance(e, RPCError):
|
|
160
|
+
if "access" in error_msg or "denied" in error_msg:
|
|
161
|
+
raise AuthenticationError(
|
|
162
|
+
f"Authentication failed: {str(e)}",
|
|
163
|
+
username=self.config.username,
|
|
164
|
+
database=self.config.db,
|
|
165
|
+
) from e
|
|
166
|
+
raise OdooRPCError(error=e, method="connect") from e
|
|
167
|
+
if isinstance(e, ConnectorError):
|
|
168
|
+
raise ConnectionTimeoutError(
|
|
169
|
+
f"Connection failed: {str(e)}", timeout=self.config.timeout
|
|
170
|
+
) from e
|
|
171
|
+
raise InternalServerError(f"Internal error: {str(e)}") from e
|
|
172
|
+
|
|
173
|
+
# ============================================================================
|
|
174
|
+
# MODEL INTROSPECTION
|
|
175
|
+
# ============================================================================
|
|
176
|
+
|
|
177
|
+
def search_models(self, query: str) -> dict[str, Any]:
|
|
178
|
+
"""
|
|
179
|
+
Search for models that match a query term
|
|
180
|
+
|
|
181
|
+
This searches through model names and display names to find models that
|
|
182
|
+
match the given query term.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
query: Search term to find models (searches in model name and display name)
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
Dictionary with search results
|
|
189
|
+
|
|
190
|
+
Examples:
|
|
191
|
+
>>> client = OdooClient(url, db, username, password)
|
|
192
|
+
>>> results = client.search_models('partner')
|
|
193
|
+
>>> print(results['length'])
|
|
194
|
+
3
|
|
195
|
+
>>> print([m['model'] for m in results['models']])
|
|
196
|
+
['res.partner', 'res.partner.bank', 'res.partner.category']
|
|
197
|
+
"""
|
|
198
|
+
try:
|
|
199
|
+
IrModel = self._get_model("ir.model")
|
|
200
|
+
IrModel.check_access_rights("read")
|
|
201
|
+
domain = [
|
|
202
|
+
"&",
|
|
203
|
+
("transient", "=", False),
|
|
204
|
+
"&",
|
|
205
|
+
"|",
|
|
206
|
+
("model", "like", query),
|
|
207
|
+
("name", "like", query),
|
|
208
|
+
"|",
|
|
209
|
+
"&",
|
|
210
|
+
("model", "not like", "base.%"),
|
|
211
|
+
("model", "not like", "ir.%"),
|
|
212
|
+
(
|
|
213
|
+
"model",
|
|
214
|
+
"in",
|
|
215
|
+
[
|
|
216
|
+
"ir.attachment",
|
|
217
|
+
"ir.model",
|
|
218
|
+
"ir.model.fields",
|
|
219
|
+
],
|
|
220
|
+
),
|
|
221
|
+
]
|
|
222
|
+
matching_models = IrModel.search_read(domain, ["model", "name"])
|
|
223
|
+
return {
|
|
224
|
+
"query": query,
|
|
225
|
+
"length": len(matching_models),
|
|
226
|
+
"models": [
|
|
227
|
+
{
|
|
228
|
+
"model": model["model"],
|
|
229
|
+
"name": model["name"],
|
|
230
|
+
}
|
|
231
|
+
for model in matching_models
|
|
232
|
+
],
|
|
233
|
+
}
|
|
234
|
+
except RPCError as e:
|
|
235
|
+
raise OdooRPCError(e, method="search_models") from e
|
|
236
|
+
|
|
237
|
+
def get_model_info(self, model_name: str) -> dict[str, Any]:
|
|
238
|
+
"""
|
|
239
|
+
Get information about a specific model
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
model_name: Name of the model (e.g., 'res.partner')
|
|
243
|
+
|
|
244
|
+
Returns:
|
|
245
|
+
Dictionary with model information
|
|
246
|
+
|
|
247
|
+
Examples:
|
|
248
|
+
>>> client = OdooClient(url, db, username, password)
|
|
249
|
+
>>> info = client.get_model_info('res.partner')
|
|
250
|
+
>>> print(info['name'])
|
|
251
|
+
'Contact'
|
|
252
|
+
"""
|
|
253
|
+
try:
|
|
254
|
+
IrModel = self._get_model("ir.model")
|
|
255
|
+
IrModel.check_access_rights("read")
|
|
256
|
+
result = IrModel.search_read(
|
|
257
|
+
[("model", "=", model_name)], ["model", "name"]
|
|
258
|
+
)
|
|
259
|
+
if not result:
|
|
260
|
+
raise ModelNotFoundError(model_name)
|
|
261
|
+
return result[0]
|
|
262
|
+
except RPCError as e:
|
|
263
|
+
raise OdooRPCError(e, method="get_model_info") from e
|
|
264
|
+
|
|
265
|
+
def get_model_fields(
|
|
266
|
+
self, model_name: str, query: str | None = None
|
|
267
|
+
) -> dict[str, Any]:
|
|
268
|
+
"""
|
|
269
|
+
Get field definitions for a specific model
|
|
270
|
+
|
|
271
|
+
Args:
|
|
272
|
+
model_name: Name of the model (e.g., 'res.partner')
|
|
273
|
+
|
|
274
|
+
Returns:
|
|
275
|
+
Dictionary mapping field names to their definitions
|
|
276
|
+
|
|
277
|
+
Examples:
|
|
278
|
+
>>> client = OdooClient(url, db, username, password)
|
|
279
|
+
>>> fields = client.get_model_fields('res.partner')
|
|
280
|
+
>>> print(fields['name']['type'])
|
|
281
|
+
'char'
|
|
282
|
+
"""
|
|
283
|
+
try:
|
|
284
|
+
Model = self._get_model(model_name)
|
|
285
|
+
data: dict[str, Any] = Model.fields_get()
|
|
286
|
+
result: dict[str, Any] = {
|
|
287
|
+
"length": 0,
|
|
288
|
+
"fields": {},
|
|
289
|
+
}
|
|
290
|
+
if query is not None:
|
|
291
|
+
for field, value in data.items():
|
|
292
|
+
if "related" in value:
|
|
293
|
+
continue
|
|
294
|
+
|
|
295
|
+
if all(
|
|
296
|
+
{
|
|
297
|
+
query.lower() in field.lower()
|
|
298
|
+
or query.lower() in value["string"].lower(),
|
|
299
|
+
}
|
|
300
|
+
):
|
|
301
|
+
result["fields"][field] = {
|
|
302
|
+
"name": field,
|
|
303
|
+
"string": value["string"],
|
|
304
|
+
"type": value["type"],
|
|
305
|
+
"required": value.get("required", False),
|
|
306
|
+
"readonly": value.get("readonly", False),
|
|
307
|
+
"searchable": value.get("searchable", False),
|
|
308
|
+
"relation": value.get("relation", False),
|
|
309
|
+
}
|
|
310
|
+
else:
|
|
311
|
+
result["fields"] = data
|
|
312
|
+
result["length"] = len(result["fields"])
|
|
313
|
+
return result
|
|
314
|
+
except RPCError as e:
|
|
315
|
+
raise OdooRPCError(e, method="get_model_fields") from e
|
|
316
|
+
|
|
317
|
+
# ============================================================================
|
|
318
|
+
# SEARCH AND READ OPERATIONS
|
|
319
|
+
# ============================================================================
|
|
320
|
+
|
|
321
|
+
def search_ids(
|
|
322
|
+
self,
|
|
323
|
+
model_name: str,
|
|
324
|
+
domain: list[Any],
|
|
325
|
+
offset: int | None = None,
|
|
326
|
+
limit: int | None = None,
|
|
327
|
+
order: str | None = None,
|
|
328
|
+
) -> list[int]:
|
|
329
|
+
"""
|
|
330
|
+
Search for record IDs that match a domain
|
|
331
|
+
|
|
332
|
+
Args:
|
|
333
|
+
model_name: Name of the model (e.g., 'res.partner')
|
|
334
|
+
domain: Search domain (e.g., [('is_company', '=', True)])
|
|
335
|
+
offset: Number of records to skip
|
|
336
|
+
limit: Maximum number of records to return
|
|
337
|
+
order: Sorting criteria (e.g., 'name ASC, id DESC')
|
|
338
|
+
|
|
339
|
+
Returns:
|
|
340
|
+
List of matching record IDs
|
|
341
|
+
|
|
342
|
+
Examples:
|
|
343
|
+
>>> client = OdooClient(url, db, username, password)
|
|
344
|
+
>>> ids = client.search_ids(
|
|
345
|
+
... 'res.partner', [('is_company', '=', True)], limit=5
|
|
346
|
+
... )
|
|
347
|
+
>>> print(ids)
|
|
348
|
+
[1, 2, 3, 4, 5]
|
|
349
|
+
"""
|
|
350
|
+
try:
|
|
351
|
+
Model = self._get_model(model_name)
|
|
352
|
+
|
|
353
|
+
# Build search kwargs
|
|
354
|
+
search_kwargs: dict[str, Any] = {}
|
|
355
|
+
if offset is not None:
|
|
356
|
+
search_kwargs["offset"] = offset
|
|
357
|
+
if limit is not None:
|
|
358
|
+
search_kwargs["limit"] = limit
|
|
359
|
+
if order is not None:
|
|
360
|
+
search_kwargs["order"] = order
|
|
361
|
+
|
|
362
|
+
return Model.search(domain, **search_kwargs)
|
|
363
|
+
except RPCError as e:
|
|
364
|
+
raise OdooRPCError(e, method="search_ids") from e
|
|
365
|
+
|
|
366
|
+
def search_count(self, model_name: str, domain: list[Any]) -> int:
|
|
367
|
+
"""
|
|
368
|
+
Count records that match a search domain
|
|
369
|
+
|
|
370
|
+
Args:
|
|
371
|
+
model_name: Name of the model (e.g., 'res.partner')
|
|
372
|
+
domain: Search domain (e.g., [('is_company', '=', True)])
|
|
373
|
+
|
|
374
|
+
Returns:
|
|
375
|
+
Integer count of matching records
|
|
376
|
+
|
|
377
|
+
Examples:
|
|
378
|
+
>>> client = OdooClient(url, db, username, password)
|
|
379
|
+
>>> count = client.search_count('res.partner', [('is_company', '=', True)])
|
|
380
|
+
>>> print(count)
|
|
381
|
+
25
|
|
382
|
+
"""
|
|
383
|
+
try:
|
|
384
|
+
Model = self._get_model(model_name)
|
|
385
|
+
return Model.search_count(domain)
|
|
386
|
+
except RPCError as e:
|
|
387
|
+
raise OdooRPCError(e, method="search_count") from e
|
|
388
|
+
|
|
389
|
+
def search_read(
|
|
390
|
+
self,
|
|
391
|
+
model_name: str,
|
|
392
|
+
domain: list[Any],
|
|
393
|
+
fields: list[str] | None = None,
|
|
394
|
+
offset: int | None = None,
|
|
395
|
+
limit: int | None = None,
|
|
396
|
+
order: str | None = None,
|
|
397
|
+
) -> list[dict[str, Any]]:
|
|
398
|
+
"""
|
|
399
|
+
Search for records and read their data in a single call
|
|
400
|
+
|
|
401
|
+
Args:
|
|
402
|
+
model_name: Name of the model (e.g., 'res.partner')
|
|
403
|
+
domain: Search domain (e.g., [('is_company', '=', True)])
|
|
404
|
+
fields: List of field names to return (None for all)
|
|
405
|
+
offset: Number of records to skip
|
|
406
|
+
limit: Maximum number of records to return
|
|
407
|
+
order: Sorting criteria (e.g., 'name ASC, id DESC')
|
|
408
|
+
|
|
409
|
+
Returns:
|
|
410
|
+
List of dictionaries with the matching records
|
|
411
|
+
|
|
412
|
+
Examples:
|
|
413
|
+
>>> client = OdooClient(url, db, username, password)
|
|
414
|
+
>>> records = client.search_read('res.partner', [
|
|
415
|
+
('is_company', '=', True)
|
|
416
|
+
], limit=5)
|
|
417
|
+
>>> print(len(records))
|
|
418
|
+
5
|
|
419
|
+
"""
|
|
420
|
+
try:
|
|
421
|
+
Model = self._get_model(model_name)
|
|
422
|
+
|
|
423
|
+
# Build search_read arguments
|
|
424
|
+
search_kwargs: dict[str, Any] = {}
|
|
425
|
+
if offset is not None:
|
|
426
|
+
search_kwargs["offset"] = offset
|
|
427
|
+
if fields is not None:
|
|
428
|
+
search_kwargs["fields"] = fields
|
|
429
|
+
if limit is not None:
|
|
430
|
+
search_kwargs["limit"] = limit
|
|
431
|
+
if order is not None:
|
|
432
|
+
search_kwargs["order"] = order
|
|
433
|
+
|
|
434
|
+
result = Model.search_read(domain, **search_kwargs)
|
|
435
|
+
return result
|
|
436
|
+
except RPCError as e:
|
|
437
|
+
raise OdooRPCError(e, method="search_read") from e
|
|
438
|
+
|
|
439
|
+
def read_records(
|
|
440
|
+
self,
|
|
441
|
+
model_name: str,
|
|
442
|
+
ids: list[int],
|
|
443
|
+
fields: list[str] | None = None,
|
|
444
|
+
) -> list[dict[str, Any]]:
|
|
445
|
+
"""
|
|
446
|
+
Read data of records by IDs
|
|
447
|
+
|
|
448
|
+
Args:
|
|
449
|
+
model_name: Name of the model (e.g., 'res.partner')
|
|
450
|
+
ids: List of record IDs to read
|
|
451
|
+
fields: List of field names to return (None for all)
|
|
452
|
+
|
|
453
|
+
Returns:
|
|
454
|
+
List of dictionaries with the requested records
|
|
455
|
+
|
|
456
|
+
Examples:
|
|
457
|
+
>>> client = OdooClient(url, db, username, password)
|
|
458
|
+
>>> records = client.read_records('res.partner', [1])
|
|
459
|
+
>>> print(records[0]['name'])
|
|
460
|
+
'YourCompany'
|
|
461
|
+
"""
|
|
462
|
+
try:
|
|
463
|
+
Model = self._get_model(model_name)
|
|
464
|
+
|
|
465
|
+
if fields is not None:
|
|
466
|
+
result = Model.browse(ids).read(fields)
|
|
467
|
+
else:
|
|
468
|
+
result = Model.browse(ids).read()
|
|
469
|
+
|
|
470
|
+
return cast(list[dict[str, Any]], result)
|
|
471
|
+
except RPCError as e:
|
|
472
|
+
raise OdooRPCError(e, method="read_records") from e
|
|
473
|
+
|
|
474
|
+
# ============================================================================
|
|
475
|
+
# CRUD OPERATIONS
|
|
476
|
+
# ============================================================================
|
|
477
|
+
|
|
478
|
+
def create_records(
|
|
479
|
+
self, model_name: str, values_list: list[dict[str, Any]]
|
|
480
|
+
) -> int | list[int]:
|
|
481
|
+
"""
|
|
482
|
+
Create records in an Odoo model
|
|
483
|
+
|
|
484
|
+
Args:
|
|
485
|
+
model_name: Name of the model (e.g., 'res.partner')
|
|
486
|
+
values_list: List of dictionaries with field values for the new records
|
|
487
|
+
|
|
488
|
+
Returns:
|
|
489
|
+
List of created record IDs
|
|
490
|
+
|
|
491
|
+
Examples:
|
|
492
|
+
>>> client = OdooClient(url, db, username, password)
|
|
493
|
+
>>> record_ids = client.create_records('res.partner', [
|
|
494
|
+
{'name': 'Company 1'}, {'name': 'Company 2'}
|
|
495
|
+
])
|
|
496
|
+
>>> print(record_ids)
|
|
497
|
+
[42, 43]
|
|
498
|
+
"""
|
|
499
|
+
try:
|
|
500
|
+
Model = self._get_model(model_name)
|
|
501
|
+
return cast(int | list[int], Model.create(values_list))
|
|
502
|
+
except RPCError as e:
|
|
503
|
+
raise OdooRPCError(e, method="create_records") from e
|
|
504
|
+
|
|
505
|
+
def write_records(
|
|
506
|
+
self,
|
|
507
|
+
model_name: str,
|
|
508
|
+
record_ids: list[int],
|
|
509
|
+
values: dict[str, Any],
|
|
510
|
+
) -> bool:
|
|
511
|
+
"""
|
|
512
|
+
Update records in an Odoo model
|
|
513
|
+
|
|
514
|
+
Args:
|
|
515
|
+
model_name: Name of the model (e.g., 'res.partner')
|
|
516
|
+
record_ids: List of record IDs to update
|
|
517
|
+
values: Dictionary with field values to update
|
|
518
|
+
|
|
519
|
+
Returns:
|
|
520
|
+
Boolean indicating success
|
|
521
|
+
|
|
522
|
+
Examples:
|
|
523
|
+
>>> client = OdooClient(url, db, username, password)
|
|
524
|
+
>>> success = client.write_records(
|
|
525
|
+
... 'res.partner', [1], {'name': 'Updated Name'}
|
|
526
|
+
... )
|
|
527
|
+
>>> print(success)
|
|
528
|
+
True
|
|
529
|
+
"""
|
|
530
|
+
try:
|
|
531
|
+
Model = self._get_model(model_name)
|
|
532
|
+
records = Model.browse(record_ids)
|
|
533
|
+
return cast(bool, records.write(values))
|
|
534
|
+
except RPCError as e:
|
|
535
|
+
raise OdooRPCError(e, method="write_records") from e
|
|
536
|
+
|
|
537
|
+
def unlink_records(self, model_name: str, record_ids: list[int]) -> bool:
|
|
538
|
+
"""
|
|
539
|
+
Delete records from an Odoo model
|
|
540
|
+
|
|
541
|
+
Args:
|
|
542
|
+
model_name: Name of the model (e.g., 'res.partner')
|
|
543
|
+
record_ids: List of record IDs to delete
|
|
544
|
+
|
|
545
|
+
Returns:
|
|
546
|
+
Boolean indicating success
|
|
547
|
+
|
|
548
|
+
Examples:
|
|
549
|
+
>>> client = OdooClient(url, db, username, password)
|
|
550
|
+
>>> success = client.unlink_records('res.partner', [42])
|
|
551
|
+
>>> print(success)
|
|
552
|
+
True
|
|
553
|
+
"""
|
|
554
|
+
try:
|
|
555
|
+
Model = self._get_model(model_name)
|
|
556
|
+
records = Model.browse(record_ids)
|
|
557
|
+
return cast(bool, records.unlink())
|
|
558
|
+
except RPCError as e:
|
|
559
|
+
raise OdooRPCError(e, method="unlink_records") from e
|
|
560
|
+
|
|
561
|
+
# ============================================================================
|
|
562
|
+
# GENERIC METHOD EXECUTION
|
|
563
|
+
# ============================================================================
|
|
564
|
+
|
|
565
|
+
def execute_method(self, model: str, method: str, *args: Any, **kwargs: Any) -> Any:
|
|
566
|
+
"""
|
|
567
|
+
Execute an arbitrary method on a model
|
|
568
|
+
|
|
569
|
+
Args:
|
|
570
|
+
model: The model name (e.g., 'res.partner')
|
|
571
|
+
method: Method name to execute
|
|
572
|
+
*args: Positional arguments to pass to the method
|
|
573
|
+
**kwargs: Keyword arguments to pass to the method
|
|
574
|
+
|
|
575
|
+
Returns:
|
|
576
|
+
Result of the method execution
|
|
577
|
+
"""
|
|
578
|
+
try:
|
|
579
|
+
model_proxy = self._get_model(model)
|
|
580
|
+
# Use getattr to dynamically call the method on the model proxy
|
|
581
|
+
method_func = getattr(model_proxy, method)
|
|
582
|
+
return method_func(*args, **kwargs)
|
|
583
|
+
except RPCError as e:
|
|
584
|
+
raise OdooRPCError(e, method=f"execute_method: {model}.{method}") from e
|
|
585
|
+
|
|
586
|
+
def call_method(
|
|
587
|
+
self,
|
|
588
|
+
model_name: str,
|
|
589
|
+
method_name: str,
|
|
590
|
+
args: list[Any],
|
|
591
|
+
kwargs: dict[str, Any],
|
|
592
|
+
) -> Any:
|
|
593
|
+
"""
|
|
594
|
+
Call a custom method on an Odoo model
|
|
595
|
+
|
|
596
|
+
Args:
|
|
597
|
+
model_name: Name of the model (e.g., 'res.partner')
|
|
598
|
+
method_name: Name of the method to call
|
|
599
|
+
args: Positional arguments to pass to the method
|
|
600
|
+
kwargs: Keyword arguments to pass to the method
|
|
601
|
+
|
|
602
|
+
Returns:
|
|
603
|
+
Method result
|
|
604
|
+
|
|
605
|
+
Examples:
|
|
606
|
+
>>> client = OdooClient(url, db, username, password)
|
|
607
|
+
>>> result = client.call_method('res.partner', 'name_get', [1], {})
|
|
608
|
+
>>> print(result)
|
|
609
|
+
[(1, 'YourCompany')]
|
|
610
|
+
"""
|
|
611
|
+
try:
|
|
612
|
+
model_proxy = self._get_model(model_name)
|
|
613
|
+
# Use getattr to dynamically call the method on the model proxy
|
|
614
|
+
method_func = getattr(model_proxy, method_name)
|
|
615
|
+
result = method_func(*args, **kwargs)
|
|
616
|
+
if result is None:
|
|
617
|
+
raise ValueError(f"Failed to call method {method_name}")
|
|
618
|
+
return result
|
|
619
|
+
except RPCError as e:
|
|
620
|
+
raise OdooRPCError(
|
|
621
|
+
e, method=f"call_method: {model_name}.{method_name}"
|
|
622
|
+
) from e
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
def get_odoo_client() -> OdooClient:
|
|
626
|
+
"""
|
|
627
|
+
Get a configured Odoo client instance
|
|
628
|
+
|
|
629
|
+
Returns:
|
|
630
|
+
OdooClient: A configured Odoo client instance
|
|
631
|
+
"""
|
|
632
|
+
return OdooClient(config=Config())
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
biszx_odoo_mcp/__init__.py,sha256=J-K5_2GapE12EHcbhrotvjxKDYFS2aGa_eIpcsnHjIg,58
|
|
2
|
+
biszx_odoo_mcp/__main__.py,sha256=b7dX--Aowgjuvgr8Sh4aXa63TCwxPa_vLjh7JYReFz0,1256
|
|
3
|
+
biszx_odoo_mcp/exceptions.py,sha256=vVMyZekFmZIg2SsH53zcefWp_swqFkE4lxQin1OVgB0,6611
|
|
4
|
+
biszx_odoo_mcp/main.py,sha256=_HW_Pblf8BFIOhfZ7-uuT4uKNn3uZ269zsK8Lk7Rt5o,3914
|
|
5
|
+
biszx_odoo_mcp/server/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
biszx_odoo_mcp/server/context.py,sha256=PxyFxFXL3GDpUynm4j63RiUjjIagDfy1oLh1cjPfvVA,378
|
|
7
|
+
biszx_odoo_mcp/server/resources.py,sha256=EliQzuWDKibpd5MPWb2FyM65acll7EkvjYcxdLBFmsw,8389
|
|
8
|
+
biszx_odoo_mcp/server/response.py,sha256=VY85S0JPpR7F1lxweZV_lqB1cqhGtGRbsTkkBmPXimo,877
|
|
9
|
+
biszx_odoo_mcp/server/tools.py,sha256=XfFSg8_5AM-HsXUrqHSRh5_eBO6ExRUGbU3JAQk_0BM,14019
|
|
10
|
+
biszx_odoo_mcp/tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
+
biszx_odoo_mcp/tools/config.py,sha256=xdV6L9CATO1PDSk6nenh1fCimONHGo25UAVuoulGFbo,2869
|
|
12
|
+
biszx_odoo_mcp/tools/odoo_client.py,sha256=LfXKHkkjgwONuOulR0JpojGcb0_OkiUAa2qnaGtZkGI,21115
|
|
13
|
+
biszx_odoo_mcp-1.1.2.dist-info/licenses/LICENSE,sha256=_FwB4PGxcQWsToTqX33moF0HqnB45Ox6Fr0VPJiLyBI,1071
|
|
14
|
+
biszx_odoo_mcp-1.1.2.dist-info/METADATA,sha256=E45-DbNR4g0817nX5GAeODhydoKtlShhRxxhOVoE2zM,5394
|
|
15
|
+
biszx_odoo_mcp-1.1.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
16
|
+
biszx_odoo_mcp-1.1.2.dist-info/entry_points.txt,sha256=vdIkuiVsddBLNV--8Wgaf8xHu6Pdi2DAia-ISGNdxP0,64
|
|
17
|
+
biszx_odoo_mcp-1.1.2.dist-info/top_level.txt,sha256=l6PxKyczED68V9LsRlWYClo1GfjSJaP9V-SDMnAUJbU,15
|
|
18
|
+
biszx_odoo_mcp-1.1.2.dist-info/RECORD,,
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
biszx_odoo_mcp/__init__.py,sha256=J-K5_2GapE12EHcbhrotvjxKDYFS2aGa_eIpcsnHjIg,58
|
|
2
|
-
biszx_odoo_mcp/__main__.py,sha256=b7dX--Aowgjuvgr8Sh4aXa63TCwxPa_vLjh7JYReFz0,1256
|
|
3
|
-
biszx_odoo_mcp/exceptions.py,sha256=vVMyZekFmZIg2SsH53zcefWp_swqFkE4lxQin1OVgB0,6611
|
|
4
|
-
biszx_odoo_mcp/main.py,sha256=_HW_Pblf8BFIOhfZ7-uuT4uKNn3uZ269zsK8Lk7Rt5o,3914
|
|
5
|
-
biszx_odoo_mcp-1.1.0.dist-info/licenses/LICENSE,sha256=_FwB4PGxcQWsToTqX33moF0HqnB45Ox6Fr0VPJiLyBI,1071
|
|
6
|
-
biszx_odoo_mcp-1.1.0.dist-info/METADATA,sha256=zok0QuLLRKeeTpC0W9T1-UCqhTSM8HEX4ZkxVp8qC-4,5394
|
|
7
|
-
biszx_odoo_mcp-1.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
8
|
-
biszx_odoo_mcp-1.1.0.dist-info/entry_points.txt,sha256=vdIkuiVsddBLNV--8Wgaf8xHu6Pdi2DAia-ISGNdxP0,64
|
|
9
|
-
biszx_odoo_mcp-1.1.0.dist-info/top_level.txt,sha256=l6PxKyczED68V9LsRlWYClo1GfjSJaP9V-SDMnAUJbU,15
|
|
10
|
-
biszx_odoo_mcp-1.1.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|