diff --git a/CHANGELOG b/CHANGELOG index b6216fed12..632e5a0472 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,4 @@ +* Add One2One field * Add AUTOINCREMENT to sqlite primary key Version 1.8.0 - 2010-11-01 diff --git a/doc/ref/models/fields.rst b/doc/ref/models/fields.rst index 49fb4398d0..5171e192ff 100644 --- a/doc/ref/models/fields.rst +++ b/doc/ref/models/fields.rst @@ -497,6 +497,28 @@ Instance methods: Return the target :class:`~trytond.model.Model`. +One2One +------- + +.. class:: One2One(relation_name, origin, target, string[, datetime_field[, \**options]]) + +A one-to-one relation field. + +.. warning:: + It is on the relation_name :class:`~trytond.model.Model` that the + unicity of the couple (origin, target) must be checked. +.. + +.. attribute:: One2One.datetime_field + + Same as :attr:`Many2One.datetime_field` + +Instance methods: + +.. method:: One2One.get_target(pool) + + Return the target :class:`~trytond.model.Model`. + Function -------- diff --git a/trytond/model/browse.py b/trytond/model/browse.py index 12d4a98112..e3ffac4d65 100644 --- a/trytond/model/browse.py +++ b/trytond/model/browse.py @@ -145,7 +145,7 @@ class BrowseRecord(object): model = self._model.pool.get(j.model_name) elif hasattr(j, 'get_target'): model = j.get_target(self._model.pool) - if model and j._type in ('many2one',): + if model and j._type in ('many2one', 'one2one'): if (not data[i] and not (isinstance(data[i], (int, long)) and not isinstance(data[i], diff --git a/trytond/model/fields/__init__.py b/trytond/model/fields/__init__.py index b3cc710f40..eb8e16d1e9 100644 --- a/trytond/model/fields/__init__.py +++ b/trytond/model/fields/__init__.py @@ -18,3 +18,4 @@ from one2many import * from many2many import * from function import * from property import * +from one2one import * diff --git a/trytond/model/fields/one2one.py b/trytond/model/fields/one2one.py new file mode 100644 index 0000000000..d3ad640e68 --- /dev/null +++ b/trytond/model/fields/one2one.py @@ -0,0 +1,48 @@ +#This file is part of Tryton. The COPYRIGHT file at the top level of +#this repository contains the full copyright notices and license terms. + +from trytond.model.fields.many2many import Many2Many +from trytond.transaction import Transaction + + +class One2One(Many2Many): + ''' + Define one2one field (``int``). + ''' + _type = 'one2one' + + def get(self, ids, model, name, values=None): + ''' + Return target record. + + :param ids: a list of ids + :param model: a string with the name of the model + :param name: a string with the name of the field + :param values: a dictionary with the read values + :return: a dictionary with ids as key and target id as value + ''' + res = super(One2One, self).get(ids, model, name, values=values) + for i, vals in res.iteritems(): + res[i] = vals[0] if vals else False + return res + + def set(self, ids, model, name, value): + ''' + Set the values. + + :param ids: A list of ids + :param model: A string with the name of the model + :param name: A string with the name of the field + :param value: The id to link + ''' + relation_obj = model.pool.get(self.relation_name) + relation_ids = relation_obj.search([ + (self.origin, 'in', ids), + ]) + relation_obj.delete(relation_ids) + if value: + for record_id in ids: + relation_obj.create({ + self.origin: record_id, + self.target: value, + }) diff --git a/trytond/model/model.py b/trytond/model/model.py index bc1d952061..7eb092d209 100644 --- a/trytond/model/model.py +++ b/trytond/model/model.py @@ -534,7 +534,7 @@ class Model(object): field = self._columns[field_name] else: field = self._inherit_fields[field_name][2] - if field._type in ('many2one',): + if field._type in ('many2one', 'one2one'): if not isinstance(field_value, (int, long)): continue obj = self.pool.get(field.model_name) @@ -740,6 +740,7 @@ class Model(object): 'one2many', 'many2many', 'many2one', + 'one2one', ): if hasattr(self._columns[field], 'model_name'): relation = copy.copy(self._columns[field].model_name) diff --git a/trytond/model/modelsql.py b/trytond/model/modelsql.py index 52aa291676..a3f14bbb4d 100644 --- a/trytond/model/modelsql.py +++ b/trytond/model/modelsql.py @@ -632,8 +632,11 @@ class ModelSQL(ModelStorage): if field not in fields_related.keys(): continue fields_related2values.setdefault(field, {}) - if self._columns[field]._type == 'many2one': - obj = self.pool.get(self._columns[field].model_name) + if self._columns[field]._type in ('many2one', 'one2one'): + if hasattr(self._columns[field], 'model_name'): + obj = self.pool.get(self._columns[field].model_name) + else: + obj = self._columns[field].get_target(self.pool) if hasattr(self._columns[field], 'datetime_field') \ and self._columns[field].datetime_field: for record in res: @@ -678,7 +681,7 @@ class ModelSQL(ModelStorage): if field not in self._columns: continue for related in fields_related[field]: - if self._columns[field]._type == 'many2one': + if self._columns[field]._type in ('many2one', 'one2one'): if record[field]: record[field + '.' + related] = \ fields_related2values[field]\ @@ -1358,18 +1361,37 @@ class ModelSQL(ModelStorage): field = table._columns.get(fargs[0], False) if len(fargs) > 1: if field._type == 'many2one': + target_obj = self.pool.get(field.model_name) if hasattr(field, 'search'): - domain.extend([(fargs[0], 'in', - self.pool.get(field.model_name).search([ - (fargs[1], domain[i][1], domain[i][2]), - ], order=[]))]) + domain.extend([(fargs[0], 'in', target_obj.search([ + (fargs[1], domain[i][1], domain[i][2]), + ], order=[]))]) domain.pop(i) else: - domain[i] = (fargs[0], 'inselect', - self.pool.get(field.model_name).search([ - (fargs[1], domain[i][1], domain[i][2]), - ], order=[], query_string=True), - table) + domain[i] = (fargs[0], 'inselect', target_obj.search([ + (fargs[1], domain[i][1], domain[i][2]), + ], order=[], query_string=True), table) + i += 1 + continue + elif field._type in ('one2one', 'many2many'): + if hasattr(field, 'model_name'): + target_obj = self.pool.get(field.model_name) + else: + target_obj = field.get_target(self.pool) + relation_obj = self.pool.get(field.relation_name) + if hasattr(field, 'search'): + domain.extend([(fargs[0], 'in', target_obj.search([ + (fargs[1], domain[i][1], domain[i][2]), + ], order=[]))]) + domain.pop(i) + else: + query1, query2 = target_obj.search([ + (fargs[1], domain[i][1], domain[i][2]), + ], order=[], query_string=True) + query1 = ('SELECT "%s" FROM "%s" WHERE "%s" IN (%s)' % + (field.origin, relation_obj._table, + field.target, query1)) + domain[i] = ('id', 'inselect', (query1, query2)) i += 1 continue else: @@ -1433,7 +1455,7 @@ class ModelSQL(ModelStorage): domain[i] = ('id', 'in', ids3) i += 1 - elif field._type == 'many2many': + elif field._type in ('many2many', 'one2one'): # XXX must find a solution for long id list if hasattr(field, 'model_name'): target_obj = self.pool.get(field.model_name) diff --git a/trytond/model/modelstorage.py b/trytond/model/modelstorage.py index 6af77286c2..2d52984b11 100644 --- a/trytond/model/modelstorage.py +++ b/trytond/model/modelstorage.py @@ -278,7 +278,7 @@ class ModelStorage(Model): data[field_name] = default[field_name] elif ftype == 'function': del data[field_name] - elif ftype == 'many2one': + elif ftype in ('many2one', 'one2one'): try: data[field_name] = data[field_name] and \ data[field_name][0] @@ -648,6 +648,9 @@ class ModelStorage(Model): res = [('set', res)] return res + def get_one2one(relation, value): + return ('set', get_many2one(relation, value)) + def get_reference(value): if not value: return False @@ -749,6 +752,8 @@ class ModelStorage(Model): res = get_many2one(this_field_def['relation'], value) elif field_type == 'many2many': res = get_many2many(this_field_def['relation'], value) + elif field_type == 'one2one': + res = get_one2one(this_field_def['relation'], value) elif field_type == 'reference': res = get_reference(value) else: @@ -969,8 +974,9 @@ class ModelStorage(Model): not field.setter: continue # validate domain - if field._type in ('many2one', 'many2many', 'one2many') \ - and field.domain: + if (field._type in + ('many2one', 'many2many', 'one2many', 'one2one') + and field.domain): if field._type in ('many2one', 'one2many'): relation_obj = self.pool.get(field.model_name) else: @@ -1095,7 +1101,7 @@ class ModelStorage(Model): for field in defaults.keys(): fld_def = (field in self._columns) and self._columns[field] \ or self._inherit_fields[field][2] - if fld_def._type in ('many2one',): + if fld_def._type in ('many2one', 'one2one'): if isinstance(defaults[field], (list, tuple)): vals[field] = defaults[field][0] else: diff --git a/trytond/test/test.py b/trytond/test/test.py index be7f8c82be..b4577b431a 100644 --- a/trytond/test/test.py +++ b/trytond/test/test.py @@ -354,3 +354,72 @@ class DateTimeRequired(ModelSQL): required=True) DateTimeRequired() + + +class One2One(ModelSQL): + 'One2One' + _name = 'test.one2one' + _description = __doc__ + name = fields.Char('Name', required=True) + one2one = fields.One2One('test.one2one.relation', 'origin', 'target', + string='One2One', help='Test one2one', required=False) + +One2One() + + +class One2OneTarget(ModelSQL): + 'One2One Target' + _name = 'test.one2one.target' + name = fields.Char('Name', required=True) + +One2OneTarget() + + +class One2OneRelation(ModelSQL): + 'One2One Relation' + _name = 'test.one2one.relation' + _description = __doc__ + origin = fields.Many2One('test.one2one', 'Origin') + target = fields.Many2One('test.one2one.target', 'Target') + + def __init__(self): + super(One2OneRelation, self).__init__() + self._sql_constraints += [ + ('origin_unique', 'UNIQUE(origin)', + 'Origin must be unique'), + ('target_unique', 'UNIQUE(target)', + 'Target must be unique'), + ] + +One2OneRelation() + + +class One2OneRequired(ModelSQL): + 'One2One' + _name = 'test.one2one_required' + _description = __doc__ + name = fields.Char('Name', required=True) + one2one = fields.One2One('test.one2one_required.relation', 'origin', 'target', + string='One2One', help='Test one2one', required=True) + +One2OneRequired() + + +class One2OneRequiredRelation(ModelSQL): + 'One2One Relation' + _name = 'test.one2one_required.relation' + _description = __doc__ + origin = fields.Many2One('test.one2one_required', 'Origin') + target = fields.Many2One('test.one2one.target', 'Target') + + def __init__(self): + super(One2OneRequiredRelation, self).__init__() + self._sql_constraints += [ + ('origin_unique', 'UNIQUE(origin)', + 'Origin must be unique'), + ('target_unique', 'UNIQUE(target)', + 'Target must be unique'), + ] + +One2OneRequiredRelation() + diff --git a/trytond/tests/test_fields.py b/trytond/tests/test_fields.py index 0976295f0c..eeb3e4f15c 100644 --- a/trytond/tests/test_fields.py +++ b/trytond/tests/test_fields.py @@ -60,6 +60,10 @@ class FieldsTestCase(unittest.TestCase): self.datetime_default = POOL.get('test.datetime_default') self.datetime_required = POOL.get('test.datetime_required') + self.one2one = POOL.get('test.one2one') + self.one2one_target = POOL.get('test.one2one.target') + self.one2one_required = POOL.get('test.one2one_required') + def test0010boolean(self): ''' Test Boolean. @@ -2072,6 +2076,127 @@ class FieldsTestCase(unittest.TestCase): transaction.cursor.rollback() + def test0100one2one(self): + ''' + Test One2One. + ''' + with Transaction().start(DB_NAME, USER, CONTEXT) as transaction: + target1_id = self.one2one_target.create({ + 'name': 'target1', + }) + one2one1_id = self.one2one.create({ + 'name': 'origin1', + 'one2one': target1_id, + }) + self.assert_(one2one1_id) + + one2one1 = self.one2one.read(one2one1_id, ['one2one', + 'one2one.name']) + self.assert_(one2one1['one2one'] == target1_id) + self.assert_(one2one1['one2one.name'] == 'target1') + + one2one_ids = self.one2one.search([ + ('one2one', '=', 'target1'), + ]) + self.assert_(one2one_ids == [one2one1_id]) + + one2one_ids = self.one2one.search([ + ('one2one', '!=', 'target1'), + ]) + self.assert_(one2one_ids == []) + + one2one_ids = self.one2one.search([ + ('one2one', 'in', [target1_id]), + ]) + self.assert_(one2one_ids == [one2one1_id]) + + one2one_ids = self.one2one.search([ + ('one2one', 'in', [0]), + ]) + self.assert_(one2one_ids == []) + + one2one_ids = self.one2one.search([ + ('one2one', 'not in', [target1_id]), + ]) + self.assert_(one2one_ids == []) + + one2one_ids = self.one2one.search([ + ('one2one', 'not in', [0]), + ]) + self.assert_(one2one_ids == [one2one1_id]) + + one2one_ids = self.one2one.search([ + ('one2one.name', '=', 'target1'), + ]) + self.assert_(one2one_ids == [one2one1_id]) + + one2one_ids = self.one2one.search([ + ('one2one.name', '!=', 'target1'), + ]) + self.assert_(one2one_ids == []) + + one2one = self.one2one.browse(one2one1_id) + self.assert_(one2one.one2one.name == 'target1') + + one2one2_id = self.one2one.create({ + 'name': 'origin2', + }) + self.assert_(one2one2_id) + + one2one2 = self.one2one.read(one2one2_id, ['one2one']) + self.assert_(one2one2['one2one'] == False) + + one2one_ids = self.one2one.search([ + ('one2one', '=', False), + ]) + self.assert_(one2one_ids == [one2one2_id]) + + target2_id = self.one2one_target.create({ + 'name': 'target2', + }) + self.one2one.write(one2one2_id, { + 'one2one': target2_id, + }) + target2_id = self.one2one_target.search([ + ('name', '=', 'target2'), + ])[0] + one2one2 = self.one2one.read(one2one2_id, ['one2one']) + self.assert_(one2one2['one2one'] == target2_id) + + self.one2one.write(one2one2_id, { + 'one2one': False, + }) + one2one2 = self.one2one.read(one2one2_id, ['one2one']) + self.assert_(one2one2['one2one'] == False) + + one2one2 = self.one2one.browse(one2one2_id) + self.assert_(not one2one2.one2one) + + self.failUnlessRaises(Exception, self.one2one.create, { + 'name': 'one2one3', + 'one2one': target1_id, + }) + + self.failUnlessRaises(Exception, self.one2one.write, one2one2_id, { + 'one2one': target1_id, + }) + + self.failUnlessRaises(Exception, self.one2one_required.create, { + 'name': 'one2one3', + }) + + target3_id = self.one2one_target.create({ + 'name': 'target3_id', + }) + + one2one3_id = self.one2one_required.create({ + 'name': 'one2one3', + 'one2one': target3_id, + }) + self.assert_(one2one3_id) + + transaction.cursor.rollback() + def suite(): return unittest.TestLoader().loadTestsFromTestCase(FieldsTestCase)