Skip to content
Merged
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
10 changes: 10 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@ Changelog
---------


2.x.x (unreleased)
~~~~~~~~~~~~~~~~~~

* Support :method:`Model.create` with a list of values.
With Odoo >= 12.0

* Support :method:`RecordList.copy`.
With Odoo >= 18.0


2.2.0 (2025-09-16)
~~~~~~~~~~~~~~~~~~

Expand Down
29 changes: 23 additions & 6 deletions odooly.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,10 @@

# Published object methods
_methods = {
'common': ['about', 'login', 'authenticate', 'version'],
'db': ['create_database', 'duplicate_database', 'db_exist', 'drop', 'dump',
'restore', 'rename', 'list', 'list_lang', 'list_countries',
'change_admin_password', 'server_version', 'migrate_databases'],
'common': ['about', 'login', 'authenticate', 'version'],
'object': ['execute', 'execute_kw'],
}
# New 6.1: (db) create_database db_exist,
Expand Down Expand Up @@ -1214,19 +1214,23 @@ def get(self, domain, *args, **kwargs):
return Record(self, ids[0]) if ids else None

def create(self, values):
"""Create a :class:`Record`.
"""Create one or many :class:`Record`(s).

The argument `values` is a dictionary of values which are used to
create the record. Relationship fields `one2many` and `many2many`
accept either a list of ids or a RecordList or the extended Odoo
syntax. Relationship fields `many2one` and `reference` accept
either a Record or the Odoo syntax.
Since Odoo 12.0, it can create multiple records.

The newly created :class:`Record` is returned.
The newly created :class:`Record` is returned (or :class:`RecordList`).
"""
values = self._unbrowse_values(values)
new_id = self._execute('create', values)
return Record(self, new_id)
if hasattr(values, "items"):
values = self._unbrowse_values(values)
else: # Odoo >= 12.0
values = [self._unbrowse_values(vals) for vals in values]
new_ids = self._execute('create', values)
return Record(self, new_ids)

def read(self, *params, **kwargs):
"""Wrapper for ``client.execute(model, 'read', [...], ('a', 'b'))``.
Expand Down Expand Up @@ -1673,6 +1677,17 @@ def read(self, fields=None):
return records
return values

def copy(self, default=None):
"""Copy records and return :class:`RecordList`. Odoo >= 18.0

The optional argument `default` is a mapping which overrides some
values of the new records.
"""
if default:
default = self._model._unbrowse_values(default)
new_ids = self._execute('copy', self.ids, default)
return RecordList(self._model, new_ids)

@property
def _external_id(self):
"""Retrieve the External IDs of the :class:`RecordList`.
Expand Down Expand Up @@ -1769,6 +1784,8 @@ def copy(self, default=None):
if default:
default = self._model._unbrowse_values(default)
new_id = self._execute('copy', self.id, default)
if isinstance(new_id, list):
[new_id] = new_id or [False]
return Record(self._model, new_id)

def _send(self, signal):
Expand Down
15 changes: 14 additions & 1 deletion tests/test_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,11 @@ def __getitem__(self, key):
ids[ids.index(8888)] = b'\xdan\xeecode'.decode('latin-1')
return [(res_id, b'name_%s'.decode() % res_id) for res_id in ids]
if method in ('create', 'copy'):
return 1999
single_id = (
(method == 'copy' and isinstance(args[0], int)) or
(method == 'create' and hasattr(args[0], 'items'))
)
return 1999 if single_id else list(range(1001, 1001 + len(args[0])))
return [sentinel.OTHER]

def setUp(self):
Expand Down Expand Up @@ -464,20 +468,26 @@ def test_create(self):
FooBar = self.env['foo.bar']

record42 = FooBar.browse(42)
record100 = FooBar.browse(100)
recordlist42 = FooBar.browse([4, 2])

FooBar.create({'spam': 42})
FooBar.create({'spam': record42})
FooBar.create({'spam': recordlist42})
FooBar._execute('create', {'spam': 42})
FooBar.create({})
# Odoo >= 12.0
FooBar.create([{'spam': record42}])
FooBar.create([{'spam': record100}, {'spam': record42}])
self.assertCalls(
OBJ('foo.bar', 'fields_get'),
OBJ('foo.bar', 'create', {'spam': 42}),
OBJ('foo.bar', 'create', {'spam': 42}),
OBJ('foo.bar', 'create', {'spam': [4, 2]}),
OBJ('foo.bar', 'create', {'spam': 42}),
OBJ('foo.bar', 'create', {}),
OBJ('foo.bar', 'create', [{'spam': 42}]),
OBJ('foo.bar', 'create', [{'spam': 100}, {'spam': 42}]),
)
self.assertOutput('')

Expand Down Expand Up @@ -724,13 +734,16 @@ def test_copy(self):
rec.copy({'spam': rec})
rec.copy({'spam': records})
rec.copy({})
# Odoo >= 18.0
records.copy({'spam': rec})
self.assertCalls(
OBJ('foo.bar', 'copy', 42, None),
OBJ('foo.bar', 'fields_get'),
OBJ('foo.bar', 'copy', 42, {'spam': 42}),
OBJ('foo.bar', 'copy', 42, {'spam': 42}),
OBJ('foo.bar', 'copy', 42, {'spam': [13, 17]}),
OBJ('foo.bar', 'copy', 42, {}),
OBJ('foo.bar', 'copy', [13, 17], {'spam': 42}),
)
self.assertOutput('')

Expand Down