[REF] models: import records in batch

Create new methods on `BaseModel` to create records with given xml ids in
batch.  Those methods replace the methods `_update` and `_update_dummy` of
model 'ir.model.data', which are now deprecated.

Adapt XML and CSV import code to create records in batch.
This commit is contained in:
Raphael Collet
2018-07-24 16:58:14 +02:00
parent 896e36edab
commit d5b687a48f
12 changed files with 522 additions and 318 deletions
+30 -27
View File
@@ -4,6 +4,7 @@ from odoo.exceptions import AccessError
from odoo import api, fields, models, _
from odoo import SUPERUSER_ID
from odoo.exceptions import UserError
from odoo.tools import pycompat
import logging
@@ -390,8 +391,35 @@ class AccountChartTemplate(models.Model):
# xmlid is the concatenation of company_id and template_xml_id
ir_model_data = self.env['ir.model.data']
template_xmlid = ir_model_data.search([('model', '=', template._name), ('res_id', '=', template.id)])
new_xml_id = str(company.id)+'_'+template_xmlid.name
return ir_model_data._update(model, template_xmlid.module, vals, xml_id=new_xml_id, store=True, noupdate=True, mode='init', res_id=False)
xml_id = "%s.%s_%s" % (template_xmlid.module, company.id, template_xmlid.name)
data = dict(xml_id=xml_id, values=vals, noupdate=True)
record = self.env[model]._load_records([data])
return record.id
@api.model
def _load_records(self, data_list, update=False):
# When creating a chart template create, for the liquidity transfer account
# - an account.account.template: this allow to define account.reconcile.model.template objects refering that liquidity transfer
# account although it's not existing in any xml file
# - an entry in ir_model_data: this allow to still use the method create_record_with_xmlid() and don't make any difference between
# regular accounts created and that liquidity transfer account
records = super(AccountChartTemplate, self)._load_records(data_list, update)
account_data_list = []
for data, record in pycompat.izip(data_list, records):
# Create the transfer account only for leaf chart template in the hierarchy.
if record.parent_id:
continue
if data.get('xml_id'):
account_xml_id = data['xml_id'] + '_liquidity_transfer'
if not self.env.ref(account_xml_id, raise_if_not_found=False):
account_vals = record._prepare_transfer_account_template()
account_data_list.append(dict(
xml_id=account_xml_id,
values=account_vals,
noupdate=data.get('noupdate'),
))
self.env['account.account.template']._load_records(account_data_list, update)
return records
def _get_account_vals(self, company, account_template, code_acc, tax_template_ref):
""" This method generates a dictionary of all the values for the account that will be created.
@@ -1036,28 +1064,3 @@ class AccountReconcileModelTemplate(models.Model):
], string="Second Amount type",required=True, default='percentage')
second_amount = fields.Float(string='Second Amount', digits=0, required=True, default=100.0, help="Fixed amount will count as a debit if it is negative, as a credit if it is positive.")
second_tax_id = fields.Many2one('account.tax.template', string='Second Tax', ondelete='restrict', domain=[('type_tax_use', '=', 'purchase')])
class IrModelData(models.Model):
_inherit = 'ir.model.data'
@api.model
def _update(self, model, module, values, xml_id=False, store=True, noupdate=False, mode='init', res_id=False):
record_id = super(IrModelData, self)._update(model, module, values, xml_id=xml_id, store=store, noupdate=noupdate, mode=mode, res_id=res_id)
# When creating a chart template create, for the liquidity transfer account
# - an account.account.template: this allow to define account.reconcile.model.template objects refering that liquidity transfer
# account although it's not existing in any xml file
# - an entry in ir_model_data: this allow to still use the method create_record_with_xmlid() and don't make any difference between
# regular accounts created and that liquidity transfer account
if model == 'account.chart.template' and xml_id and module:
chart_template = self.env[model].browse(record_id)
# Create the transfer account only for leaf chart template in the hierarchy.
if chart_template.parent_id:
return record_id
new_xml_id = xml_id + '_liquidity_transfer'
if not self.search([('model', '=', 'account.account.template'), ('module', '=', module), ('name', '=', new_xml_id)]):
vals = chart_template._prepare_transfer_account_template()
self._update('account.account.template', module, vals, xml_id=new_xml_id, store=True, noupdate=noupdate, mode=mode, res_id=False)
return record_id
+6 -9
View File
@@ -83,15 +83,12 @@
<field name="sequence">0</field>
</record>
<function model="ir.model.data" name="_update">
<value>stock.picking.type</value>
<value>mrp</value>
<value eval="{}"/>
<value>picking_type_manufacturing</value>
<value eval="True"/>
<value eval="True"/>
<value>init</value>
<value model="stock.warehouse" eval="obj().env.ref('stock.warehouse0').manu_type_id.id"/>
<function model="ir.model.data" name="_update_xmlids">
<value model="base" eval="[{
'xml_id': 'mrp.picking_type_manufacturing',
'record': obj().env.ref('stock.warehouse0').manu_type_id,
'noupdate': True,
}]"/>
</function>
</data>
+30 -69
View File
@@ -144,75 +144,36 @@
</record>
<!-- create xml ids for demo data that are widely used in tests or in other codes, for more convenience -->
<function model="ir.model.data" name="_update">
<value>stock.location</value>
<value>stock</value>
<value eval="{}"/>
<value>stock_location_stock</value>
<value eval="True"/>
<value eval="True"/>
<value>init</value>
<value model="stock.warehouse" eval="obj().env.ref('stock.warehouse0').lot_stock_id.id"/>
</function>
<function model="ir.model.data" name="_update">
<value>stock.location</value>
<value>stock</value>
<value eval="{}"/>
<value>stock_location_company</value>
<value eval="True"/>
<value eval="True"/>
<value>init</value>
<value model="stock.warehouse" eval="obj().env.ref('stock.warehouse0').wh_input_stock_loc_id.id"/>
</function>
<function model="ir.model.data" name="_update">
<value>stock.location</value>
<value>stock</value>
<value eval="{}"/>
<value>stock_location_output</value>
<value eval="True"/>
<value eval="True"/>
<value>init</value>
<value model="stock.warehouse" eval="obj().env.ref('stock.warehouse0').wh_output_stock_loc_id.id"/>
</function>
<function model="ir.model.data" name="_update">
<value>stock.location</value>
<value>stock</value>
<value eval="{}"/>
<value>location_pack_zone</value>
<value eval="True"/>
<value eval="True"/>
<value>init</value>
<value model="stock.warehouse" eval="obj().env.ref('stock.warehouse0').wh_pack_stock_loc_id.id"/>
</function>
<function model="ir.model.data" name="_update">
<value>stock.picking.type</value>
<value>stock</value>
<value eval="{}"/>
<value>picking_type_internal</value>
<value eval="True"/>
<value eval="True"/>
<value>init</value>
<value model="stock.warehouse" eval="obj().env.ref('stock.warehouse0').int_type_id.id"/>
</function>
<function model="ir.model.data" name="_update">
<value>stock.picking.type</value>
<value>stock</value>
<value eval="{}"/>
<value>picking_type_in</value>
<value eval="True"/>
<value eval="True"/>
<value>init</value>
<value model="stock.warehouse" eval="obj().env.ref('stock.warehouse0').in_type_id.id"/>
</function>
<function model="ir.model.data" name="_update">
<value>stock.picking.type</value>
<value>stock</value>
<value eval="{}"/>
<value>picking_type_out</value>
<value eval="True"/>
<value eval="True"/>
<value>init</value>
<value model="stock.warehouse" eval="obj().env.ref('stock.warehouse0').out_type_id.id"/>
<function model="ir.model.data" name="_update_xmlids">
<value model="base" eval="[{
'xml_id': 'stock.stock_location_stock',
'record': obj().env.ref('stock.warehouse0').lot_stock_id,
'noupdate': True,
}, {
'xml_id': 'stock.stock_location_company',
'record': obj().env.ref('stock.warehouse0').wh_input_stock_loc_id,
'noupdate': True,
}, {
'xml_id': 'stock.stock_location_output',
'record': obj().env.ref('stock.warehouse0').wh_output_stock_loc_id,
'noupdate': True,
}, {
'xml_id': 'stock.location_pack_zone',
'record': obj().env.ref('stock.warehouse0').wh_pack_stock_loc_id,
'noupdate': True,
}, {
'xml_id': 'stock.picking_type_internal',
'record': obj().env.ref('stock.warehouse0').int_type_id,
'noupdate': True,
}, {
'xml_id': 'stock.picking_type_in',
'record': obj().env.ref('stock.warehouse0').in_type_id,
'noupdate': True,
}, {
'xml_id': 'stock.picking_type_out',
'record': obj().env.ref('stock.warehouse0').out_type_id,
'noupdate': True,
}]"/>
</function>
<!-- create the transit location for each company existing -->
+14 -29
View File
@@ -1,35 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<function model="ir.model.data" name="_update">
<value>stock.picking.type</value>
<value>stock</value>
<value eval="{}"/>
<value>chi_picking_type_in</value>
<value eval="True"/>
<value eval="True"/>
<value>init</value>
<value model="stock.warehouse" eval="obj().env.ref('stock.stock_warehouse_shop0').in_type_id.id"/>
</function>
<function model="ir.model.data" name="_update">
<value>stock.picking.type</value>
<value>stock</value>
<value eval="{}"/>
<value>chi_picking_type_out</value>
<value eval="True"/>
<value eval="True"/>
<value>init</value>
<value model="stock.warehouse" eval="obj().env.ref('stock.stock_warehouse_shop0').out_type_id.id"/>
</function>
<function model="ir.model.data" name="_update">
<value>stock.location</value>
<value>stock</value>
<value eval="{}"/>
<value>stock_location_shop0</value>
<value eval="True"/>
<value eval="True"/>
<value>init</value>
<value model="stock.warehouse" eval="obj().env.ref('stock.stock_warehouse_shop0').lot_stock_id.id"/>
<function model="ir.model.data" name="_update_xmlids">
<value model="base" eval="[{
'xml_id': 'stock.chi_picking_type_in',
'record': obj().env.ref('stock.stock_warehouse_shop0').in_type_id,
'noupdate': True,
}, {
'xml_id': 'stock.chi_picking_type_out',
'record': obj().env.ref('stock.stock_warehouse_shop0').out_type_id,
'noupdate': True,
}, {
'xml_id': 'stock.stock_location_shop0',
'record': obj().env.ref('stock.stock_warehouse_shop0').lot_stock_id,
'noupdate': True,
}]"/>
</function>
<record id="location_refrigerator_small" model="stock.location">
+2 -1
View File
@@ -84,10 +84,11 @@ def _configure_journals(cr, registry):
if not account_id:
account_id = env['account.account'].search([('user_type_id', '=', env.ref('account.data_account_type_current_assets').id)], limit=1).id
if account_id:
xml_id = 'stock_account.property_stock_valuation_account_id'
vals = {
'name': 'property_stock_valuation_account_id',
'fields_id': fields_id,
'value': 'account.account,'+str(account_id),
'company_id': env.ref('base.main_company').id,
}
env['ir.model.data']._update('ir.property', 'stock_account', vals, 'property_stock_valuation_account_id')
env['ir.property']._load_records([dict(xml_id=xml_id, values=vals)])
+7 -4
View File
@@ -297,6 +297,10 @@ class IrFieldsConverter(models.AbstractModel):
warnings
:rtype: (ID|None, unicode, list)
"""
# the function 'flush' comes from BaseModel.load(), and forces the
# creation/update of former records (batch creation)
flush = self._context.get('import_flush', lambda arg=None: None)
id = None
warnings = []
action = {'type': 'ir.actions.act_window', 'target': 'new',
@@ -330,12 +334,11 @@ class IrFieldsConverter(models.AbstractModel):
xmlid = value
else:
xmlid = "%s.%s" % (self._context.get('_import_current_module', ''), value)
try:
id = self.env.ref(xmlid).id
except ValueError:
pass # leave id is None
flush(xmlid)
id = getattr(self.env.ref(xmlid, raise_if_not_found=False), 'id', None)
elif subfield is None:
field_type = _(u"name")
flush()
ids = RelatedModel.name_search(name=value, operator='=')
if ids:
if len(ids) > 1:
+87 -122
View File
@@ -1403,139 +1403,104 @@ class IrModelData(models.Model):
"""
return self.xmlid_to_object("%s.%s" % (module, xml_id), raise_if_not_found=True)
@api.model
def _update_dummy(self, model, module, xml_id=False, store=True):
if xml_id:
try:
# One step to check the ID is defined and the record actually exists
record = self.get_object(module, xml_id)
if record:
self.loads[(module, xml_id)] = (model, record.id)
for parent_model, parent_field in self.env[model]._inherits.items():
parent = record[parent_field]
parent_xid = '%s_%s' % (xml_id, parent_model.replace('.', '_'))
self.loads[(module, parent_xid)] = (parent_model, parent.id)
return record.id
except Exception:
pass
return False
@api.multi
def unlink(self):
""" Regular unlink method, but make sure to clear the caches. """
self.clear_caches()
return super(IrModelData, self).unlink()
def _lookup_xmlids(self, xml_ids, model):
""" Look up the given XML ids of the given model. """
if not xml_ids:
return []
# group xml_ids by prefix
bymodule = defaultdict(set)
for xml_id in xml_ids:
prefix, suffix = xml_id.split('.', 1)
bymodule[prefix].add(suffix)
# query xml_ids by prefix
result = []
cr = self.env.cr
for prefix, suffixes in bymodule.items():
query = """
SELECT d.id, d.module, d.name, d.model, d.res_id, d.noupdate, r.id
FROM ir_model_data d LEFT JOIN "{}" r on d.res_id=r.id
WHERE d.module=%s AND d.name IN %s
""".format(model._table)
for subsuffixes in cr.split_for_in_conditions(suffixes):
cr.execute(query, (prefix, subsuffixes))
result.extend(cr.fetchall())
return result
def _generate_xmlids(self, xml_id, model):
""" Return all the XML ids to create for the given model. """
yield xml_id
for parent_model in model._inherits:
yield '%s_%s' % (xml_id, parent_model.replace('.', '_'))
@api.model
def _update(self, model, module, values, xml_id=False, store=True, noupdate=False, mode='init', res_id=False):
# records created during module install should not display the messages of OpenChatter
self = self.with_context(install_mode=True)
current_module = module
def _update_xmlids(self, data_list, update=False):
""" Create or update the given XML ids.
if xml_id and ('.' in xml_id):
assert len(xml_id.split('.')) == 2, _("'%s' contains too many dots. XML ids should not contain dots ! These are used to refer to other modules data, as in module.reference_id") % xml_id
module, xml_id = xml_id.split('.')
:param data_list: list of dicts with keys `xml_id` (XMLID to
assign), `noupdate` (flag on XMLID), `record` (target record).
:param update: should be ``True`` when upgrading a module
"""
if not data_list:
return
action = self.browse()
record = self.env[model].browse(res_id)
if xml_id:
self._cr.execute("""SELECT imd.id, imd.res_id, md.id, imd.model, imd.noupdate
FROM ir_model_data imd LEFT JOIN %s md ON (imd.res_id = md.id)
WHERE imd.module=%%s AND imd.name=%%s""" % record._table,
(module, xml_id))
results = self._cr.fetchall()
for imd_id, imd_res_id, real_id, imd_model, imd_noupdate in results:
# In update mode, do not update a record if it's ir.model.data is flagged as noupdate
if mode == 'update' and imd_noupdate:
return imd_res_id
if not real_id:
self.clear_caches()
self._cr.execute('DELETE FROM ir_model_data WHERE id=%s', (imd_id,))
record = record.browse()
else:
assert model == imd_model, "External ID conflict, %s already refers to a `%s` record,"\
" you can't define a `%s` record with this ID." % (xml_id, imd_model, model)
action = self.browse(imd_id)
record = record.browse(imd_res_id)
if action and record:
record.write(values)
action.sudo().write({'date_update': fields.Datetime.now()})
elif record:
record.write(values)
if xml_id:
for parent_model, parent_field in record._inherits.items():
self.sudo().create({
'name': xml_id + '_' + parent_model.replace('.', '_'),
'model': parent_model,
'module': module,
'res_id': record[parent_field].id,
'noupdate': noupdate,
})
self.sudo().create({
'name': xml_id,
'model': model,
'module': module,
'res_id': record.id,
'noupdate': noupdate,
})
elif mode == 'init' or (mode == 'update' and xml_id):
existing_parents = set() # {parent_model, ...}
if xml_id:
for parent_model, parent_field in record._inherits.items():
xid = self.sudo().search([
('module', '=', module),
('name', '=', xml_id + '_' + parent_model.replace('.', '_')),
])
# XML ID found in the database, try to recover an existing record
if xid:
parent = self.env[xid.model].browse(xid.res_id)
if parent.exists():
existing_parents.add(xid.model)
values[parent_field] = parent.id
else:
xid.unlink()
record = record.create(values)
if xml_id:
#To add an external identifiers to all inherits model
inherit_models = [record]
while inherit_models:
current_model = inherit_models.pop()
for parent_model_name, parent_field in current_model._inherits.items():
inherit_models.append(self.env[parent_model_name])
if parent_model_name in existing_parents:
continue
self.sudo().create({
'name': xml_id + '_' + parent_model_name.replace('.', '_'),
'model': parent_model_name,
'module': module,
'res_id': record[parent_field].id,
'noupdate': noupdate,
})
existing_parents.add(parent_model_name)
self.sudo().create({
'name': xml_id,
'model': model,
'module': module,
'res_id': record.id,
'noupdate': noupdate
})
if current_module and module != current_module:
_logger.warning("Creating the ir.model.data %s in module %s instead of %s.",
xml_id, module, current_module)
if xml_id and record:
self.loads[(module, xml_id)] = (model, record.id)
# rows to insert
rowf = "(%s, %s, %s, %s, %s, now() at time zone 'UTC', now() at time zone 'UTC')"
rows = set()
for data in data_list:
prefix, suffix = data['xml_id'].split('.', 1)
record = data['record']
noupdate = bool(data.get('noupdate'))
rows.add((prefix, suffix, record._name, record.id, noupdate))
# also create XML ids for parent records
for parent_model, parent_field in record._inherits.items():
parent_xml_id = xml_id + '_' + parent_model.replace('.', '_')
self.loads[(module, parent_xml_id)] = (parent_model, record[parent_field].id)
parent = record[parent_field]
puffix = suffix + '_' + parent_model.replace('.', '_')
rows.add((prefix, puffix, parent._name, parent.id, noupdate))
return record.id
for sub_rows in self.env.cr.split_for_in_conditions(rows):
# insert rows or update them
query = """
INSERT INTO ir_model_data (module, name, model, res_id, noupdate, date_init, date_update)
VALUES {rows}
ON CONFLICT (module, name)
DO UPDATE SET date_update=(now() at time zone 'UTC') {where}
""".format(
rows=", ".join([rowf] * len(sub_rows)),
where="WHERE NOT ir_model_data.noupdate" if update else "",
)
try:
self.env.cr.execute(query, [arg for row in sub_rows for arg in row])
except Exception:
_logger.error("Failed to insert ir_model_data\n%s", "\n".join(str(row) for row in sub_rows))
raise
# update self.loads
for prefix, suffix, res_model, res_id, noupdate in rows:
self.loads[(prefix, suffix)] = (res_model, res_id)
@api.model
def _load_xmlid(self, xml_id):
""" Simply mark the given XML id as being loaded, and return the
corresponding record.
"""
record = self.xmlid_to_object(xml_id)
if record:
prefix, suffix = xml_id.split('.', 1)
self.loads[(prefix, suffix)] = (record._name, record.id)
for parent_model, parent_field in record._inherits.items():
parent = record[parent_field]
puffix = suffix + '_' + parent_model.replace('.', '_')
self.loads[(prefix, puffix)] = (parent._name, parent.id)
return record
@api.model
def _module_data_uninstall(self, modules_to_remove):
+1
View File
@@ -10,6 +10,7 @@ from . import test_ir_actions
from . import test_ir_attachment
from . import test_ir_http
from . import test_ir_filters
from . import test_ir_model
from . import test_ir_sequence
from . import test_ir_sequence_date_range
from . import test_ir_default
+157
View File
@@ -0,0 +1,157 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests.common import TransactionCase
class TestXMLID(TransactionCase):
def get_data(self, xml_id):
""" Return the 'ir.model.data' record corresponding to ``xml_id``. """
module, suffix = xml_id.split('.', 1)
domain = [('module', '=', module), ('name', '=', suffix)]
return self.env['ir.model.data'].search(domain)
def test_create(self):
model = self.env['res.partner.category']
xml_id = 'test_convert.category_foo'
# create category (flag 'noupdate' should be False by default)
data = dict(xml_id=xml_id, values={'name': 'Foo'})
category = model._load_records([data])
self.assertEqual(category, self.env.ref(xml_id, raise_if_not_found=False))
self.assertEqual(category.name, 'Foo')
self.assertEqual(self.get_data(xml_id).noupdate, False)
# update category
data = dict(xml_id=xml_id, values={'name': 'Bar'})
category1 = model._load_records([data], update=True)
self.assertEqual(category, category1)
self.assertEqual(category.name, 'Bar')
self.assertEqual(self.get_data(xml_id).noupdate, False)
# update category
data = dict(xml_id=xml_id, values={'name': 'Baz'}, noupdate=True)
category2 = model._load_records([data], update=True)
self.assertEqual(category, category2)
self.assertEqual(category.name, 'Baz')
self.assertEqual(self.get_data(xml_id).noupdate, False)
def test_create_noupdate(self):
model = self.env['res.partner.category']
xml_id = 'test_convert.category_foo'
# create category
data = dict(xml_id=xml_id, values={'name': 'Foo'}, noupdate=True)
category = model._load_records([data])
self.assertEqual(category, self.env.ref(xml_id, raise_if_not_found=False))
self.assertEqual(category.name, 'Foo')
self.assertEqual(self.get_data(xml_id).noupdate, True)
# update category
data = dict(xml_id=xml_id, values={'name': 'Bar'}, noupdate=False)
category1 = model._load_records([data], update=True)
self.assertEqual(category, category1)
self.assertEqual(category.name, 'Foo')
self.assertEqual(self.get_data(xml_id).noupdate, True)
# update category
data = dict(xml_id=xml_id, values={'name': 'Baz'}, noupdate=True)
category2 = model._load_records([data], update=True)
self.assertEqual(category, category2)
self.assertEqual(category.name, 'Foo')
self.assertEqual(self.get_data(xml_id).noupdate, True)
def test_create_noupdate_multi(self):
model = self.env['res.partner.category']
data_list = [
dict(xml_id='test_convert.category_foo', values={'name': 'Foo'}, noupdate=True),
dict(xml_id='test_convert.category_bar', values={'name': 'Bar'}, noupdate=True),
]
# create category
categories = model._load_records(data_list)
foo = self.env.ref('test_convert.category_foo')
bar = self.env.ref('test_convert.category_bar')
self.assertEqual(categories, foo + bar)
self.assertEqual(foo.name, 'Foo')
self.assertEqual(bar.name, 'Bar')
# check data
self.assertEqual(self.get_data('test_convert.category_foo').noupdate, True)
self.assertEqual(self.get_data('test_convert.category_bar').noupdate, True)
def test_create_order(self):
model = self.env['res.partner.category']
data_list = [
dict(xml_id='test_convert.category_foo', values={'name': 'Foo'}),
dict(xml_id='test_convert.category_bar', values={'name': 'Bar'}, noupdate=True),
dict(xml_id='test_convert.category_baz', values={'name': 'Baz'}),
]
# create categories
foo = model._load_records([data_list[0]])
bar = model._load_records([data_list[1]])
baz = model._load_records([data_list[2]])
self.assertEqual(foo.name, 'Foo')
self.assertEqual(bar.name, 'Bar')
self.assertEqual(baz.name, 'Baz')
# update them, and check the order of result
for data in data_list:
data['values']['name'] += 'X'
cats = model._load_records(data_list, update=True)
self.assertEqual(list(cats), [foo, bar, baz])
self.assertEqual(foo.name, 'FooX')
self.assertEqual(bar.name, 'Bar')
self.assertEqual(baz.name, 'BazX')
def test_create_inherits(self):
model = self.env['res.users']
xml_id = 'test_convert.user_foo'
par_xml_id = xml_id + '_res_partner'
# create user
user = model._load_records([dict(xml_id=xml_id, values={'name': 'Foo', 'login': 'foo'})])
self.assertEqual(user, self.env.ref(xml_id, raise_if_not_found=False))
self.assertEqual(user.partner_id, self.env.ref(par_xml_id, raise_if_not_found=False))
self.assertEqual(user.name, 'Foo')
self.assertEqual(user.login, 'foo')
def test_recreate(self):
model = self.env['res.partner.category']
xml_id = 'test_convert.category_foo'
data = dict(xml_id=xml_id, values={'name': 'Foo'})
# create category
category = model._load_records([data])
self.assertEqual(category, self.env.ref(xml_id, raise_if_not_found=False))
self.assertEqual(category.name, 'Foo')
# suppress category
category.unlink()
self.assertFalse(self.env.ref(xml_id, raise_if_not_found=False))
# update category, this should recreate it
category = model._load_records([data], update=True)
self.assertEqual(category, self.env.ref(xml_id, raise_if_not_found=False))
self.assertEqual(category.name, 'Foo')
def test_create_xmlids(self):
# create users and assign them xml ids
foo, bar = self.env['res.users'].create([
{'name': 'Foo', 'login': 'foo'},
{'name': 'Bar', 'login': 'bar'},
])
self.env['ir.model.data']._update_xmlids([
dict(xml_id='test_convert.foo', record=foo, noupdate=True),
dict(xml_id='test_convert.bar', record=bar, noupdate=True),
])
self.assertEqual(foo, self.env.ref('test_convert.foo', raise_if_not_found=False))
self.assertEqual(bar, self.env.ref('test_convert.bar', raise_if_not_found=False))
self.assertEqual(foo.partner_id, self.env.ref('test_convert.foo_res_partner', raise_if_not_found=False))
self.assertEqual(bar.partner_id, self.env.ref('test_convert.bar_res_partner', raise_if_not_found=False))
self.assertEqual(self.get_data('test_convert.foo').noupdate, True)
self.assertEqual(self.get_data('test_convert.bar').noupdate, True)
+5 -11
View File
@@ -8,25 +8,19 @@ class TestHasGroup(TransactionCase):
def setUp(self):
super(TestHasGroup, self).setUp()
group0 = self.env['ir.model.data']._update(
'res.groups', 'test_user_has_group',
{'name': 'group0'},
xml_id='group0'
)
self.group0 = 'test_user_has_group.group0'
self.env['ir.model.data']._update(
'res.groups', 'test_user_has_group',
{'name': 'group1'},
xml_id='group1'
)
self.group1 = 'test_user_has_group.group1'
group0, group1 = self.env['res.groups']._load_records([
dict(xml_id=self.group0, values={'name': 'group0'}),
dict(xml_id=self.group1, values={'name': 'group1'}),
])
self.test_user = self.env['res.users'].create({
'login': 'testuser',
'partner_id': self.env['res.partner'].create({
'name': "Strawman Test User"
}).id,
'groups_id': [(4, group0, 0)]
'groups_id': [(4, group0.id, 0)]
})
def test_env_uid(self):
+156 -21
View File
@@ -840,36 +840,84 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
messages = []
ModelData = self.env['ir.model.data']
ModelData.clear_caches()
extracted = self._extract_records(fields, data, log=messages.append)
converted = self._convert_records(extracted, log=messages.append)
for id, xid, record, info in converted:
# list of (xid, vals, info) for records to be created in batch
batch = []
batch_xml_ids = set()
def flush(xml_id=None):
if not batch:
return
if xml_id and xml_id not in batch_xml_ids:
return
data_list = [
dict(xml_id=xid, values=vals, info=info, noupdate=noupdate)
for xid, vals, info in batch
]
batch.clear()
batch_xml_ids.clear()
try:
cr.execute('SAVEPOINT model_load_save')
except psycopg2.InternalError as e:
# broken transaction, exit and hope the source error was
# already logged
if not any(message['type'] == 'error' for message in messages):
messages.append(dict(info, type='error',message=u"Unknown database error: '%s'" % e))
break
info = data_list[0]['info']
messages.append(dict(info, type='error', message=u"Unknown database error: '%s'" % e))
return
# try to create in batch
try:
ids.append(ModelData._update(self._name, current_module, record, mode=mode,
xml_id=xid, noupdate=noupdate, res_id=id))
recs = self._load_records(data_list, mode == 'update')
ids.extend(recs.ids)
cr.execute('RELEASE SAVEPOINT model_load_save')
except psycopg2.Warning as e:
messages.append(dict(info, type='warning', message=str(e)))
cr.execute('ROLLBACK TO SAVEPOINT model_load_save')
except psycopg2.Error as e:
messages.append(dict(info, type='error', **PGERROR_TO_OE[e.pgcode](self, fg, info, e)))
# Failed to write, log to messages, rollback savepoint (to
# avoid broken transaction) and keep going
cr.execute('ROLLBACK TO SAVEPOINT model_load_save')
except Exception as e:
message = (_(u'Unknown error during import:') + u' %s: %s' % (type(e), e))
moreinfo = _('Resolve other errors first')
messages.append(dict(info, type='error', message=message, moreinfo=moreinfo))
# Failed for some reason, perhaps due to invalid data supplied,
# rollback savepoint and keep going
return
except Exception:
cr.execute('ROLLBACK TO SAVEPOINT model_load_save')
# try again, this time record by record
for rec_data in data_list:
try:
cr.execute('SAVEPOINT model_load_save')
rec = self._load_records([rec_data], mode == 'update')
ids.append(rec.id)
cr.execute('RELEASE SAVEPOINT model_load_save')
except psycopg2.Warning as e:
info = rec_data['info']
messages.append(dict(info, type='warning', message=str(e)))
cr.execute('ROLLBACK TO SAVEPOINT model_load_save')
except psycopg2.Error as e:
info = rec_data['info']
messages.append(dict(info, type='error', **PGERROR_TO_OE[e.pgcode](self, fg, info, e)))
# Failed to write, log to messages, rollback savepoint (to
# avoid broken transaction) and keep going
cr.execute('ROLLBACK TO SAVEPOINT model_load_save')
except Exception as e:
info = rec_data['info']
message = (_(u'Unknown error during import:') + u' %s: %s' % (type(e), e))
moreinfo = _('Resolve other errors first')
messages.append(dict(info, type='error', message=message, moreinfo=moreinfo))
# Failed for some reason, perhaps due to invalid data supplied,
# rollback savepoint and keep going
cr.execute('ROLLBACK TO SAVEPOINT model_load_save')
# make 'flush' available to the methods below, in the case where XMLID
# resolution fails, for instance
flush_self = self.with_context(import_flush=flush)
extracted = flush_self._extract_records(fields, data, log=messages.append)
converted = flush_self._convert_records(extracted, log=messages.append)
for id, xid, record, info in converted:
if xid:
xid = xid if '.' in xid else "%s.%s" % (current_module, xid)
batch_xml_ids.update(ModelData._generate_xmlids(xid, self))
elif id:
record['id'] = id
batch.append((xid, record, info))
flush()
if any(message['type'] == 'error' for message in messages):
cr.execute('ROLLBACK TO SAVEPOINT model_load')
ids = False
@@ -3568,6 +3616,93 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
modified_ids = {row[0] for row in cr.fetchall()}
self.browse(modified_ids).modified(['parent_path'])
def _load_records(self, data_list, update=False):
""" Create or update records of this model, and assign XMLIDs.
:param data_list: list of dicts with keys `xml_id` (XMLID to
assign), `noupdate` (flag on XMLID), `values` (field values)
:param update: should be ``True`` when upgrading a module
:return: the records corresponding to ``data_list``
"""
original_self = self.browse()
# records created during installation should not display messages
self = self.with_context(install_mode=True)
imd = self.env['ir.model.data'].sudo()
# The algorithm below partitions 'data_list' into three sets: the ones
# to create, the ones to update, and the others. For each set, we assign
# data['record'] for each data. All those records are then retrieved for
# the result.
# determine existing xml_ids
xml_ids = [data['xml_id'] for data in data_list if data.get('xml_id')]
existing = {
("%s.%s" % row[1:3]): row
for row in imd._lookup_xmlids(xml_ids, self)
}
# determine which records to create and update
to_create = [] # list of data
to_update = [] # list of data
for data in data_list:
xml_id = data.get('xml_id')
if not xml_id:
vals = data['values']
if vals.get('id'):
data['record'] = self.browse(vals['id'])
to_update.append(data)
elif not update:
to_create.append(data)
continue
row = existing.get(xml_id)
if not row:
to_create.append(data)
continue
d_id, d_module, d_name, d_model, d_res_id, d_noupdate, r_id = row
record = self.browse(d_res_id)
if update and d_noupdate:
data['record'] = record
elif r_id:
data['record'] = record
to_update.append(data)
else:
imd.browse(d_id).unlink()
to_create.append(data)
# update existing records
for data in to_update:
data['record'].write(data['values'])
# determine existing parents for new records
for parent_model, parent_field in self._inherits.items():
suffix = '_' + parent_model.replace('.', '_')
xml_ids_vals = {
(data['xml_id'] + suffix): data['values']
for data in to_create
if data.get('xml_id')
}
for row in imd._lookup_xmlids(xml_ids_vals, self.env[parent_model]):
d_id, d_module, d_name, d_model, d_res_id, d_noupdate, r_id = row
if r_id:
xml_id = '%s.%s' % (d_module, d_name)
xml_ids_vals[xml_id][parent_field] = r_id
else:
imd.browse(d_id).unlink()
# create records
records = self.create([data['values'] for data in to_create])
for data, record in pycompat.izip(to_create, records):
data['record'] = record
# create or update XMLIDs
if to_create or to_update:
imd_data_list = [data for data in data_list if data.get('xml_id')]
imd._update_xmlids(imd_data_list, update)
return original_self.concat(*(data['record'] for data in data_list))
# TODO: ameliorer avec NULL
@api.model
def _where_calc(self, domain, active_test=True):
+27 -25
View File
@@ -241,6 +241,11 @@ class xml_import(object):
return self.id_get(node_uid)
return self.uid
def make_xml_id(self, xml_id):
if not xml_id or '.' in xml_id:
return xml_id
return "%s.%s" % (self.module, xml_id)
def _test_xml_id(self, xml_id):
id = xml_id
if '.' in xml_id:
@@ -316,17 +321,17 @@ form: module.record_id""" % (xml_id,)
pf_id = self.id_get(pf_name)
res['paperformat_id'] = pf_id
id = self.env['ir.model.data']._update("ir.actions.report", self.module, res, xml_id, noupdate=self.isnoupdate(data_node), mode=self.mode)
self.idref[xml_id] = int(id)
xid = self.make_xml_id(xml_id)
data = dict(xml_id=xid, values=res, noupdate=self.isnoupdate(data_node))
report = self.env['ir.actions.report']._load_records([data], self.mode == 'update')
self.idref[xml_id] = report.id
if not rec.get('menu') or safe_eval(rec.get('menu','False')):
report = self.env['ir.actions.report'].browse(id)
report.create_action()
elif self.mode=='update' and safe_eval(rec.get('menu','False'))==False:
# Special check for report having attribute menu=False on update
report = self.env['ir.actions.report'].browse(id)
report.unlink_action()
return id
return report.id
def _tag_function(self, rec, data_node=None, mode=None):
if self.isnoupdate(data_node) and self.mode != 'init':
@@ -434,8 +439,10 @@ form: module.record_id""" % (xml_id,)
if rec.get('key2') in (None, 'client_action_relate'):
if not res.get('multi'):
res['binding_type'] = 'action_form_only'
id = self.env['ir.model.data']._update('ir.actions.act_window', self.module, res, xml_id, noupdate=self.isnoupdate(data_node), mode=self.mode)
self.idref[xml_id] = int(id)
xid = self.make_xml_id(xml_id)
data = dict(xml_id=xid, values=res, noupdate=self.isnoupdate(data_node))
self.env['ir.actions.act_window']._load_records([data], self.mode == 'update')
def _tag_menuitem(self, rec, data_node=None, mode=None):
rec_id = rec.get("id")
@@ -497,12 +504,9 @@ form: module.record_id""" % (xml_id,)
if rec.get('web_icon'):
values['web_icon'] = rec.get('web_icon')
pid = self.env['ir.model.data']._update('ir.ui.menu', self.module, values, rec_id, noupdate=self.isnoupdate(data_node), mode=self.mode, res_id=res and res[0] or False)
if rec_id and pid:
self.idref[rec_id] = int(pid)
return 'ir.ui.menu', pid
xid = self.make_xml_id(rec_id)
data = dict(xml_id=xid, values=values, noupdate=self.isnoupdate(data_node))
self.env['ir.ui.menu']._load_records([data], self.mode == 'update')
def _assert_equals(self, f1, f2, prec=4):
return not round(f1 - f2, prec)
@@ -579,24 +583,21 @@ form: module.record_id""" % (xml_id,)
rec_context['install_filename'] = self.xml_filename
self._test_xml_id(rec_id)
xid = self.make_xml_id(rec_id)
# in update mode, the record won't be updated if the data node explicitly
# opt-out using @noupdate="1". A second check will be performed in
# ir.model.data#_update() using the record's ir.model.data `noupdate` field.
# model._load_records() using the record's ir.model.data `noupdate` field.
if self.isnoupdate(data_node) and self.mode != 'init':
# check if the xml record has no id, skip
if not rec_id:
return None
if '.' in rec_id:
module,rec_id2 = rec_id.split('.')
else:
module = self.module
rec_id2 = rec_id
id = self.env['ir.model.data']._update_dummy(rec_model, module, rec_id2)
if id:
record = self.env['ir.model.data']._load_xmlid(xid)
if record:
# if the resource already exists, don't update it but store
# its database id (can be useful)
self.idref[rec_id] = int(id)
self.idref[rec_id] = record.id
return None
elif not self.nodeattr2bool(rec, 'forcecreate', True):
# if it doesn't exist and we shouldn't create it, skip it
@@ -650,12 +651,13 @@ form: module.record_id""" % (xml_id,)
f_val = str2bool(f_val)
res[f_name] = f_val
id = self.env(context=rec_context)['ir.model.data']._update(rec_model, self.module, res, rec_id or False, not self.isnoupdate(data_node), noupdate=self.isnoupdate(data_node), mode=self.mode)
data = dict(xml_id=xid, values=res, noupdate=self.isnoupdate(data_node))
record = model.with_context(rec_context)._load_records([data], self.mode == 'update')
if rec_id:
self.idref[rec_id] = int(id)
self.idref[rec_id] = record.id
if config.get('import_partial'):
self.cr.commit()
return rec_model, id
return rec_model, record.id
def _tag_template(self, el, data_node=None, mode=None):
# This helper transforms a <template> element into a <record> and forwards it