diff options
| author | Christophe Simonis <chs@odoo.com> | 2017-04-13 13:22:27 +0200 |
|---|---|---|
| committer | Christophe Simonis <chs@odoo.com> | 2017-04-13 13:22:27 +0200 |
| commit | 13ad16104afa58fd7d3d70f502ada972727084eb (patch) | |
| tree | 85cf1db6e9bd45960cc76e2b04122c460d945337 /odoo | |
| parent | bb5e0a3cfa08845dc935b1bc4881807f56e116c8 (diff) | |
| parent | 00e169c05a270a0943a74fbeb0a884009946c7cd (diff) | |
[MERGE] forward port branch 10.0 up to 00e169c05a2
Diffstat (limited to 'odoo')
| -rw-r--r-- | odoo/addons/base/ir/ir_filters.xml | 2 | ||||
| -rw-r--r-- | odoo/addons/base/ir/ir_model.py | 50 | ||||
| -rw-r--r-- | odoo/addons/base/ir/ir_qweb/fields.py | 2 | ||||
| -rw-r--r-- | odoo/addons/base/res/res_company_view.xml | 2 | ||||
| -rw-r--r-- | odoo/addons/base/res/res_users.py | 2 | ||||
| -rw-r--r-- | odoo/addons/base/tests/test_expression.py | 20 | ||||
| -rw-r--r-- | odoo/addons/base/tests/test_ir_actions.py | 111 | ||||
| -rw-r--r-- | odoo/addons/test_new_api/ir.model.access.csv | 1 | ||||
| -rw-r--r-- | odoo/addons/test_new_api/models.py | 16 | ||||
| -rw-r--r-- | odoo/addons/test_new_api/tests/test_new_fields.py | 23 | ||||
| -rw-r--r-- | odoo/fields.py | 1 | ||||
| -rw-r--r-- | odoo/models.py | 5 | ||||
| -rw-r--r-- | odoo/modules/graph.py | 7 | ||||
| -rw-r--r-- | odoo/modules/module.py | 2 | ||||
| -rw-r--r-- | odoo/osv/expression.py | 23 |
15 files changed, 225 insertions, 42 deletions
diff --git a/odoo/addons/base/ir/ir_filters.xml b/odoo/addons/base/ir/ir_filters.xml index d21c6ad25d5..f46ec82c7f6 100644 --- a/odoo/addons/base/ir/ir_filters.xml +++ b/odoo/addons/base/ir/ir_filters.xml @@ -24,7 +24,7 @@ <record id="ir_filters_delete_own_rule" model="ir.rule"> <field name="name">ir.filters.own.rule.delete</field> <field name="model_id" ref="model_ir_filters"/> - <field name="domain_force">[('create_uid', '=', user.id)]</field> + <field name="domain_force">[('user_id', '=', user.id)]</field> <field name="groups" eval="[(4, ref('base.group_user'))]"/> <field name="perm_read" eval="False"/> <field name="perm_write" eval="False"/> diff --git a/odoo/addons/base/ir/ir_model.py b/odoo/addons/base/ir/ir_model.py index 98410ba110b..f67d9c1dc5f 100644 --- a/odoo/addons/base/ir/ir_model.py +++ b/odoo/addons/base/ir/ir_model.py @@ -9,6 +9,7 @@ from collections import defaultdict from odoo import api, fields, models, SUPERUSER_ID, tools, _ from odoo.exceptions import AccessError, UserError, ValidationError from odoo.modules.registry import Registry +from odoo.osv import expression from odoo.tools.safe_eval import safe_eval _logger = logging.getLogger(__name__) @@ -292,6 +293,25 @@ class IrModelFields(models.Model): raise UserError(_("The Selection Options expression is not a valid Pythonic expression." "Please provide an expression in the [('key','Label'), ...] format.")) + @api.constrains('name', 'state') + def _check_name(self): + for field in self: + if field.state == 'manual' and not field.name.startswith('x_'): + raise ValidationError(_("Custom fields must have a name that starts with 'x_' !")) + try: + models.check_pg_name(field.name) + except ValidationError: + msg = _("Field names can only contain characters, digits and underscores (up to 63).") + raise ValidationError(msg) + + @api.constrains('model', 'name') + def _unique_name(self): + # fix on stable branch (to be converted into an SQL constraint) + for field in self: + count = self.search_count([('model', '=', field.model), ('name', '=', field.name)]) + if count > 1: + raise ValidationError(_("Field names must be unique per model.")) + _sql_constraints = [ ('size_gt_zero', 'CHECK (size>=0)', 'Size of the field cannot be negative.'), ] @@ -449,6 +469,10 @@ class IrModelFields(models.Model): This method prevents the modification/deletion of many2one fields that have an inverse one2many, for instance. """ + self = self.filtered(lambda record: record.state == 'manual') + if not self: + return + for record in self: model = self.env[record.model] field = model._fields[record.name] @@ -462,6 +486,23 @@ class IrModelFields(models.Model): msg = _("The field '%s' cannot be removed because the field '%s' depends on it.") raise UserError(msg % (field, model._field_inverses[field][0])) + # remove fields from registry, and check that views are not broken + fields = [self.env[record.model]._pop_field(record.name) for record in self] + domain = expression.OR([('arch_db', 'like', record.name)] for record in self) + views = self.env['ir.ui.view'].search(domain) + try: + for view in views: + view._check_xml() + except Exception: + raise UserError("\n".join([ + _("Cannot rename/delete fields that are still present in views:"), + _("Fields:") + " " + ", ".join(map(str, fields)), + _("View:") + " " + view.name, + ])) + finally: + # the registry has been modified, restore it + self.pool.setup_models(self._cr) + @api.multi def unlink(self): if not self: @@ -504,9 +545,6 @@ class IrModelFields(models.Model): res = super(IrModelFields, self).create(vals) if vals.get('state', 'manual') == 'manual': - if not vals['name'].startswith('x_'): - raise UserError(_("Custom fields must have a name that starts with 'x_' !")) - if vals.get('relation') and not self.env['ir.model'].search([('model', '=', vals['relation'])]): raise UserError(_("Model %s does not exist!") % vals['relation']) @@ -569,12 +607,6 @@ class IrModelFields(models.Model): item._prepare_update() if column_rename: raise UserError(_('Can only rename one field at a time!')) - if vals['name'] in obj._fields: - raise UserError(_('Cannot rename field to %s, because that field already exists!') % vals['name']) - if vals.get('state', 'manual') == 'manual' and not vals['name'].startswith('x_'): - raise UserError(_('New field name must still start with x_ , because it is a custom field!')) - if '\'' in vals['name'] or '"' in vals['name'] or ';' in vals['name']: - raise ValueError('Invalid character in column name') column_rename = (obj._table, item.name, vals['name'], item.index) # We don't check the 'state', because it might come from the context diff --git a/odoo/addons/base/ir/ir_qweb/fields.py b/odoo/addons/base/ir/ir_qweb/fields.py index f0c7d576672..4beec41db7c 100644 --- a/odoo/addons/base/ir/ir_qweb/fields.py +++ b/odoo/addons/base/ir/ir_qweb/fields.py @@ -340,7 +340,7 @@ class MonetaryConverter(models.AbstractModel): if not options.get('display_currency') and field.type == 'monetary' and field.currency_field: options['display_currency'] = record[field.currency_field] - return self.value_to_html(record[field_name], options) + return super(MonetaryConverter, self).record_to_html(record, field_name, options) TIMEDELTA_UNITS = ( ('year', 3600 * 24 * 365), diff --git a/odoo/addons/base/res/res_company_view.xml b/odoo/addons/base/res/res_company_view.xml index 95bafe4f334..4746deeb1b1 100644 --- a/odoo/addons/base/res/res_company_view.xml +++ b/odoo/addons/base/res/res_company_view.xml @@ -1,7 +1,7 @@ <?xml version="1.0" encoding="UTF-8"?> <odoo> <data> - <report id="preview_rml_report" model="res.company" name="preview.report" menu="False" rml="base/report/preview_report.rml" string="Preview RML Report"/> + <report id="preview_rml_report" model="res.config.settings" name="preview.report" menu="False" rml="base/report/preview_report.rml" string="Preview RML Report"/> <record id="view_company_form" model="ir.ui.view"> <field name="name">res.company.form</field> diff --git a/odoo/addons/base/res/res_users.py b/odoo/addons/base/res/res_users.py index e92aaf5315e..da025eeca78 100644 --- a/odoo/addons/base/res/res_users.py +++ b/odoo/addons/base/res/res_users.py @@ -744,7 +744,7 @@ class GroupsView(models.Model): xml = E.field(E.group(*(xml1), col="2"), E.group(*(xml2), col="4"), name="groups_id", position="replace") xml.addprevious(etree.Comment("GENERATED AUTOMATICALLY BY GROUPS")) xml_content = etree.tostring(xml, pretty_print=True, xml_declaration=True, encoding="utf-8") - view.with_context(lang=None).write({'arch': xml_content}) + view.with_context(lang=None).write({'arch': xml_content, 'arch_fs': False}) def get_application_groups(self, domain): """ Return the non-share groups that satisfy ``domain``. """ diff --git a/odoo/addons/base/tests/test_expression.py b/odoo/addons/base/tests/test_expression.py index 932268d717c..104c4a6dfe1 100644 --- a/odoo/addons/base/tests/test_expression.py +++ b/odoo/addons/base/tests/test_expression.py @@ -683,8 +683,8 @@ class TestAutoJoin(TransactionCase): self.assertEqual(partners, p_aa, "_auto_join off: ('bank_ids.sanitized_acc_number', 'like', '..'): incorrect result") # Test produced queries - self.assertEqual(len(self.query_list), 3, - "_auto_join off: ('bank_ids.sanitized_acc_number', 'like', '..') should produce 3 queries (1 in res_partner_bank, 2 on res_partner)") + self.assertEqual(len(self.query_list), 2, + "_auto_join off: ('bank_ids.sanitized_acc_number', 'like', '..') should produce 2 queries (1 in res_partner_bank, 1 on res_partner)") sql_query = self.query_list[0].get_sql() self.assertIn('res_partner_bank', sql_query[0], "_auto_join off: ('bank_ids.sanitized_acc_number', 'like', '..') first query incorrect main table") @@ -692,16 +692,16 @@ class TestAutoJoin(TransactionCase): expected = "%s::text like %s" % (unaccent('"res_partner_bank"."sanitized_acc_number"'), unaccent('%s')) self.assertIn(expected, sql_query[1], "_auto_join off: ('bank_ids.sanitized_acc_number', 'like', '..') first query incorrect where condition") - + self.assertEqual(['%' + name_test + '%'], sql_query[2], "_auto_join off: ('bank_ids.sanitized_acc_number', 'like', '..') first query incorrect parameter") - sql_query = self.query_list[2].get_sql() + sql_query = self.query_list[1].get_sql() self.assertIn('res_partner', sql_query[0], - "_auto_join off: ('bank_ids.sanitized_acc_number', 'like', '..') third query incorrect main table") + "_auto_join off: ('bank_ids.sanitized_acc_number', 'like', '..') second query incorrect main table") self.assertIn('"res_partner"."id" in (%s)', sql_query[1], - "_auto_join off: ('bank_ids.sanitized_acc_number', 'like', '..') third query incorrect where condition") + "_auto_join off: ('bank_ids.sanitized_acc_number', 'like', '..') second query incorrect where condition") self.assertIn(p_aa.id, sql_query[2], - "_auto_join off: ('bank_ids.sanitized_acc_number', 'like', '..') third query incorrect parameter") + "_auto_join off: ('bank_ids.sanitized_acc_number', 'like', '..') second query incorrect parameter") # Do: cascaded one2many without _auto_join self._reinit_mock() @@ -710,8 +710,8 @@ class TestAutoJoin(TransactionCase): self.assertEqual(partners, p_a + p_b, "_auto_join off: ('child_ids.bank_ids.id', 'in', [..]): incorrect result") # Test produced queries - self.assertEqual(len(self.query_list), 5, - "_auto_join off: ('child_ids.bank_ids.id', 'in', [..]) should produce 5 queries (1 in res_partner_bank, 4 on res_partner)") + self.assertEqual(len(self.query_list), 3, + "_auto_join off: ('child_ids.bank_ids.id', 'in', [..]) should produce 3 queries (1 in res_partner_bank, 2 on res_partner)") # Do: one2many with _auto_join patch_auto_join(partner_obj, 'bank_ids', True) @@ -943,7 +943,7 @@ class TestAutoJoin(TransactionCase): self.assertLessEqual(p_a + p_b, partners, "_auto_join off: ('child_ids.state_id.country_id.code', 'like', '..') incorrect result") # Test produced queries - self.assertEqual(len(self.query_list), 5, + self.assertEqual(len(self.query_list), 4, "_auto_join off: ('child_ids.state_id.country_id.code', 'like', '..') number of queries incorrect") # Do: ('child_ids.state_id.country_id.code', 'like', '..') with _auto_join diff --git a/odoo/addons/base/tests/test_ir_actions.py b/odoo/addons/base/tests/test_ir_actions.py index 74d220b05bb..62711a8eab5 100644 --- a/odoo/addons/base/tests/test_ir_actions.py +++ b/odoo/addons/base/tests/test_ir_actions.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo.exceptions import ValidationError +from odoo.exceptions import UserError, ValidationError from odoo.tools import mute_logger import odoo.tests.common as common @@ -181,3 +181,112 @@ class TestServerActions(TestServerActionsBase): self.action.write({ 'child_ids': [(6, 0, [self.action.id])] }) + + +class TestCustomFields(common.TransactionCase): + MODEL = 'res.partner' + + def setUp(self): + # use a test cursor instead of a real cursor + registry = odoo.registry() + registry.enter_test_mode() + fnames = set(registry[self.MODEL]._fields) + + @self.addCleanup + def callback(): + registry.leave_test_mode() + # the tests may have modified the registry, reset it + with registry.cursor() as cr: + registry.clear_manual_fields() + registry.setup_models(cr) + assert set(registry[self.MODEL]._fields) == fnames + + super(TestCustomFields, self).setUp() + + # do not reload the registry after removing a field + self.env = self.env(context={'_force_unlink': True}) + + def create_field(self, name): + """ create a custom field and return it """ + model = self.env['ir.model'].search([('model', '=', self.MODEL)]) + field = self.env['ir.model.fields'].create({ + 'model_id': model.id, + 'name': name, + 'field_description': name, + 'ttype': 'char', + }) + self.assertIn(name, self.env[self.MODEL]._fields) + return field + + def create_view(self, name): + """ create a view with the given field name """ + return self.env['ir.ui.view'].create({ + 'name': 'yet another view', + 'model': self.MODEL, + 'arch': '<tree string="X"><field name="%s"/></tree>' % name, + }) + + def test_create_custom(self): + """ custom field names must be start with 'x_' """ + with self.assertRaises(ValidationError): + self.create_field('foo') + + def test_rename_custom(self): + """ custom field names must be start with 'x_' """ + field = self.create_field('x_foo') + with self.assertRaises(ValidationError): + field.name = 'foo' + + def test_create_valid(self): + """ field names must be valid pg identifiers """ + with self.assertRaises(ValidationError): + self.create_field('x_foo bar') + + def test_rename_valid(self): + """ field names must be valid pg identifiers """ + field = self.create_field('x_foo') + with self.assertRaises(ValidationError): + field.name = 'x_foo bar' + + def test_create_unique(self): + """ one cannot create two fields with the same name on a given model """ + self.create_field('x_foo') + with self.assertRaises(ValidationError): + self.create_field('x_foo') + + def test_rename_unique(self): + """ one cannot create two fields with the same name on a given model """ + field1 = self.create_field('x_foo') + field2 = self.create_field('x_bar') + with self.assertRaises(ValidationError): + field2.name = field1.name + + def test_remove_without_view(self): + """ try removing a custom field that does not occur in views """ + field = self.create_field('x_foo') + field.unlink() + + def test_rename_without_view(self): + """ try renaming a custom field that does not occur in views """ + field = self.create_field('x_foo') + field.name = 'x_bar' + + def test_remove_with_view(self): + """ try removing a custom field that occurs in a view """ + field = self.create_field('x_foo') + self.create_view('x_foo') + + # try to delete the field, this should fail but not modify the registry + with self.assertRaises(UserError): + field.unlink() + self.assertIn('x_foo', self.env[self.MODEL]._fields) + + def test_rename_with_view(self): + """ try renaming a custom field that occurs in a view """ + field = self.create_field('x_foo') + self.create_view('x_foo') + + # try to delete the field, this should fail but not modify the registry + with self.assertRaises(UserError): + field.name = 'x_bar' + self.assertIn('x_foo', self.env[self.MODEL]._fields) diff --git a/odoo/addons/test_new_api/ir.model.access.csv b/odoo/addons/test_new_api/ir.model.access.csv index 03ec40061ce..4e136481512 100644 --- a/odoo/addons/test_new_api/ir.model.access.csv +++ b/odoo/addons/test_new_api/ir.model.access.csv @@ -17,3 +17,4 @@ access_test_new_api_company,access_test_new_api_company,model_test_new_api_compa access_test_new_api_company_attr,access_test_new_api_company_attr,model_test_new_api_company_attr,,1,1,1,1 access_test_new_api_sparse,access_test_new_api_sparse,model_test_new_api_sparse,,1,1,1,1 access_test_new_api_compute_inverse,access_test_new_api_compute_inverse,model_test_new_api_compute_inverse,,1,1,1,1 +access_test_new_api_recursive,access_test_new_api_recursive,model_test_new_api_recursive,,1,1,1,1 diff --git a/odoo/addons/test_new_api/models.py b/odoo/addons/test_new_api/models.py index f719c696494..ad7092159a4 100644 --- a/odoo/addons/test_new_api/models.py +++ b/odoo/addons/test_new_api/models.py @@ -360,3 +360,19 @@ class Sparse(models.Model): char = fields.Char(sparse='data') selection = fields.Selection([('one', 'One'), ('two', 'Two')], sparse='data') partner = fields.Many2one('res.partner', sparse='data') + + +class ComputeRecursive(models.Model): + _name = 'test_new_api.recursive' + + name = fields.Char(required=True) + parent = fields.Many2one('test_new_api.recursive') + display_name = fields.Char(compute='_compute_display_name', store=True) + + @api.depends('name', 'parent.display_name') + def _compute_display_name(self): + for rec in self: + if rec.parent: + rec.display_name = rec.parent.display_name + " / " + rec.name + else: + rec.display_name = rec.name diff --git a/odoo/addons/test_new_api/tests/test_new_fields.py b/odoo/addons/test_new_api/tests/test_new_fields.py index 1326eb3da39..9deaee1a5f0 100644 --- a/odoo/addons/test_new_api/tests/test_new_fields.py +++ b/odoo/addons/test_new_api/tests/test_new_fields.py @@ -179,6 +179,29 @@ class TestFields(common.TransactionCase): cath.parent = finn self.assertEqual(ewan.display_name, "Gabriel / Finnley / Catherine / Ewan") + def test_12_recursive_recompute(self): + """ test recomputation on recursively dependent field """ + a = self.env['test_new_api.recursive'].create({'name': 'A'}) + b = self.env['test_new_api.recursive'].create({'name': 'B', 'parent': a.id}) + c = self.env['test_new_api.recursive'].create({'name': 'C', 'parent': b.id}) + d = self.env['test_new_api.recursive'].create({'name': 'D', 'parent': c.id}) + self.assertEqual(a.display_name, 'A') + self.assertEqual(b.display_name, 'A / B') + self.assertEqual(c.display_name, 'A / B / C') + self.assertEqual(d.display_name, 'A / B / C / D') + + b.parent = False + self.assertEqual(a.display_name, 'A') + self.assertEqual(b.display_name, 'B') + self.assertEqual(c.display_name, 'B / C') + self.assertEqual(d.display_name, 'B / C / D') + + b.name = 'X' + self.assertEqual(a.display_name, 'A') + self.assertEqual(b.display_name, 'X') + self.assertEqual(c.display_name, 'X / C') + self.assertEqual(d.display_name, 'X / C / D') + def test_12_cascade(self): """ test computed field depending on computed field """ message = self.env.ref('test_new_api.message_0_0') diff --git a/odoo/fields.py b/odoo/fields.py index a5404e3c367..b14be35ba54 100644 --- a/odoo/fields.py +++ b/odoo/fields.py @@ -708,6 +708,7 @@ class Field(object): model._field_triggers.add(field, (self, path_str)) elif path: self.recursive = True + model._field_triggers.add(field, (self, '.'.join(path))) ############################################################################ # diff --git a/odoo/models.py b/odoo/models.py index c6e265e4f86..56923cd1558 100644 --- a/odoo/models.py +++ b/odoo/models.py @@ -669,7 +669,7 @@ class BaseModel(object): field = cls._fields.get(name) if not field: _logger.warning("method %s.%s: @constrains parameter %r is not a field name", cls._name, attr, name) - if not (field.store or field.inverse): + elif not (field.store or field.inverse): _logger.warning("method %s.%s: @constrains parameter %r is not writeable", cls._name, attr, name) methods.append(func) @@ -4342,7 +4342,8 @@ class BaseModel(object): """ self.ensure_one() vals = self.copy_data(default)[0] - new = self.create(vals) + # To avoid to create a translation in the lang of the user, copy_translation will do it + new = self.with_context(lang=None).create(vals) self.copy_translations(new) return new diff --git a/odoo/modules/graph.py b/odoo/modules/graph.py index aaf90ae8443..ff2f99188fe 100644 --- a/odoo/modules/graph.py +++ b/odoo/modules/graph.py @@ -80,7 +80,7 @@ class Graph(dict): info = odoo.modules.module.load_information_from_description_file(module) if info and info['installable']: packages.append((module, info)) # TODO directly a dict, like in get_modules_with_version - else: + elif module != 'studio_customization': _logger.warning('module %s: not installable, skipped', module) dependencies = dict([(p, info['depends']) for p, info in packages]) @@ -112,10 +112,7 @@ class Graph(dict): unmet_deps = filter(lambda p: p not in self, dependencies[package]) _logger.error('module %s: Unmet dependencies: %s', package, ', '.join(unmet_deps)) - result = len(self) - len_graph - if result != len(module_list): - _logger.warning('Some modules were not loaded.') - return result + return len(self) - len_graph def __iter__(self): diff --git a/odoo/modules/module.py b/odoo/modules/module.py index ecf95be088b..d15df6dba59 100644 --- a/odoo/modules/module.py +++ b/odoo/modules/module.py @@ -302,7 +302,7 @@ def load_information_from_description_file(module, mod_path=None): :param mod_path: Physical path of module, if not providedThe name of the module (sale, purchase, ...) """ if not mod_path: - mod_path = get_module_path(module) + mod_path = get_module_path(module, downloaded=True) manifest_file = module_manifest(mod_path) if manifest_file: # default values for descriptor diff --git a/odoo/osv/expression.py b/odoo/osv/expression.py index eea14988eb7..bef1cb1c523 100644 --- a/odoo/osv/expression.py +++ b/odoo/osv/expression.py @@ -876,8 +876,7 @@ class expression(object): # Making search easier when there is a left operand as one2many or many2many elif len(path) > 1 and field.store and field.type in ('many2many', 'one2many'): right_ids = comodel.search([('.'.join(path[1:]), operator, right)]).ids - table_ids = model.with_context(active_test=False).search([(path[0], 'in', right_ids)]).ids - leaf.leaf = ('id', 'in', table_ids) + leaf.leaf = (path[0], 'in', right_ids) push(leaf) elif not field.store: @@ -980,15 +979,16 @@ class expression(object): rel_table, rel_id1, rel_id2 = field.relation, field.column1, field.column2 if operator in HIERARCHY_FUNCS: - def _rec_convert(ids): - if comodel == model: - return ids - return select_from_where(cr, rel_id1, rel_table, rel_id2, ids, operator) - ids2 = to_ids(right, comodel) dom = HIERARCHY_FUNCS[operator]('id', ids2, comodel) ids2 = comodel.search(dom).ids - push(create_substitution_leaf(leaf, ('id', 'in', _rec_convert(ids2)), model)) + if comodel == model: + push(create_substitution_leaf(leaf, ('id', 'in', ids2), model)) + else: + subquery = 'SELECT "%s" FROM "%s" WHERE "%s" IN %%s' % (rel_id1, rel_table, rel_id2) + # avoid flattening of argument in to_sql() + subquery = cr.mogrify(subquery, [tuple(ids2)]) + push(create_substitution_leaf(leaf, ('id', 'inselect', (subquery, [])), internal=True)) else: call_null_m2m = True if right is not False: @@ -1014,8 +1014,11 @@ class expression(object): operator = 'in' # operator changed because ids are directly related to main object else: call_null_m2m = False - m2m_op = 'not in' if operator in NEGATIVE_TERM_OPERATORS else 'in' - push(create_substitution_leaf(leaf, ('id', m2m_op, select_from_where(cr, rel_id1, rel_table, rel_id2, res_ids, operator) or [0]), model)) + subop = 'not inselect' if operator in NEGATIVE_TERM_OPERATORS else 'inselect' + subquery = 'SELECT "%s" FROM "%s" WHERE "%s" IN %%s' % (rel_id1, rel_table, rel_id2) + # avoid flattening of argument in to_sql() + subquery = cr.mogrify(subquery, [tuple(filter(None, res_ids))]) + push(create_substitution_leaf(leaf, ('id', subop, (subquery, [])), internal=True)) if call_null_m2m: m2m_op = 'in' if operator in NEGATIVE_TERM_OPERATORS else 'not in' |
