summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRaphael Collet <rco@openerp.com>2016-02-23 14:54:34 +0100
committerRaphael Collet <rco@openerp.com>2016-03-21 13:46:44 +0100
commit4ddc32313996c8cf6e8e6dffd2253aad8a144ccf (patch)
tree32e6eaf4c7317b944c18d7758cb61f0636949a40
parenteb04aa1e395d347c474181e90f8c8eded1d17ca3 (diff)
[FIX] base: fix and complete migration of base/ir
- add or fix decorators on methods - fix most compute methods (wrong dependencies) - revert changes in `eval_context` of `ir.actions` - various code simplifications and improvements - migrate methods that were not - ir_qweb: modify `QWebContext` to take an environment instead of `cr`, `uid`, `context` - ir_qweb: convert `AssetsBundle` to use new API `env` - ir_ui_view: use decorator `multi` on method `read_combined` - ir_ui_view: ensure that method `render` is never called with an xml_id - ir_ui_view: factor out part of the `ormcache` key of method `_read_template`
-rw-r--r--addons/auth_oauth/ir_configparameter.py16
-rw-r--r--addons/mail/models/ir_autovacuum.py2
-rw-r--r--addons/mass_mailing/controllers/web_editor.py2
-rw-r--r--addons/payment/models/payment_acquirer.py5
-rw-r--r--addons/report/models/report.py4
-rw-r--r--addons/report_webkit/ir_report.py8
-rw-r--r--addons/web_editor/controllers/main.py2
-rw-r--r--addons/web_editor/models/ir_qweb.py6
-rw-r--r--addons/web_editor/models/ir_ui_view.py2
-rw-r--r--addons/web_editor/tests/test_converter.py62
-rw-r--r--addons/website/controllers/main.py11
-rw-r--r--addons/website/models/ir_actions.py23
-rw-r--r--addons/website/models/website.py2
-rw-r--r--addons/website_mail_channel/controllers/main.py2
-rw-r--r--addons/website_theme_install/views/views.xml2
-rw-r--r--openerp/addons/base/ir/ir_actions.py650
-rw-r--r--openerp/addons/base/ir/ir_attachment.py425
-rw-r--r--openerp/addons/base/ir/ir_autovacuum.py12
-rw-r--r--openerp/addons/base/ir/ir_config_parameter.py43
-rw-r--r--openerp/addons/base/ir/ir_config_parameter_view.xml4
-rw-r--r--openerp/addons/base/ir/ir_cron.py84
-rw-r--r--openerp/addons/base/ir/ir_exports.py4
-rw-r--r--openerp/addons/base/ir/ir_fields.py18
-rw-r--r--openerp/addons/base/ir/ir_filters.py69
-rw-r--r--openerp/addons/base/ir/ir_http.py44
-rw-r--r--openerp/addons/base/ir/ir_logging.py6
-rw-r--r--openerp/addons/base/ir/ir_mail_server.py90
-rw-r--r--openerp/addons/base/ir/ir_mail_server_view.xml2
-rw-r--r--openerp/addons/base/ir/ir_model.py817
-rw-r--r--openerp/addons/base/ir/ir_needaction.py1
-rw-r--r--openerp/addons/base/ir/ir_qweb.py392
-rw-r--r--openerp/addons/base/ir/ir_rule.py134
-rw-r--r--openerp/addons/base/ir/ir_sequence.py132
-rw-r--r--openerp/addons/base/ir/ir_translation.py389
-rw-r--r--openerp/addons/base/ir/ir_ui_menu.py225
-rw-r--r--openerp/addons/base/ir/ir_ui_view.py591
-rw-r--r--openerp/addons/base/ir/ir_values.py124
-rw-r--r--openerp/addons/base/module/wizard/base_module_immediate_install.xml2
-rw-r--r--openerp/addons/base/res/res_currency.py1
-rw-r--r--openerp/addons/base/tests/test_api.py18
-rw-r--r--openerp/addons/base/tests/test_ormcache.py2
-rw-r--r--openerp/addons/base/tests/test_qweb.py15
-rw-r--r--openerp/addons/test_assetsbundle/controllers/main.py2
-rw-r--r--openerp/addons/test_assetsbundle/tests/test_assetsbundle.py77
-rw-r--r--openerp/addons/test_converter/tests/test_html.py4
-rw-r--r--openerp/http.py5
-rw-r--r--openerp/models.py11
47 files changed, 2158 insertions, 2384 deletions
diff --git a/addons/auth_oauth/ir_configparameter.py b/addons/auth_oauth/ir_configparameter.py
index e421bcb4b39..b0fa17ffa5b 100644
--- a/addons/auth_oauth/ir_configparameter.py
+++ b/addons/auth_oauth/ir_configparameter.py
@@ -1,12 +1,14 @@
# -*- coding: utf-8 -*-
-from odoo import models
+from openerp import SUPERUSER_ID
+from openerp.osv import osv
-
-class IrConfigParameter(models.Model):
+class ir_configparameter(osv.Model):
_inherit = 'ir.config_parameter'
- def init(self, force=False):
- super(IrConfigParameter, self).init(force=force)
+ def init(self, cr, force=False):
+ super(ir_configparameter, self).init(cr, force=force)
if force:
- oauth_oe = self.env.ref('auth_oauth.provider_openerp')
- oauth_oe.write({'client_id': self.sudo().get_param('database.uuid')})
+ IMD = self.pool['ir.model.data']
+ oauth_oe = IMD.xmlid_to_object(cr, SUPERUSER_ID, 'auth_oauth.provider_openerp')
+ dbuuid = self.get_param(cr, SUPERUSER_ID, 'database.uuid')
+ oauth_oe.write({'client_id': dbuuid})
diff --git a/addons/mail/models/ir_autovacuum.py b/addons/mail/models/ir_autovacuum.py
index 37ea8d98fb1..428256fc061 100644
--- a/addons/mail/models/ir_autovacuum.py
+++ b/addons/mail/models/ir_autovacuum.py
@@ -3,7 +3,7 @@
from openerp import models
-class AutoVacuum(models.TransientModel):
+class AutoVacuum(models.AbstractModel):
_inherit = 'ir.autovacuum'
def power_on(self, cr, uid, *args, **kwargs):
diff --git a/addons/mass_mailing/controllers/web_editor.py b/addons/mass_mailing/controllers/web_editor.py
index a9cff910c69..ac2a7ba3b4e 100644
--- a/addons/mass_mailing/controllers/web_editor.py
+++ b/addons/mass_mailing/controllers/web_editor.py
@@ -22,4 +22,4 @@ class Web_Editor(Web_Editor):
@http.route(['/mass_mailing/snippets'], type='json', auth="user", website=True)
def mass_mailing_snippets(self):
values = {'company_id': request.env['res.users'].browse(request.uid).company_id}
- return request.registry["ir.ui.view"].render(request.cr, request.uid, 'mass_mailing.email_designer_snippets', values, context=request.context)
+ return request.env.ref('mass_mailing.email_designer_snippets').render(values)
diff --git a/addons/payment/models/payment_acquirer.py b/addons/payment/models/payment_acquirer.py
index fc9f501270e..01418103cfb 100644
--- a/addons/payment/models/payment_acquirer.py
+++ b/addons/payment/models/payment_acquirer.py
@@ -279,8 +279,7 @@ class PaymentAcquirer(osv.Model):
})
values.setdefault('return_url', False)
- # because render accepts view ids but not qweb -> need to use the xml_id
- return self.pool['ir.ui.view'].render(cr, uid, acquirer.view_template_id.xml_id, values, engine='ir.qweb', context=context)
+ return acquirer.view_template_id.render(values, engine='ir.qweb')
def _registration_render(self, cr, uid, id, partner_id, qweb_context=None, context=None):
acquirer = self.browse(cr, uid, id, context=context)
@@ -291,7 +290,7 @@ class PaymentAcquirer(osv.Model):
if hasattr(self, method_name):
method = getattr(self, method_name)
qweb_context.update(method(cr, uid, id, qweb_context, context=context))
- return self.pool['ir.ui.view'].render(cr, uid, acquirer.registration_view_template_id.xml_id, qweb_context, engine='ir.qweb', context=context)
+ return acquirer.registration_view_template_id.render(qweb_context, engine='ir.qweb')
def s2s_process(self, cr, uid, id, data, context=None):
acquirer = self.browse(cr, uid, id, context=context)
diff --git a/addons/report/models/report.py b/addons/report/models/report.py
index 71655932603..aacf988b90f 100644
--- a/addons/report/models/report.py
+++ b/addons/report/models/report.py
@@ -103,7 +103,7 @@ class Report(osv.Model):
res_company=user.company_id,
website=website,
)
- return view_obj.render(cr, uid, template, values, context=context)
+ return view_obj.render_template(cr, uid, template, values, context=context)
#--------------------------------------------------------------------------
# Main report methods
@@ -184,7 +184,7 @@ class Report(osv.Model):
# Minimal page renderer
view_obj = self.pool['ir.ui.view']
- render_minimal = partial(view_obj.render, cr, uid, 'report.minimal_layout', context=context)
+ render_minimal = partial(view_obj.render_template, cr, uid, 'report.minimal_layout', context=context)
# The received html report must be simplified. We convert it in a xml tree
# in order to extract headers, bodies and footers.
diff --git a/addons/report_webkit/ir_report.py b/addons/report_webkit/ir_report.py
index 347d59352e8..4f5be2949b0 100644
--- a/addons/report_webkit/ir_report.py
+++ b/addons/report_webkit/ir_report.py
@@ -25,7 +25,7 @@ class ir_actions_report_xml(orm.Model):
" is printed on a separate HTML but memory and disk usage are wider.")
}
- def _lookup_report(self, name):
+ def _lookup_report(self, cr, name):
"""
Look up a report definition.
"""
@@ -41,8 +41,8 @@ class ir_actions_report_xml(orm.Model):
if not isinstance(new_report, WebKitParser):
new_report = None
else:
- self.env.cr.execute("SELECT * FROM ir_act_report_xml WHERE report_name=%s and report_type=%s", (name, 'webkit'))
- r = self.env.cr.dictfetchone()
+ cr.execute("SELECT * FROM ir_act_report_xml WHERE report_name=%s and report_type=%s", (name, 'webkit'))
+ r = cr.dictfetchone()
if r:
if r['parser']:
parser = operator.attrgetter(r['parser'])(openerp.addons)
@@ -59,4 +59,4 @@ class ir_actions_report_xml(orm.Model):
if new_report:
return new_report
else:
- return super(ir_actions_report_xml, self)._lookup_report(name)
+ return super(ir_actions_report_xml, self)._lookup_report(cr, name)
diff --git a/addons/web_editor/controllers/main.py b/addons/web_editor/controllers/main.py
index bd4fd66bcb8..ca418645471 100644
--- a/addons/web_editor/controllers/main.py
+++ b/addons/web_editor/controllers/main.py
@@ -18,7 +18,7 @@ class Web_Editor(http.Controller):
#------------------------------------------------------
@http.route('/web_editor/snippets', type='json', auth="user")
def snippets(self, **kwargs):
- return request.registry["ir.ui.view"].render(request.cr, request.uid, 'web_editor.snippets', None, context=request.context)
+ return request.env.ref('web_editor.snippets').render(None)
#------------------------------------------------------
# Backend html field
diff --git a/addons/web_editor/models/ir_qweb.py b/addons/web_editor/models/ir_qweb.py
index 149fcc5cc74..a0fe989391c 100644
--- a/addons/web_editor/models/ir_qweb.py
+++ b/addons/web_editor/models/ir_qweb.py
@@ -57,9 +57,6 @@ class QWeb(orm.AbstractModel):
template_attributes['call'] = template_attributes['snippet']
return self.render_tag_call(element, template_attributes, generated_attributes, qwebcontext)
- def get_converter_for(self, field_type):
- return self.pool.get('ir.qweb.field.' + field_type, self.pool['ir.qweb.field'])
-
#------------------------------------------------------
# QWeb fields
@@ -102,9 +99,6 @@ class Field(orm.AbstractModel):
def from_html(self, cr, uid, model, field, element, context=None):
return self.value_from_string(element.text_content().strip())
- def qweb_object(self):
- return self.pool['ir.qweb']
-
class Integer(orm.AbstractModel):
_name = 'ir.qweb.field.integer'
diff --git a/addons/web_editor/models/ir_ui_view.py b/addons/web_editor/models/ir_ui_view.py
index dd7501c44f9..01082efb37b 100644
--- a/addons/web_editor/models/ir_ui_view.py
+++ b/addons/web_editor/models/ir_ui_view.py
@@ -12,6 +12,8 @@ class view(osv.osv):
@api.cr_uid_ids_context
def render(self, cr, uid, id_or_xml_id, values=None, engine='ir.qweb', context=None):
+ if isinstance(id_or_xml_id, list):
+ id_or_xml_id = id_or_xml_id[0]
if not values:
values = {}
if values.get('editable'):
diff --git a/addons/web_editor/tests/test_converter.py b/addons/web_editor/tests/test_converter.py
index 48a157c5d17..6772a15019a 100644
--- a/addons/web_editor/tests/test_converter.py
+++ b/addons/web_editor/tests/test_converter.py
@@ -6,7 +6,7 @@ from lxml import etree, html
from lxml.builder import E
from openerp.tests import common
-from openerp.addons.base.ir import ir_qweb
+from openerp.addons.base.ir.ir_qweb import QWebContext
from openerp.addons.web_editor.models.ir_qweb import html_to_text
@@ -116,32 +116,25 @@ class TestHTMLToText(unittest.TestCase):
class TestConvertBack(common.TransactionCase):
def setUp(self):
super(TestConvertBack, self).setUp()
+ self.env = self.env(context={'inherit_branding': True})
+
+ def qwebcontext(self, data):
+ return QWebContext(self.env, data)
def field_rountrip_result(self, field, value, expected):
model = 'web_editor.converter.test'
- Model = self.registry(model)
- id = Model.create(
- self.cr, self.uid, {
- field: value
- })
- [record] = Model.browse(self.cr, self.uid, [id])
+ record = self.env[model].create({field: value})
e = etree.Element('span')
field_value = 'record.%s' % field
e.set('t-field', field_value)
- rendered = self.registry('ir.qweb').render_tag_field(
- e, {'field': field_value}, '', ir_qweb.QWebContext(self.cr, self.uid, {
- 'record': record,
- }, context={'inherit_branding': True}))
- element = html.fromstring(
- rendered, parser=html.HTMLParser(encoding='utf-8'))
-
- converter = self.registry('ir.qweb').get_converter_for(
- element.get('data-oe-type'))
+ qwebcontext = self.qwebcontext({'record': record})
+ rendered = self.env['ir.qweb'].render_tag_field(e, {'field': field_value}, '', qwebcontext)
+ element = html.fromstring(rendered, parser=html.HTMLParser(encoding='utf-8'))
- value_back = converter.from_html(
- self.cr, self.uid, model, Model._fields[field], element)
+ converter = self.env['ir.qweb'].get_converter_for(element.get('data-oe-type'))
+ value_back = converter.from_html(model, record._fields[field], element)
if isinstance(expected, str):
expected = expected.decode('utf-8')
@@ -162,7 +155,6 @@ class TestConvertBack(common.TransactionCase):
def test_char(self):
self.field_roundtrip('char', "foo bar")
-
self.field_roundtrip('char', "ⒸⓄⓇⒼⒺ")
def test_selection(self):
@@ -199,45 +191,35 @@ class TestConvertBack(common.TransactionCase):
model = 'web_editor.converter.test'
field = 'many2one'
- Sub = self.registry('web_editor.converter.test.sub')
- sub_id = Sub.create(self.cr, self.uid, {'name': "Foo"})
- sub2_id = Sub.create(self.cr, self.uid, {'name': "Bar"})
-
- Model = self.registry(model)
- id = Model.create(self.cr, self.uid, {field: sub_id})
- [record] = Model.browse(self.cr, self.uid, [id])
+ subrec1 = self.env['web_editor.converter.test.sub'].create({'name': "Foo"})
+ subrec2 = self.env['web_editor.converter.test.sub'].create({'name': "Bar"})
+ record = self.env[model].create({field: subrec1.id})
e = etree.Element('span')
field_value = 'record.%s' % field
e.set('t-field', field_value)
- rendered = self.registry('ir.qweb').render_tag_field(
- e, {'field': field_value}, '', ir_qweb.QWebContext(self.cr, self.uid, {
- 'record': record,
- }, context={'inherit_branding': True}))
-
+ qwebcontext = self.qwebcontext({'record': record})
+ rendered = self.env['ir.qweb'].render_tag_field(e, {'field': field_value}, '', qwebcontext)
element = html.fromstring(rendered, parser=html.HTMLParser(encoding='utf-8'))
+
# emulate edition
- element.set('data-oe-many2one-id', str(sub2_id))
+ element.set('data-oe-many2one-id', str(subrec2.id))
element.text = "New content"
- converter = self.registry('ir.qweb').get_converter_for(
- element.get('data-oe-type'))
-
- value_back = converter.from_html(
- self.cr, self.uid, model, Model._fields[field], element)
+ converter = self.env['ir.qweb'].get_converter_for(element.get('data-oe-type'))
+ value_back = converter.from_html(model, record._fields[field], element)
self.assertIsNone(
value_back, "the m2o converter should return None to avoid spurious"
" or useless writes on the parent record")
-
self.assertEqual(
- Sub.browse(self.cr, self.uid, sub_id).name,
+ subrec1.name,
"Foo",
"element edition can't change directly the m2o record"
)
self.assertEqual(
- Model.browse(self.cr, self.uid, id).many2one.name,
+ record.many2one.name,
"Bar",
"element edition should have been change the m2o id"
)
diff --git a/addons/website/controllers/main.py b/addons/website/controllers/main.py
index e73cc55bc9f..b161b5411fb 100644
--- a/addons/website/controllers/main.py
+++ b/addons/website/controllers/main.py
@@ -135,9 +135,9 @@ class Website(openerp.addons.web.controllers.main.Home):
'locs': islice(locs, 0, LOC_PER_SITEMAP),
'url_root': request.httprequest.url_root[:-1],
}
- urls = iuv.render(cr, uid, 'website.sitemap_locs', values, context=context)
+ urls = iuv.render_template(cr, uid, 'website.sitemap_locs', values, context=context)
if urls.strip():
- content = iuv.render(cr, uid, 'website.sitemap_xml', dict(content=urls), context=context)
+ content = iuv.render_template(cr, uid, 'website.sitemap_xml', dict(content=urls), context=context)
pages += 1
last = create_sitemap('/sitemap-%d.xml' % pages, content)
else:
@@ -148,7 +148,7 @@ class Website(openerp.addons.web.controllers.main.Home):
ira.write(cr, uid, last, dict(url="/sitemap.xml", name="/sitemap.xml"), context=context)
else:
# Sitemaps must be split in several smaller files with a sitemap index
- content = iuv.render(cr, uid, 'website.sitemap_index_xml', dict(
+ content = iuv.render_template(cr, uid, 'website.sitemap_index_xml', dict(
pages=range(1, pages + 1),
url_root=request.httprequest.url_root,
), context=context)
@@ -317,7 +317,7 @@ class Website(openerp.addons.web.controllers.main.Home):
set_active(enable, True)
if get_bundle:
- bundle = AssetsBundle('web.assets_frontend', cr=http.request.cr, uid=http.request.uid, context={}, registry=http.request.registry)
+ bundle = AssetsBundle('web.assets_frontend', env=request.env(context={}))
return bundle.to_html()
return True
@@ -329,9 +329,10 @@ class Website(openerp.addons.web.controllers.main.Home):
@http.route(['/website/multi_render'], type='json', auth="public", website=True)
def multi_render(self, ids_or_xml_ids, values=None):
+ View = request.env['ir.ui.view']
res = {}
for id_or_xml_id in ids_or_xml_ids:
- res[id_or_xml_id] = request.registry["ir.ui.view"].render(request.cr, request.uid, id_or_xml_id, values=values, engine='ir.qweb', context=request.context)
+ res[id_or_xml_id] = View.render_template(id_or_xml_id, values)
return res
#------------------------------------------------------
diff --git a/addons/website/models/ir_actions.py b/addons/website/models/ir_actions.py
index dff702ba867..21c89c955bf 100644
--- a/addons/website/models/ir_actions.py
+++ b/addons/website/models/ir_actions.py
@@ -2,8 +2,8 @@
import urlparse
-from odoo.http import request
from odoo import api, fields, models
+from odoo.http import request
class actions_server(models.Model):
@@ -11,7 +11,7 @@ class actions_server(models.Model):
_name = 'ir.actions.server'
_inherit = 'ir.actions.server'
- xml_id = fields.Char(compute=models.Model.get_xml_id, string="External ID",
+ xml_id = fields.Char(compute='_compute_xml_id', string="External ID",
help="ID of the action if defined in a XML file")
website_path = fields.Char()
website_url = fields.Char(compute='_get_website_url', help='The full URL to access the server action through the website.')
@@ -21,6 +21,11 @@ class actions_server(models.Model):
'Set this field as True to allow users to run this action. If it '
'is set to False the action cannot be run through the website.')
+ def _compute_xml_id(self):
+ res = self.get_external_id()
+ for action in self:
+ action.xml_id = res.get(action.id)
+
def _compute_website_url(self, website_path, xml_id):
base_url = self.env['ir.config_parameter'].get_param('web.base.url')
link = website_path or xml_id or (self.id and '%d' % self.id) or ''
@@ -29,19 +34,12 @@ class actions_server(models.Model):
return '%s' % urlparse.urljoin(base_url, path)
return ''
- @api.depends('website_url')
+ @api.depends('state', 'website_published', 'website_path', 'xml_id')
def _get_website_url(self):
for action in self:
if action.state == 'code' and action.website_published:
action.website_url = self._compute_website_url(action.website_path, action.xml_id)
- @api.onchange('website_path', 'xml_id')
- def on_change_website_path(self):
- values = {
- 'website_url': self._compute_website_url(self.website_path, self.xml_id)
- }
- return {'value': values}
-
@api.model
def _get_eval_context(self, action):
""" Override to add the request object in eval_context. """
@@ -50,11 +48,10 @@ class actions_server(models.Model):
eval_context['request'] = request
return eval_context
+ @api.model
def run_action_code_multi(self, action, eval_context=None):
""" Override to allow returning response the same way action is already
returned by the basic server action behavior. Note that response has
priority over action, avoid using both. """
res = super(actions_server, self).run_action_code_multi(action, eval_context)
- if 'response' in eval_context:
- return eval_context['response']
- return res
+ return eval_context.get('response', res)
diff --git a/addons/website/models/website.py b/addons/website/models/website.py
index 4d54f7bacd5..64ece3eb3b0 100644
--- a/addons/website/models/website.py
+++ b/addons/website/models/website.py
@@ -417,7 +417,7 @@ class website(osv.osv):
def _render(self, cr, uid, ids, template, values=None, context=None):
# TODO: remove this. (just kept for backward api compatibility for saas-3)
- return self.pool['ir.ui.view'].render(cr, uid, template, values=values, context=context)
+ return self.pool['ir.ui.view'].render_template(cr, uid, template, values=values, context=context)
def render(self, cr, uid, ids, template, values=None, status_code=None, context=None):
# TODO: remove this. (just kept for backward api compatibility for saas-3)
diff --git a/addons/website_mail_channel/controllers/main.py b/addons/website_mail_channel/controllers/main.py
index 01d55c7b75a..f97f52289e5 100644
--- a/addons/website_mail_channel/controllers/main.py
+++ b/addons/website_mail_channel/controllers/main.py
@@ -195,7 +195,7 @@ class MailGroup(http.Controller):
'msg_more_count': msg_count - self._replies_per_page,
'replies_per_page': self._replies_per_page,
}
- return request.registry['ir.ui.view'].render(request.cr, request.uid, 'website_mail_channel.messages_short', values, engine='ir.qweb', context=request.context)
+ return request.env.ref('website_mail_channel.messages_short').render(values, engine='ir.qweb')
@http.route("/groups/<model('mail.channel'):group>/get_alias_info", type='json', auth='public', website=True)
def get_alias_info(self, group, **post):
diff --git a/addons/website_theme_install/views/views.xml b/addons/website_theme_install/views/views.xml
index 249b926baf9..1b5ec2e3912 100644
--- a/addons/website_theme_install/views/views.xml
+++ b/addons/website_theme_install/views/views.xml
@@ -62,7 +62,7 @@
<field name="name">Choose Your Theme Todo Action</field>
<field name="model_id" ref="model_ir_module_module"/>
<field name="code">
-self.update_list()
+self.update_list(cr, uid, context=context)
action = {
'type': 'ir.actions.act_url',
'url': '/web?reload#action=website_theme_install.theme_install_kanban_action', # the ?reload option is there to fool the webclient into thinking it is a different location and so to force a reload
diff --git a/openerp/addons/base/ir/ir_actions.py b/openerp/addons/base/ir/ir_actions.py
index 45af48f64e6..aab685ef7c8 100644
--- a/openerp/addons/base/ir/ir_actions.py
+++ b/openerp/addons/base/ir/ir_actions.py
@@ -4,22 +4,20 @@
import datetime
import dateutil
import logging
-import operator
import os
import time
-from functools import partial
from pytz import timezone
import odoo
from odoo import api, fields, models, tools, workflow, _
-from odoo.exceptions import MissingError, UserError
+from odoo.exceptions import MissingError, UserError, ValidationError
from odoo.report.report_sxw import report_sxw, report_rml
from odoo.tools.safe_eval import safe_eval as eval
_logger = logging.getLogger(__name__)
-class Actions(models.Model):
+class IrActions(models.Model):
_name = 'ir.actions.actions'
_table = 'ir_actions'
_order = 'name'
@@ -27,21 +25,26 @@ class Actions(models.Model):
name = fields.Char(required=True)
type = fields.Char(string='Action Type', required=True)
usage = fields.Char(string='Action Usage')
- xml_id = fields.Char(compute=models.Model.get_external_id, string="External ID")
+ xml_id = fields.Char(compute='_compute_xml_id', string="External ID")
help = fields.Html(string='Action Description',
- help='Optional help text for the users with a description of the target view, such as its usage and purpose.',
- translate=True)
+ help='Optional help text for the users with a description of the target view, such as its usage and purpose.',
+ translate=True)
+
+ def _compute_xml_id(self):
+ res = self.get_external_id()
+ for record in self:
+ record.xml_id = res.get(record.id)
@api.model
def create(self, vals):
- res = super(Actions, self).create(vals)
+ res = super(IrActions, self).create(vals)
# ir_values.get_actions() depends on action records
self.env['ir.values'].clear_caches()
return res
@api.multi
def write(self, vals):
- res = super(Actions, self).write(vals)
+ res = super(IrActions, self).write(vals)
# ir_values.get_actions() depends on action records
self.env['ir.values'].clear_caches()
return res
@@ -50,9 +53,9 @@ class Actions(models.Model):
def unlink(self):
"""unlink ir.action.todo which are related to actions which will be deleted.
NOTE: ondelete cascade will not work on ir.actions.actions so we will need to do it manually."""
- todo_ids = self.env['ir.actions.todo'].search([('action_id', 'in', self.ids)])
- todo_ids.unlink()
- res = super(Actions, self).unlink()
+ todos = self.env['ir.actions.todo'].search([('action_id', 'in', self.ids)])
+ todos.unlink()
+ res = super(IrActions, self).unlink()
# ir_values.get_actions() depends on action records
self.env['ir.values'].clear_caches()
return res
@@ -61,7 +64,7 @@ class Actions(models.Model):
def _get_eval_context(self, action=None):
""" evaluation context to pass to safe_eval """
return {
- 'uid': self.env.uid,
+ 'uid': self._uid,
'user': self.env.user,
'time': time,
'datetime': datetime,
@@ -77,8 +80,8 @@ class IrActionsReportXml(models.Model):
_sequence = 'ir_actions_id_seq'
_order = 'name'
- type = fields.Char(string='Action Type', default='ir.actions.report.xml', required=True)
- name = fields.Char(required=True, translate=True)
+ name = fields.Char(translate=True)
+ type = fields.Char(default='ir.actions.report.xml')
model = fields.Char(required=True)
report_type = fields.Selection([('qweb-pdf', 'PDF'),
@@ -86,7 +89,8 @@ class IrActionsReportXml(models.Model):
('controller', 'Controller'),
('pdf', 'RML pdf (deprecated)'),
('sxw', 'RML sxw (deprecated)'),
- ('webkit', 'Webkit (deprecated)')], default="pdf", required=True,
+ ('webkit', 'Webkit (deprecated)')],
+ required=True, default="pdf",
help="HTML will open the report directly in your browser, PDF will use wkhtmltopdf to render the HTML into a PDF file and let you download it, Controller allows you to define the url of a custom controller outputting any kind of report.")
report_name = fields.Char(string='Template Name', required=True,
help="For QWeb reports, name of the template used in the rendering. The method 'render_html' of the model 'report.template_name' will be called (if any) to give the html. For RML reports, this is the LocalService name.")
@@ -101,7 +105,6 @@ class IrActionsReportXml(models.Model):
help='This is the filename of the attachment used to store the printing result. Keep empty to not save the printed reports. You can use a python expression with the object and time variables.')
# Deprecated rml stuff
- usage = fields.Char(string='Action Usage')
header = fields.Boolean(string='Add RML Header', default=True, help="Add or not the corporate RML header")
parser = fields.Char(string='Parser Class')
auto = fields.Boolean(string='Custom Python Parser', default=True)
@@ -116,123 +119,119 @@ class IrActionsReportXml(models.Model):
report_sxw = fields.Char(compute='_compute_report_sxw', string='SXW Path')
report_sxw_content_data = fields.Binary(string='SXW Content')
report_rml_content_data = fields.Binary(string='RML Content')
- report_sxw_content = fields.Binary(compute='_compute_report_sxw_content', inverse='_inverse_report_content', string='SXW Content')
- report_rml_content = fields.Binary(compute='_compute_report_rml_content', inverse='_inverse_report_content', string='RML Content')
+ report_sxw_content = fields.Binary(compute='_compute_report_sxw_content', inverse='_inverse_report_sxw_content', string='SXW Content')
+ report_rml_content = fields.Binary(compute='_compute_report_rml_content', inverse='_inverse_report_rml_content', string='RML Content')
- def _report_content(self, name):
- res = {}
+ @api.depends('report_rml')
+ def _compute_report_sxw(self):
for report in self:
- data = report[name + '_data']
- if not data and report[name[:-8]]:
- fp = None
- try:
- fp = tools.file_open(report[name[:-8]], mode='rb')
+ if report.report_rml:
+ self.report_sxw = report.report_rml.replace('.rml', '.sxw')
+
+ def _report_content(self, name):
+ data = self[name + '_content_data']
+ if not data and self[name]:
+ try:
+ with tools.file_open(self[name], mode='rb') as fp:
data = fp.read()
- except:
- data = False
- finally:
- if fp:
- fp.close()
- res[report.id] = data
- return res
+ except Exception:
+ data = False
+ return data
- @api.depends('report_sxw_content')
+ @api.depends('report_sxw', 'report_sxw_content_data')
def _compute_report_sxw_content(self):
- self.report_sxw_content = self._report_content(self.name)
+ for report in self:
+ report.report_sxw_content = report._report_content('report_sxw')
- @api.depends('report_rml_content')
+ @api.depends('report_rml', 'report_rml_content_data')
def _compute_report_rml_content(self):
- self.report_rml_content = self._report_content(self.name)
+ for report in self:
+ report.report_rml_content = report._report_content('report_rml')
- def _inverse_report_content(self, name, value):
- self.write({name+'_data': value})
+ def _inverse_report_sxw_content(self):
+ for report in self:
+ report.report_sxw_content_data = report.report_sxw_content
- @api.depends('report_sxw')
- def _compute_report_sxw(self):
+ def _inverse_report_rml_content(self):
for report in self:
- if report.report_rml:
- self.report_sxw = report.report_rml.replace('.rml', '.sxw')
+ report.report_rml_content_data = report.report_rml_content
- @api.multi
+ @api.model_cr
def _lookup_report(self, name):
"""
Look up a report definition.
"""
- opj = os.path.join
+ join = os.path.join
# First lookup in the deprecated place, because if the report definition
# has not been updated, it is more likely the correct definition is there.
# Only reports with custom parser sepcified in Python are still there.
if 'report.' + name in odoo.report.interface.report_int._reports:
- new_report = odoo.report.interface.report_int._reports['report.' + name]
+ return odoo.report.interface.report_int._reports['report.' + name]
+
+ self._cr.execute("SELECT * FROM ir_act_report_xml WHERE report_name=%s", (name,))
+ row = self._cr.dictfetchone()
+ if not row:
+ raise Exception("Required report does not exist: %s" % name)
+
+ if row['report_type'] in ('qweb-pdf', 'qweb-html'):
+ return row['report_name']
+ elif row['report_rml'] or row['report_rml_content_data']:
+ kwargs = {}
+ if row['parser']:
+ kwargs['parser'] = getattr(odoo.addons, row['parser'])
+ return report_sxw('report.'+row['report_name'], row['model'],
+ join('addons', row['report_rml'] or '/'),
+ header=row['header'], register=False, **kwargs)
+ elif row['report_xsl'] and row['report_xml']:
+ return report_rml('report.'+row['report_name'], row['model'],
+ join('addons', row['report_xml']),
+ row['report_xsl'] and join('addons', row['report_xsl']),
+ register=False)
else:
- self.env.cr.execute("SELECT * FROM ir_act_report_xml WHERE report_name=%s", (name,))
- report = self.env.cr.dictfetchone()
- if report:
- if report['report_type'] in ['qweb-pdf', 'qweb-html']:
- return report['report_name']
- elif report['report_rml'] or report['report_rml_content_data']:
- if report['parser']:
- kwargs = {'parser': operator.attrgetter(report['parser'])(odoo.addons)}
- else:
- kwargs = {}
- new_report = report_sxw('report.'+report['report_name'], report['model'],
- opj('addons', report['report_rml'] or '/'), header=report['header'], register=False, **kwargs)
- elif report['report_xsl'] and report['report_xml']:
- new_report = report_rml('report.'+report['report_name'], report['model'],
- opj('addons', report['report_xml']),
- report['report_xsl'] and opj('addons', report['report_xsl']), register=False)
- else:
- raise Exception, "Unhandled report type: %s" % report
- else:
- raise Exception, "Required report does not exist: %s" % name
-
- return new_report
+ raise Exception("Unhandled report type: %s" % row)
@api.multi
def create_action(self):
- """ Create a contextual action for each of the report."""
- for ir_actions_report_xml in self:
- ir_values_id = self.env['ir.values'].sudo().create({
- 'name': ir_actions_report_xml.name,
- 'model': ir_actions_report_xml.model,
+ """ Create a contextual action for each report. """
+ for report in self:
+ ir_values = self.env['ir.values'].sudo().create({
+ 'name': report.name,
+ 'model': report.model,
'key2': 'client_print_multi',
- 'value': "ir.actions.report.xml,%s" % ir_actions_report_xml.id,
- })
- ir_actions_report_xml.write({
- 'ir_values_id': ir_values_id.id,
+ 'value': "ir.actions.report.xml,%s" % report.id,
})
+ report.write({'ir_values_id': ir_values.id})
return True
@api.multi
def unlink_action(self):
- """ Remove the contextual actions created for the reports."""
+ """ Remove the contextual actions created for the reports. """
self.check_access_rights('write', raise_exception=True)
- for ir_actions_report_xml in self:
- if ir_actions_report_xml.ir_values_id:
+ for report in self:
+ if report.ir_values_id:
try:
- self.ir_actions_report_xml.ir_values_id.sudo().unlink()
+ self.report.ir_values_id.sudo().unlink()
except Exception:
raise UserError(_('Deletion of the action record failed.'))
return True
- @api.multi
- def render_report(self, name, data):
+ @api.model
+ def render_report(self, res_ids, name, data):
"""
Look up a report definition and render the report for the provided IDs.
"""
- new_report = self._lookup_report(name)
-
- if isinstance(new_report, (str, unicode)): # Qweb report
+ report = self._lookup_report(name)
+ if isinstance(report, basestring): # Qweb report
# The only case where a QWeb report is rendered with this method occurs when running
# yml tests originally written for RML reports.
if tools.config['test_enable'] and not tools.config['test_report_directory']:
# Only generate the pdf when a destination folder has been provided.
- return self.env['report']._model.get_html(self._cr, self._uid, self.ids, new_report, data=data), 'html'
+ return self.pool['report'].get_html(self._cr, self._uid, res_ids, report, data=data, context=self._context), 'html'
else:
- return self.env['report']._model.get_pdf(self._cr, self._uid, self.ids, new_report, data=data), 'pdf'
+ return self.pool['report'].get_pdf(self._cr, self._uid, res_ids, report, data=data, context=self._context), 'pdf'
else:
- return new_report.create(self._cr, self._uid, self.ids, data)
+ return report.create(self._cr, self._uid, res_ids, data, context=self._context)
class IrActionsActWindow(models.Model):
@@ -246,46 +245,40 @@ class IrActionsActWindow(models.Model):
def _check_model(self):
for action in self:
if action.res_model not in self.env:
- raise UserError(_('Invalid model name in the action definition.'))
+ raise ValidationError(_('Invalid model name %r in action definition.') % action.res_model)
if action.src_model and action.src_model not in self.env:
- raise UserError(_('Invalid model name in the action definition.'))
- return True
+ raise ValidationError(_('Invalid model name %r in action definition.') % action.src_model)
- @api.depends('views')
- def _views_get_fnc(self):
- """Returns an ordered list of the specific view modes that should be
- enabled when displaying the result of this action, along with the
- ID of the specific view to use for each mode, if any were required.
-
- This function hides the logic of determining the precedence between
- the view_modes string, the view_ids o2m, and the view_id m2o that can
- be set on the action.
-
- :rtype: dict in the form { action_id: list of pairs (tuples) }
- :return: { action_id: [(view_id, view_mode), ...], ... }, where view_mode
- is one of the possible values for ir.ui.view.type and view_id
- is the ID of a specific view to use for this mode, or False for
- the default one.
+ @api.depends('view_ids.view_mode', 'view_mode', 'view_id.type')
+ def _compute_views(self):
+ """ Compute an ordered list of the specific view modes that should be
+ enabled when displaying the result of this action, along with the
+ ID of the specific view to use for each mode, if any were required.
+
+ This function hides the logic of determining the precedence between
+ the view_modes string, the view_ids o2m, and the view_id m2o that
+ can be set on the action.
"""
for act in self:
act.views = [(view.view_id.id, view.view_mode) for view in act.view_ids]
- view_ids_modes = [view.view_mode for view in act.view_ids]
- modes = act.view_mode.split(',')
- missing_modes = [mode for mode in modes if mode not in view_ids_modes]
+ got_modes = [view.view_mode for view in act.view_ids]
+ all_modes = act.view_mode.split(',')
+ missing_modes = [mode for mode in all_modes if mode not in got_modes]
if missing_modes:
- if act.view_id and act.view_id.type in missing_modes:
+ if act.view_id.type in missing_modes:
# reorder missing modes to put view_id first if present
missing_modes.remove(act.view_id.type)
act.views.append((act.view_id.id, act.view_id.type))
act.views.extend([(False, mode) for mode in missing_modes])
- @api.depends('search_view')
- def _search_view(self):
+ @api.depends('res_model', 'search_view_id')
+ def _compute_search_view(self):
for act in self:
- act.search_view = str(self.env[act.res_model].fields_view_get(act.search_view_id and act.search_view_id.id, 'search'))
+ fvg = self.env[act.res_model].fields_view_get(act.search_view_id.id, 'search')
+ act.search_view = str(fvg)
- name = fields.Char(string='Action Name', required=True, translate=True)
- type = fields.Char(string='Action Type', default="ir.actions.act_window", required=True)
+ name = fields.Char(string='Action Name', translate=True)
+ type = fields.Char(default="ir.actions.act_window")
view_id = fields.Many2one('ir.ui.view', string='View Ref.', ondelete='set null')
domain = fields.Char(string='Domain Value',
help="Optional domain filtering of the destination data, as a Python expression")
@@ -304,7 +297,7 @@ class IrActionsActWindow(models.Model):
usage = fields.Char(string='Action Usage',
help="Used to filter menu and home actions from the user form.")
view_ids = fields.One2many('ir.actions.act_window.view', 'act_window_id', string='Views')
- views = fields.Binary(compute='_views_get_fnc',
+ views = fields.Binary(compute='_compute_views',
help="This function field computes the ordered list of views that should be enabled " \
"when displaying the result of an action, federating view mode, views and " \
"reference view. The result is returned as an ordered list of pairs (view_id,view_mode).")
@@ -315,26 +308,25 @@ class IrActionsActWindow(models.Model):
search_view_id = fields.Many2one('ir.ui.view', string='Search View Ref.')
filter = fields.Boolean()
auto_search = fields.Boolean(default=True)
- search_view = fields.Text(compute='_search_view')
+ search_view = fields.Text(compute='_compute_search_view')
multi = fields.Boolean(string='Restrict to lists', help="If checked and the action is bound to a model, it will only appear in the More menu on list views")
+ @api.v7
def read(self, cr, uid, ids, fields=None, context=None, load='_classic_read'):
+ result = IrActionsActWindow.read(self.browse(cr, uid, ids, context), fields, load=load)
+ return result if isinstance(ids, list) else (bool(result) and result[0])
+
+ @api.v8
+ def read(self, fields=None, load='_classic_read'):
""" call the method get_empty_list_help of the model and set the window action help message
"""
- ids_int = isinstance(ids, (int, long))
- if ids_int:
- ids = [ids]
- results = super(IrActionsActWindow, self).read(cr, uid, ids, fields=fields, context=context, load=load)
-
+ result = super(IrActionsActWindow, self).read(fields, load=load)
if not fields or 'help' in fields:
- for res in results:
- model = res.get('res_model')
- if model and self.pool.get(model):
- ctx = dict(context or {})
- res['help'] = self.pool[model].get_empty_list_help(cr, uid, res.get('help', ""), context=ctx)
- if ids_int:
- return results[0]
- return results
+ for values in result:
+ model = values.get('res_model')
+ if model in self.env:
+ values['help'] = self.env[model].get_empty_list_help(values.get('help', ""))
+ return result
@api.model
def for_xml_id(self, module, xml_id):
@@ -345,11 +337,8 @@ class IrActionsActWindow(models.Model):
attribute from the XML file)
:return: A read() view of the ir.actions.act_window
"""
- DataObj = self.env['ir.model.data']
- data_id = DataObj.sudo()._get_id(module, xml_id)
- res = DataObj.browse(data_id)
- result = self.env['ir.actions.act_window'].sudo().browse(res.res_id)
- return result
+ record = self.env.ref("%s.%s" % (module, xml_id))
+ return record.read()[0]
@api.model
def create(self, vals):
@@ -378,6 +367,16 @@ class IrActionsActWindow(models.Model):
return set(row[0] for row in self._cr.fetchall())
+VIEW_TYPES = [
+ ('tree', 'Tree'),
+ ('form', 'Form'),
+ ('graph', 'Graph'),
+ ('pivot', 'Pivot'),
+ ('calendar', 'Calendar'),
+ ('gantt', 'Gantt'),
+ ('kanban', 'Kanban'),
+]
+
class IrActionsActWindowView(models.Model):
_name = 'ir.actions.act_window.view'
_table = 'ir_act_window_view'
@@ -386,21 +385,16 @@ class IrActionsActWindowView(models.Model):
sequence = fields.Integer()
view_id = fields.Many2one('ir.ui.view', string='View')
- view_mode = fields.Selection([('tree', 'Tree'),
- ('form', 'Form'),
- ('graph', 'Graph'),
- ('pivot', 'Pivot'),
- ('calendar', 'Calendar'),
- ('gantt', 'Gantt'),
- ('kanban', 'Kanban')], string='View Type', required=True)
+ view_mode = fields.Selection(VIEW_TYPES, string='View Type', required=True)
act_window_id = fields.Many2one('ir.actions.act_window', string='Action', ondelete='cascade')
multi = fields.Boolean(string='On Multiple Doc.', help="If set to true, the action will not be displayed on the right toolbar of a form view.")
+ @api.model_cr_context
def _auto_init(self):
super(IrActionsActWindowView, self)._auto_init()
- self.env.cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = \'act_window_view_unique_mode_per_action\'')
- if not self.env.cr.fetchone():
- self.env.cr.execute('CREATE UNIQUE INDEX act_window_view_unique_mode_per_action ON ir_act_window_view (act_window_id, view_mode)')
+ self._cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = \'act_window_view_unique_mode_per_action\'')
+ if not self._cr.fetchone():
+ self._cr.execute('CREATE UNIQUE INDEX act_window_view_unique_mode_per_action ON ir_act_window_view (act_window_id, view_mode)')
class IrActionsActWindowclose(models.Model):
@@ -408,7 +402,7 @@ class IrActionsActWindowclose(models.Model):
_inherit = 'ir.actions.actions'
_table = 'ir_actions'
- type = fields.Char(string='Action Type', default='ir.actions.act_window_close')
+ type = fields.Char(default='ir.actions.act_window_close')
class IrActionsActUrl(models.Model):
@@ -418,8 +412,8 @@ class IrActionsActUrl(models.Model):
_sequence = 'ir_actions_id_seq'
_order = 'name'
- name = fields.Char(string='Action Name', required=True, translate=True)
- type = fields.Char(string='Action Type', default='ir.actions.act_url')
+ name = fields.Char(string='Action Name', translate=True)
+ type = fields.Char(default='ir.actions.act_url')
url = fields.Text(string='Action URL', required=True)
target = fields.Selection([('new', 'New Window'), ('self', 'This Window')],
string='Action Target', default='new', required=True)
@@ -453,10 +447,12 @@ class IrActionsServer(models.Model):
_sequence = 'ir_actions_id_seq'
_order = 'sequence,name'
+ @api.model
def _select_objects(self):
records = self.env['ir.model'].search([])
return [(record.model, record.name) for record in records] + [('', '')]
+ @api.model
def _get_states(self):
""" Override me in order to add new states in the server action. Please
note that the added key length should not be higher than already-existing
@@ -468,18 +464,17 @@ class IrActionsServer(models.Model):
('object_write', 'Write on a Record'),
('multi', 'Execute several actions')]
- @api.model
- def _get_states_wrapper(self):
- return self._get_states()
+ name = fields.Char(string='Action Name', translate=True)
+ type = fields.Char(default='ir.actions.server')
- name = fields.Char(string='Action Name', required=True, translate=True)
condition = fields.Char(default="True",
help="Condition verified before executing the server action. If it "
"is not verified, the action will not be executed. The condition is "
"a Python expression, like 'object.list_price > 5000'. A void "
"condition is considered as always True. Help about python expression "
"is given in the help tab.")
- state = fields.Selection(selection='_get_states_wrapper', string='Action To Do', default='code', required=True,
+ state = fields.Selection(selection=lambda self: self._get_states(),
+ string='Action To Do', default='code', required=True,
help="Type of server action. The following values are available:\n"
"- 'Execute Python Code': a block of python code that will be executed\n"
"- 'Trigger a Workflow Signal': send a signal to a workflow\n"
@@ -488,8 +483,6 @@ class IrActionsServer(models.Model):
"- 'Write on a Record': update the values of a record\n"
"- 'Execute several actions': define an action that triggers several other server actions\n"
"- 'Send Email': automatically send an email (available in email_template)")
- usage = fields.Char(string='Action Usage')
- type = fields.Char(string='Action Type', default='ir.actions.server', required=True)
# Generic
sequence = fields.Integer(default=5,
help="When dealing with multiple actions, the execution order is "
@@ -569,19 +562,19 @@ class IrActionsServer(models.Model):
id_object = fields.Reference(string='Record', selection='_select_objects')
id_value = fields.Char(string='Record ID')
- def _check_expression(self, expression, model_id):
+ def _check_expression(self, expression, model):
""" Check python expression (condition, write_expression). Each step of
the path must be a valid many2one field, or an integer field for the last
step.
:param str expression: a python expression, beginning by 'obj' or 'object'
- :param int model_id: the base model of the server action
+ :param model: a record of the model 'ir.model'
:returns tuple: (is_valid, target_model_name, error_msg)
"""
- if not model_id:
+ if not model:
return (False, None, 'Your expression cannot be validated because the Base Model is not set.')
# fetch current model
- current_model_name = self.env['ir.model'].browse(model_id).model
+ model_name = model.model
# transform expression into a path that should look like 'object.many2onefield.many2onefield'
path = expression.split('.')
initial = path.pop(0)
@@ -590,89 +583,71 @@ class IrActionsServer(models.Model):
# analyze path
while path:
step = path.pop(0)
- field = self.env[current_model_name]._fields.get(step)
+ field = self.env[model_name]._fields.get(step)
if not field:
- return (False, None, 'Part of the expression (%s) is not recognized as a column in the model %s.' % (step, current_model_name))
+ return (False, None, 'Part of the expression (%s) is not recognized as a column in the model %s.' % (step, model_name))
ftype = field.type
if ftype not in ['many2one', 'int']:
return (False, None, 'Part of the expression (%s) is not a valid column type (is %s, should be a many2one or an int)' % (step, ftype))
if ftype == 'int' and path:
return (False, None, 'Part of the expression (%s) is an integer field that is only allowed at the end of an expression' % (step))
if ftype == 'many2one':
- current_model_name = field.comodel_name
- return (True, current_model_name, None)
+ model_name = field.comodel_name
+ return (True, model_name, None)
@api.constrains('write_expression', 'model_id')
def _check_write_expression(self):
for record in self:
if record.write_expression and record.model_id:
- correct, model_name, message = self._check_expression(record.write_expression, record.model_id.id)
+ correct, model_name, message = self._check_expression(record.write_expression, record.model_id)
if not correct:
_logger.warning('Invalid expression: %s' % message)
- raise ValueError(_('Incorrect Write Record Expression'))
- return True
+ raise ValidationError(_('Incorrect Write Record Expression'))
- _constraints = [
- (partial(models.Model._check_m2m_recursion, field_name='child_ids'),
- 'Recursion found in child server actions',
- ['child_ids']),
- ]
+ @api.constrains('child_ids')
+ def _check_recursion(self):
+ if not self._check_m2m_recursion('child_ids'):
+ raise ValidationError(_('Recursion found in child server actions'))
- @api.onchange('model_id', 'wkf_model_id', 'crud_model_id')
- def on_change_model_id(self):
+ @api.onchange('model_id')
+ def _onchange_model_id(self):
""" When changing the action base model, reset workflow and crud config
to ease value coherence. """
- values = {
- 'use_create': 'new',
- 'use_write': 'current',
- 'use_relational_model': 'base',
- 'wkf_model_id': self.model_id.id,
- 'wkf_field_id': False,
- 'crud_model_id': self.model_id.id,
- }
-
- if self.model_id:
- values['model_name'] = self.env['ir.model'].browse(self.model_id.id).model
-
- return {'value': values}
-
- @api.onchange('use_relational_model', 'wkf_field_id', 'wkf_model_id', 'model_id')
- def on_change_wkf_wonfig(self):
+ self.use_create = 'new'
+ self.use_write = 'current'
+ self.use_relational_model = 'base'
+ self.wkf_model_id = self.model_id
+ self.wkf_field_id = False
+ self.crud_model_id = self.model_id
+
+ @api.onchange('use_relational_model', 'wkf_field_id')
+ def _onchange_wkf_config(self):
""" Update workflow type configuration
- update the workflow model (for base (model_id) /relational (field.relation))
- update wkf_transition_id to False if workflow model changes, to force
the user to choose a new one
"""
- values = {}
if self.use_relational_model == 'relational' and self.wkf_field_id:
- field = self.env['ir.model.fields'].browse(self.wkf_field_id.id)
- new_wkf_model = self.env['ir.model'].search([('model', '=', field.relation)])
- values['wkf_model_id'] = new_wkf_model.id
+ field = self.wkf_field_id
+ self.wkf_model_id = self.env['ir.model'].search([('model', '=', field.relation)])
else:
- values['wkf_model_id'] = self.model_id.id
- return {'value': values}
+ self.wkf_model_id = self.model_id
@api.onchange('wkf_model_id')
- def on_change_wkf_model_id(self):
+ def _onchange_wkf_model_id(self):
""" When changing the workflow model, update its stored name also """
- values = {'wkf_transition_id': False}
- if self.wkf_model_id:
- values['wkf_model_name'] = self.env['ir.model'].browse(self.wkf_model_id.id).model
- return {'value': values}
+ self.wkf_transition_id = False
- @api.onchange('state', 'use_create', 'use_write', 'ref_object', 'crud_model_id', 'model_id')
- def on_change_crud_config(self):
+ @api.onchange('use_create', 'use_write', 'ref_object')
+ def _onchange_crud_config(self):
""" Wrapper on CRUD-type (create or write) on_change """
if self.state == 'object_create':
- return self.on_change_create_config(self.use_create, self.ref_object, self.crud_model_id.id, self.model_id.id)
+ self._onchange_create_config()
elif self.state == 'object_write':
- return self.on_change_write_config(self.use_write, self.ref_object, self.crud_model_id.id, self.model_id.id)
- else:
- return {}
+ self._onchange_write_config()
- @api.multi
- def on_change_create_config(self, use_create, ref_object, crud_model_id, model_id):
+ def _onchange_create_config(self):
""" When changing the object_create type configuration:
- `new` and `copy_current`: crud_model_id is the same as base model
@@ -681,74 +656,65 @@ class IrActionsServer(models.Model):
- if the target model has changed, then reset the link field that is
probably not correct anymore
"""
- values = {}
- if use_create == 'new':
- values['crud_model_id'] = model_id
- elif use_create == 'new_other':
+ crud_model_id = self.crud_model_id
+
+ if self.use_create == 'new':
+ self.crud_model_id = self.model_id
+ elif self.use_create == 'new_other':
pass
- elif use_create == 'copy_current':
- values['crud_model_id'] = model_id
- elif use_create == 'copy_other' and ref_object:
- ref_model = str(ref_object).split('(')
- ref_model = self.env['ir.model'].search([('model', '=', ref_model)])
- values['crud_model_id'] = ref_model.id
+ elif self.use_create == 'copy_current':
+ self.crud_model_id = self.model_id
+ elif self.use_create == 'copy_other' and self.ref_object:
+ ref_model = self.ref_object._name
+ self.crud_model_id = self.env['ir.model'].search([('model', '=', ref_model)])
- if values.get('crud_model_id') != crud_model_id:
- values['link_field_id'] = False
- return {'value': values}
+ if self.crud_model_id != crud_model_id:
+ self.link_field_id = False
- @api.multi
- def on_change_write_config(self, use_write, ref_object, crud_model_id, model_id):
+ def _onchange_write_config(self):
""" When changing the object_write type configuration:
- `current`: crud_model_id is the same as base model
- `other`: disassemble the reference object to have its model
- `expression`: has its own on_change, nothing special here
"""
- values = {}
- if use_write == 'current':
- values['crud_model_id'] = model_id
- elif use_write == 'other' and ref_object:
- ref_model = str(ref_object).split('(')
- ref_model = self.env['ir.model'].search([('model', '=', ref_model)])
- values['crud_model_id'] = ref_model.id
- elif use_write == 'expression':
+ crud_model_id = self.crud_model_id
+
+ if self.use_write == 'current':
+ self.crud_model_id = self.model_id
+ elif self.use_write == 'other' and self.ref_object:
+ ref_model = self.ref_object._name
+ self.crud_model_id = self.env['ir.model'].search([('model', '=', ref_model)])
+ elif self.use_write == 'expression':
pass
- if values.get('crud_model_id') != crud_model_id:
- values['link_field_id'] = False
- return {'value': values}
+ if self.crud_model_id != crud_model_id:
+ self.link_field_id = False
+
+ @api.onchange('crud_model_id')
+ def _onchange_crud_model_id(self):
+ """ When changing the CRUD model, update its stored name also """
+ self.link_field_id = False
- @api.onchange('write_expression', 'model_id')
- def on_change_write_expression(self):
+ @api.onchange('write_expression')
+ def _onchange_write_expression(self):
""" Check the write_expression and update crud_model_id accordingly """
values = {}
if self.write_expression:
- valid, model_name, message = self._check_expression(self.write_expression, self.model_id.id)
+ valid, model_name, message = self._check_expression(self.write_expression, self.model_id)
else:
valid, model_name, message = True, None, False
if self.model_id:
- model_name = self.env['ir.model'].browse(self.model_id.id).model
+ model_name = self.model_id.model
if not valid:
return {
'warning': {
- 'title': 'Incorrect expression',
- 'message': message or 'Invalid expression',
+ 'title': _('Incorrect expression'),
+ 'message': message or _('Invalid expression'),
}
}
if model_name:
- ref_model = self.env['ir.model'].search([('model', '=', model_name)])
- values['crud_model_id'] = ref_model.id
- return {'value': values}
- return {'value': {}}
-
- @api.onchange('crud_model_id')
- def on_change_crud_model_id(self):
- """ When changing the CRUD model, update its stored name also """
- values = {'link_field_id': False}
- if self.crud_model_id:
- values['crud_model_name'] = self.env['ir.model'].browse(self.crud_model_id.id).model
- return {'value': values}
+ self.crud_model_id = self.env['ir.model'].search([('model', '=', model_name)])
def _build_expression(self, field_name, sub_field_name):
""" Returns a placeholder expression for use in a template field,
@@ -766,50 +732,39 @@ class IrActionsServer(models.Model):
return expression
@api.onchange('model_object_field', 'sub_model_object_field')
- def onchange_sub_model_object_value_field(self):
- result = {
- 'sub_object': False,
- 'copyvalue': False,
- 'sub_model_object_field': False,
- }
- if self.model_object_field:
- FieldsObj = self.env['ir.model.fields']
- field_value = FieldsObj.browse(self.model_object_field.id)
- if field_value.ttype in ['many2one', 'one2many', 'many2many']:
- res_ids = self.env['ir.model'].search([('model', '=', field_value.relation)])
- if self.sub_model_object_field:
- sub_field_value = FieldsObj.browse(self.sub_model_object_field.id)
- if res_ids:
- result.update({
- 'sub_object': res_ids.id,
- 'copyvalue': self._build_expression(field_value.name, sub_field_value and sub_field_value.name or False),
- 'sub_model_object_field': self.sub_model_object_field,
- })
+ def _onchange_model_object_field(self):
+ field = self.model_object_field
+ sub_field = self.sub_model_object_field
+
+ self.sub_object = False
+ self.sub_model_object_field = False
+ self.copyvalue = False
+
+ if field:
+ if field.ttype in ['many2one', 'one2many', 'many2many']:
+ comodel = self.env['ir.model'].search([('model', '=', field.relation)])
+ if comodel:
+ self.sub_object = comodel
+ self.copyvalue = self._build_expression(field.name, sub_field.name)
+ self.sub_model_object_field = sub_field
else:
- result.update({
- 'copyvalue': self._build_expression(field_value.name, False),
- })
- return {'value': result}
+ self.copyvalue = self._build_expression(field.name, False)
@api.onchange('id_object')
- def onchange_id_object(self):
- if self.id_object:
- return {'value': {'id_value': self.id_object.id}}
- return {'value': {'id_value': False}}
+ def _onchange_id_object(self):
+ self.id_value = self.id_object.id if self.id_object else False
@api.multi
def create_action(self):
- """ Create a contextual action for each of the server actions. """
+ """ Create a contextual action for each server action. """
for action in self:
- ir_values_id = self.env['ir.values'].sudo().create({
+ ir_values = self.env['ir.values'].sudo().create({
'name': _('Run %s') % action.name,
'model': action.model_id.model,
'key2': 'client_action_multi',
'value': "ir.actions.server,%s" % action.id,
})
- action.write({
- 'menu_ir_values_id': ir_values_id.id,
- })
+ action.write({'menu_ir_values_id': ir_values.id})
return True
@api.multi
@@ -828,7 +783,8 @@ class IrActionsServer(models.Model):
def run_action_client_action(self, action, eval_context=None):
if not action.action_id:
raise UserError(_("Please specify an action to launch!"))
- return self.env[action.action_id.type].browse(action.action_id.id)[0]
+ record = self.env[action.action_id.type].browse(action.action_id.id)
+ return record.read()[0]
@api.model
def run_action_code_multi(self, action, eval_context=None):
@@ -845,7 +801,7 @@ class IrActionsServer(models.Model):
field, then target_model_pool.signal_workflow(cr, uid, target_id, <TRIGGER_NAME>)
"""
# weird signature and calling -> no self.env, use action param's
- record = action.env[action.model_id.model].browse(self.env.context['active_id'])
+ record = action.env[action.model_id.model].browse(self._context['active_id'])
if action.use_relational_model == 'relational':
record = getattr(record, action.wkf_field_id.name)
if not isinstance(record, models.BaseModel):
@@ -879,15 +835,15 @@ class IrActionsServer(models.Model):
if action.use_write == 'current':
model = action.model_id.model
- ref_id = self.env.context.get('active_id')
+ ref_id = self._context.get('active_id')
elif action.use_write == 'other':
model = action.crud_model_id.model
ref_id = action.ref_object.id
elif action.use_write == 'expression':
model = action.crud_model_id.model
ref = eval(action.write_expression, eval_context)
- if isinstance(ref, odoo.osv.orm.browse_record):
- ref_id = getattr(ref, 'id')
+ if isinstance(ref, models.BaseModel):
+ ref_id = ref.id
else:
ref_id = int(ref)
@@ -915,17 +871,17 @@ class IrActionsServer(models.Model):
elif action.use_create in ['new_other', 'copy_other']:
model = action.crud_model_id.model
- ObjPool = self.env[model]
if action.use_create == 'copy_current':
- ref_id = self.env.context.get('active_id')
- res = ObjPool.browse(ref_id).copy(res)
+ ref_id = self._context.get('active_id')
+ res = self.env[model].browse(ref_id).copy(res)
elif action.use_create == 'copy_other':
res = action.ref_object.copy(res)
else:
- res = ObjPool.create(res)
+ res = self.env[model].create(res)
if action.link_new_record and action.link_field_id:
- self.env[action.model_id.model].browse(self.env.context.get('active_id')).write({action.link_field_id.name: res.id})
+ record = self.env[action.model_id.model].browse(self._context.get('active_id'))
+ record.write({action.link_field_id.name: res.id})
@api.model
def _get_eval_context(self, action=None):
@@ -936,22 +892,21 @@ class IrActionsServer(models.Model):
:type action: browse record
:returns: dict -- evaluation context given to (safe_)eval """
def log(message, level="info"):
- val = ('server', self.env.cr.dbname, __name__, level, message, "action", action.id, action.name)
- self.env.cr.execute("""
+ self._cr.execute("""
INSERT INTO ir_logging(create_date, create_uid, type, dbname, name, level, message, path, line, func)
VALUES (NOW() at time zone 'UTC', %s, %s, %s, %s, %s, %s, %s, %s, %s)
- """, val)
+ """, ('server', self._cr.dbname, __name__, level, message, "action", action.id, action.name))
eval_context = super(IrActionsServer, self)._get_eval_context(action=action)
- Model = self.env[action.model_id.model]
- if self.env.context.get('active_model') == action.model_id.model and self.env.context.get('active_id'):
- obj = Model.browse(self.env.context['active_id'])
- if self.env.context.get('onchange_self'):
- obj = self.env.context['onchange_self']
+ model = self.env[action.model_id.model]
+ if self._context.get('active_model') == action.model_id.model and self._context.get('active_id'):
+ obj = model.browse(self._context['active_id'])
+ if self._context.get('onchange_self'):
+ obj = self._context['onchange_self']
eval_context.update({
# orm
'env': self.env,
- 'model': Model,
+ 'model': model,
'workflow': workflow,
# Exceptions
'Warning': odoo.exceptions.Warning,
@@ -961,10 +916,10 @@ class IrActionsServer(models.Model):
'object': obj,
'obj': obj,
# Deprecated use env or model instead
- 'self': self.env[action.model_id.model],
- 'pool': self.env,
- 'cr': self.env.cr,
- 'context': self.env.context,
+ 'self': model._model,
+ 'pool': self.pool,
+ 'cr': self._cr,
+ 'context': self._context,
'user': self.env.user,
# helpers
'log': log,
@@ -1002,20 +957,22 @@ class IrActionsServer(models.Model):
if not expr:
continue
# call the multi method
- func = getattr(self, 'run_action_%s_multi' % action.state)
+ run_self = self.with_context(eval_context['context'])
+ func = getattr(run_self, 'run_action_%s_multi' % action.state)
res = func(action, eval_context=eval_context)
elif hasattr(self, 'run_action_%s' % action.state):
- func = getattr(self, 'run_action_%s' % action.state)
- active_id = self.env.context.get('active_id')
- active_ids = self.env.context.get('active_ids', [active_id] if active_id else [])
+ active_id = self._context.get('active_id')
+ active_ids = self._context.get('active_ids', [active_id] if active_id else [])
for active_id in active_ids:
# run context dedicated to a particular active_id
- eval_context["context"] = dict(self.env.context, active_ids=[active_id], active_id=active_id)
+ run_self = self.with_context(active_ids=[active_id], active_id=active_id)
+ eval_context["context"] = run_self._context
expr = eval(str(condition), eval_context)
if not expr:
continue
# call the single method related to the action: run_action_<STATE>
+ func = getattr(run_self, 'run_action_%s' % action.state)
res = func(action, eval_context=eval_context)
return res
@@ -1057,7 +1014,7 @@ class IrActionsTodo(models.Model):
_description = "Configuration Wizards"
_order = "sequence, id"
- action_id = fields.Many2one('ir.actions.actions', string='Action', select=True, required=True)
+ action_id = fields.Many2one('ir.actions.actions', string='Action', required=True, index=True)
sequence = fields.Integer(default=10)
state = fields.Selection([('open', 'To Do'), ('done', 'Done')], string='Status', default='open', required=True)
name = fields.Char()
@@ -1071,7 +1028,6 @@ class IrActionsTodo(models.Model):
note = fields.Text(string='Text', translate=True)
@api.multi
- @api.depends('action_id', 'name')
def name_get(self):
return [(record.id, record.action_id.name) for record in self]
@@ -1080,35 +1036,34 @@ class IrActionsTodo(models.Model):
if args is None:
args = []
if name:
- action = self.search([('action_id', operator, name)] + args, limit=limit)
- return action.name_get()
+ actions = self.search([('action_id', operator, name)] + args, limit=limit)
+ return actions.name_get()
return super(IrActionsTodo, self).name_search(name, args=args, operator=operator, limit=limit)
@api.multi
def action_launch(self, context=None):
""" Launch Action of Wizard"""
- wizard_id = self.ids and self.ids[0]
- wizard = self.browse(wizard_id)
- if wizard.type in ('automatic', 'once'):
- wizard.write({'state': 'done'})
+ self.ensure_one()
+ if self.type in ('automatic', 'once'):
+ self.write({'state': 'done'})
# Load action
- act_type = wizard.action_id.type
+ action = self.env[self.action_id.type].browse(self.action_id.id)
- result = self.env[act_type].browse(wizard.action_id.id).read()[0]
- if act_type != 'ir.actions.act_window':
+ result = action.read()[0]
+ if action._name != 'ir.actions.act_window':
return result
result.setdefault('context', '{}')
# Open a specific record when res_id is provided in the context
ctx = eval(result['context'], {'user': self.env.user})
if ctx.get('res_id'):
- result.update({'res_id': ctx.pop('res_id')})
+ result['res_id'] = ctx.pop('res_id')
# disable log for automatic wizards
- if wizard.type == 'automatic':
- ctx.update({'disable_log': True})
- result.update({'context': ctx})
+ if self.type == 'automatic':
+ ctx['disable_log'] = True
+ result['context'] = ctx
return result
@@ -1117,6 +1072,7 @@ class IrActionsTodo(models.Model):
""" Sets configuration wizard in TODO state"""
return self.write({'state': 'open'})
+ @api.multi
def progress(self):
""" Returns a dict with 3 keys {todo, done, total}.
@@ -1126,14 +1082,11 @@ class IrActionsTodo(models.Model):
:rtype: dict
"""
- user_groups = set(map(lambda x: x.id, self.env.user.groups_id))
+ user_groups = self.env.user.groups_id
def groups_match(todo):
- """ Checks if the todo's groups match those of the current user
- """
- return not todo.groups_id \
- or bool(user_groups.intersection((
- group.id for group in todo.groups_id)))
+ """ Checks if the todo's groups match those of the current user """
+ return not todo.groups_id or bool(todo.groups_id & user_groups)
done = filter(groups_match, self.browse(self.search([('state', '!=', 'open')])))
total = filter(groups_match, self.browse(self.search([])))
@@ -1141,7 +1094,7 @@ class IrActionsTodo(models.Model):
return {
'done': len(done),
'total': len(total),
- 'todo': len(total) - len(done)
+ 'todo': len(total) - len(done),
}
@@ -1152,28 +1105,27 @@ class IrActionsActClient(models.Model):
_sequence = 'ir_actions_id_seq'
_order = 'name'
- name = fields.Char(string='Action Name', required=True, translate=True)
+ name = fields.Char(string='Action Name', translate=True)
+ type = fields.Char(default='ir.actions.client')
+
tag = fields.Char(string='Client action tag', required=True,
help="An arbitrary string, interpreted by the client"
" according to its own needs and wishes. There "
"is no central tag repository across clients.")
res_model = fields.Char(string='Destination Model', help="Optional model, mostly used for needactions.")
- context = fields.Char(string='Context Value', default={}, required=True, help="Context dictionary as Python expression, empty by default (Default: {})")
- params = fields.Binary(compute='_get_params', inverse='_set_params', string='Supplementary arguments',
+ context = fields.Char(string='Context Value', default="{}", required=True, help="Context dictionary as Python expression, empty by default (Default: {})")
+ params = fields.Binary(compute='_compute_params', inverse='_inverse_params', string='Supplementary arguments',
help="Arguments sent to the client along with"
"the view tag")
params_store = fields.Binary(string='Params storage', readonly=True)
- type = fields.Char(string='Action Type', default='ir.actions.client')
@api.depends('params')
- def _get_params(self):
- # Need to remove bin_size from context, to obtains the binary and not the length.
- self = self.with_context(bin_size_params_store=False)
- for record in self:
- record.params = record.params_store and eval(record.params_store, {'uid': self.env.uid})
+ def _compute_params(self):
+ self_bin = self.with_context(bin_size=False, bin_size_params_store=False)
+ for record, record_bin in zip(self, self_bin):
+ record.params = record_bin.params_store and eval(record_bin.params_store, {'uid': self._uid})
- def _set_params(self):
- if isinstance(self.params, dict):
- self.write({'params_store': repr(self.params)})
- else:
- self.write({'params_store': self.params})
+ def _inverse_params(self):
+ for record in self:
+ params = record.params
+ record.params_store = repr(params) if isinstance(params, dict) else params
diff --git a/openerp/addons/base/ir/ir_attachment.py b/openerp/addons/base/ir/ir_attachment.py
index 18311e0ce53..6db01014d05 100644
--- a/openerp/addons/base/ir/ir_attachment.py
+++ b/openerp/addons/base/ir/ir_attachment.py
@@ -7,109 +7,105 @@ import logging
import mimetypes
import os
import re
+from collections import defaultdict
-from openerp import SUPERUSER_ID, tools
-from openerp.exceptions import AccessError
-from openerp.osv import fields,osv
-from openerp.tools.translate import _
-from openerp.tools.misc import ustr
-from openerp.tools.mimetypes import guess_mimetype
+from odoo import api, fields, models, SUPERUSER_ID, _
+from odoo.exceptions import AccessError
+from odoo.tools import config, human_size, ustr
+from odoo.tools.mimetypes import guess_mimetype
_logger = logging.getLogger(__name__)
-class ir_attachment(osv.osv):
+
+class IrAttachment(models.Model):
"""Attachments are used to link binary files or url to any openerp document.
External attachment storage
---------------------------
- The 'data' function field (_data_get,data_set) is implemented using
- _file_read, _file_write and _file_delete which can be overridden to
- implement other storage engines, such methods should check for other
- location pseudo uri (example: hdfs://hadoppserver)
+ The computed field ``datas`` is implemented using ``_file_read``,
+ ``_file_write`` and ``_file_delete``, which can be overridden to implement
+ other storage engines. Such methods should check for other location pseudo
+ uri (example: hdfs://hadoppserver).
The default implementation is the file:dirname location that stores files
on the local filesystem using name based on their sha1 hash
"""
+ _name = 'ir.attachment'
_order = 'id desc'
- def _name_get_resname(self, cr, uid, ids, object, method, context):
- data = {}
- for attachment in self.browse(cr, uid, ids, context=context):
- model_object = attachment.res_model
- res_id = attachment.res_id
- if model_object and res_id:
- model_pool = self.pool[model_object]
- res = model_pool.name_get(cr,uid,[res_id],context)
- res_name = res and res[0][1] or None
- if res_name:
- field = self._columns.get('res_name',False)
- if field and len(res_name) > field.size:
- res_name = res_name[:30] + '...'
- data[attachment.id] = res_name or False
- else:
- data[attachment.id] = False
- return data
- def _storage(self, cr, uid, context=None):
- return self.pool['ir.config_parameter'].get_param(cr, SUPERUSER_ID, 'ir_attachment.location', 'file')
+ @api.depends('res_model', 'res_id')
+ def _compute_res_name(self):
+ for attachment in self:
+ if attachment.res_model and attachment.res_id:
+ record = self.env[attachment.res_model].browse(attachment.res_id)
+ attachment.res_name = record.display_name
- def _filestore(self, cr, uid, context=None):
- return tools.config.filestore(cr.dbname)
+ @api.model
+ def _storage(self):
+ return self.env['ir.config_parameter'].sudo().get_param('ir_attachment.location', 'file')
- def force_storage(self, cr, uid, context=None):
+ @api.model
+ def _filestore(self):
+ return config.filestore(self._cr.dbname)
+
+ @api.model
+ def force_storage(self):
"""Force all attachments to be stored in the currently configured storage"""
- if not self.pool['res.users']._is_admin(cr, uid, [uid]):
+ if not self.env.user._is_admin():
raise AccessError(_('Only administrators can execute this action.'))
- location = self._storage(cr, uid, context)
+ # domain to retrieve the attachments to migrate
domain = {
'db': [('store_fname', '!=', False)],
'file': [('db_datas', '!=', False)],
- }[location]
+ }[self._storage()]
- ids = self.search(cr, uid, domain, context=context)
- for attach in self.browse(cr, uid, ids, context=context):
+ for attach in self.search(domain):
attach.write({'datas': attach.datas})
return True
- # 'data' field implementation
- def _full_path(self, cr, uid, path):
- # sanitize ath
+ @api.model
+ def _full_path(self, path):
+ # sanitize path
path = re.sub('[.]', '', path)
path = path.strip('/\\')
- return os.path.join(self._filestore(cr, uid), path)
+ return os.path.join(self._filestore(), path)
- def _get_path(self, cr, uid, bin_data, sha):
+ @api.model
+ def _get_path(self, bin_data, sha):
# retro compatibility
fname = sha[:3] + '/' + sha
- full_path = self._full_path(cr, uid, fname)
+ full_path = self._full_path(fname)
if os.path.isfile(full_path):
return fname, full_path # keep existing path
# scatter files across 256 dirs
# we use '/' in the db (even on windows)
fname = sha[:2] + '/' + sha
- full_path = self._full_path(cr, uid, fname)
+ full_path = self._full_path(fname)
dirname = os.path.dirname(full_path)
if not os.path.isdir(dirname):
os.makedirs(dirname)
return fname, full_path
- def _file_read(self, cr, uid, fname, bin_size=False):
- full_path = self._full_path(cr, uid, fname)
+ @api.model
+ def _file_read(self, fname, bin_size=False):
+ full_path = self._full_path(fname)
r = ''
try:
if bin_size:
- r = os.path.getsize(full_path)
+ r = human_size(os.path.getsize(full_path))
else:
r = open(full_path,'rb').read().encode('base64')
except (IOError, OSError):
_logger.info("_read_file reading %s", full_path, exc_info=True)
return r
- def _file_write(self, cr, uid, value, checksum):
+ @api.model
+ def _file_write(self, value, checksum):
bin_value = value.decode('base64')
- fname, full_path = self._get_path(cr, uid, bin_value, checksum)
+ fname, full_path = self._get_path(bin_value, checksum)
if not os.path.exists(full_path):
try:
with open(full_path, 'wb') as fp:
@@ -118,11 +114,12 @@ class ir_attachment(osv.osv):
_logger.info("_file_write writing %s", full_path, exc_info=True)
return fname
- def _file_delete(self, cr, uid, fname):
+ @api.model
+ def _file_delete(self, fname):
# using SQL to include files hidden through unlink or due to record rules
- cr.execute("SELECT COUNT(*) FROM ir_attachment WHERE store_fname = %s", (fname,))
- count = cr.fetchone()[0]
- full_path = self._full_path(cr, uid, fname)
+ self._cr.execute("SELECT COUNT(*) FROM ir_attachment WHERE store_fname = %s", (fname,))
+ count = self._cr.fetchone()[0]
+ full_path = self._full_path(fname)
if not count and os.path.exists(full_path):
try:
os.unlink(full_path)
@@ -132,60 +129,39 @@ class ir_attachment(osv.osv):
# Harmless and needed for race conditions
_logger.info("_file_delete could not unlink %s", full_path, exc_info=True)
- def _data_get(self, cr, uid, ids, name, arg, context=None):
- if context is None:
- context = {}
- result = {}
- bin_size = context.get('bin_size')
- for attach in self.browse(cr, uid, ids, context=context):
+ @api.depends('store_fname', 'db_datas')
+ def _compute_datas(self):
+ bin_size = self._context.get('bin_size')
+ for attach in self:
if attach.store_fname:
- result[attach.id] = self._file_read(cr, uid, attach.store_fname, bin_size)
+ attach.datas = self._file_read(attach.store_fname, bin_size)
else:
- result[attach.id] = attach.db_datas
- return result
-
- def _data_set(self, cr, uid, id, name, value, arg, context=None):
- # compute the field depending of datas, supporting the case of a empty/None datas
- bin_data = value and value.decode('base64') or '' # empty string to compute its hash
- checksum = self._compute_checksum(bin_data)
- vals = {
- 'file_size': len(bin_data),
- 'checksum' : checksum,
- }
- # We dont handle setting data to null
- # datas is false, but file_size and checksum are not (computed as datas is an empty string)
- if not value:
- # reset computed fields
- super(ir_attachment, self).write(cr, SUPERUSER_ID, [id], vals, context=context)
- return True
- if context is None:
- context = {}
- # browse the attachment and get the file to delete
- attach = self.browse(cr, uid, id, context=context)
- fname_to_delete = attach.store_fname
- location = self._storage(cr, uid, context)
- # compute the index_content field
- vals['index_content'] = self._index(cr, SUPERUSER_ID, bin_data, attach.datas_fname, attach.mimetype),
- if location != 'db':
- # create the file
- fname = self._file_write(cr, uid, value, checksum)
- vals.update({
- 'store_fname': fname,
- 'db_datas': False
- })
- else:
- vals.update({
+ attach.datas = attach.db_datas
+
+ def _inverse_datas(self):
+ location = self._storage()
+ for attach in self:
+ # compute the fields that depend on datas
+ value = attach.datas
+ bin_data = value and value.decode('base64') or ''
+ vals = {
+ 'file_size': len(bin_data),
+ 'checksum': self._compute_checksum(bin_data),
+ 'index_content': self._index(bin_data, attach.datas_fname, attach.mimetype),
'store_fname': False,
- 'db_datas': value
- })
- # SUPERUSER_ID as probably don't have write access, trigger during create
- super(ir_attachment, self).write(cr, SUPERUSER_ID, [id], vals, context=context)
-
- # After de-referencing the file in the database, check whether we need
- # to garbage-collect it on the filesystem
- if fname_to_delete:
- self._file_delete(cr, uid, fname_to_delete)
- return True
+ 'db_datas': value,
+ }
+ if value and location != 'db':
+ # save it to the filestore
+ vals['store_fname'] = self._file_write(value, vals['checksum'])
+ vals['db_datas'] = False
+
+ # take current location in filestore to possibly garbage-collect it
+ fname = attach.store_fname
+ # write as superuser, as user probably does not have write access
+ super(IrAttachment, attach.sudo()).write(vals)
+ if fname:
+ self._file_delete(fname)
def _compute_checksum(self, bin_data):
""" compute the checksum for the given datas
@@ -208,7 +184,8 @@ class ir_attachment(osv.osv):
mimetype = guess_mimetype(values['datas'].decode('base64'))
return mimetype
- def _index(self, cr, uid, bin_data, datas_fname, file_type):
+ @api.model
+ def _index(self, bin_data, datas_fname, file_type):
""" compute the index content of the given filename, or binary data.
This is a python implementation of the unix command 'strings'.
:param bin_data : datas in binary form
@@ -222,104 +199,93 @@ class ir_attachment(osv.osv):
index_content = ustr("\n".join(words))
return index_content
-
- _name = 'ir.attachment'
- _columns = {
- 'name': fields.char('Attachment Name', required=True),
- 'datas_fname': fields.char('File Name'),
- 'description': fields.text('Description'),
- 'res_name': fields.function(_name_get_resname, type='char', string='Resource Name', store=True),
- 'res_model': fields.char('Resource Model', readonly=True, help="The database object this attachment will be attached to"),
- 'res_field': fields.char('Resource Field', readonly=True),
- 'res_id': fields.integer('Resource ID', readonly=True, help="The record id this is attached to"),
- 'create_date': fields.datetime('Date Created', readonly=True),
- 'create_uid': fields.many2one('res.users', 'Owner', readonly=True),
- 'company_id': fields.many2one('res.company', 'Company', change_default=True),
- 'type': fields.selection( [ ('url','URL'), ('binary','File'), ],
- 'Type', help="You can either upload a file from your computer or copy/paste an internet link to your file", required=True, change_default=True),
- 'url': fields.char('Url', size=1024),
- # al: We keep shitty field names for backward compatibility with document
- 'datas': fields.function(_data_get, fnct_inv=_data_set, string='File Content', type="binary", nodrop=True),
- 'store_fname': fields.char('Stored Filename'),
- 'db_datas': fields.binary('Database Data'),
- # computed fields depending on datas
- 'file_size': fields.integer('File Size', readonly=True),
- 'checksum': fields.char("Checksum/SHA1", size=40, select=True, readonly=True),
- 'mimetype': fields.char('Mime Type', readonly=True),
- 'index_content': fields.text('Indexed Content', readonly=True, _prefetch=False),
- 'public': fields.boolean('Is public document'),
- }
-
- _defaults = {
- 'type': 'binary',
- 'file_size': 0,
- 'mimetype' : False,
- 'company_id': lambda s,cr,uid,c: s.pool.get('res.company')._company_default_get(cr, uid, 'ir.attachment', context=c),
- }
-
- def _auto_init(self, cr, context=None):
- super(ir_attachment, self)._auto_init(cr, context)
- cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = %s', ('ir_attachment_res_idx',))
- if not cr.fetchone():
- cr.execute('CREATE INDEX ir_attachment_res_idx ON ir_attachment (res_model, res_id)')
- cr.commit()
-
- def check(self, cr, uid, ids, mode, context=None, values=None):
+ name = fields.Char('Attachment Name', required=True)
+ datas_fname = fields.Char('File Name')
+ description = fields.Text('Description')
+ res_name = fields.Char('Resource Name', compute='_compute_res_name', store=True)
+ res_model = fields.Char('Resource Model', readonly=True, help="The database object this attachment will be attached to.")
+ res_field = fields.Char('Resource Field', readonly=True)
+ res_id = fields.Integer('Resource ID', readonly=True, help="The record id this is attached to.")
+ create_date = fields.Datetime('Date Created', readonly=True)
+ create_uid = fields.Many2one('res.users', string='Owner', readonly=True)
+ company_id = fields.Many2one('res.company', string='Company', change_default=True,
+ default=lambda self: self.env['res.company']._company_default_get('ir.attachment'))
+ type = fields.Selection([('url', 'URL'), ('binary', 'File')],
+ string='Type', required=True, default='binary', change_default=True,
+ help="You can either upload a file from your computer or copy/paste an internet link to your file.")
+ url = fields.Char('Url', size=1024)
+ public = fields.Boolean('Is public document')
+
+ # the field 'datas' is computed and may use the other fields below
+ datas = fields.Binary(string='File Content', compute='_compute_datas', inverse='_inverse_datas')
+ db_datas = fields.Binary('Database Data')
+ store_fname = fields.Char('Stored Filename')
+ file_size = fields.Integer('File Size', readonly=True)
+ checksum = fields.Char("Checksum/SHA1", size=40, index=True, readonly=True)
+ mimetype = fields.Char('Mime Type', readonly=True)
+ index_content = fields.Text('Indexed Content', readonly=True, _prefetch=False)
+
+ @api.model_cr_context
+ def _auto_init(self):
+ super(IrAttachment, self)._auto_init()
+ self._cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = %s', ('ir_attachment_res_idx',))
+ if not self._cr.fetchone():
+ self._cr.execute('CREATE INDEX ir_attachment_res_idx ON ir_attachment (res_model, res_id)')
+ self._cr.commit()
+
+ @api.model
+ def check(self, mode, values=None):
"""Restricts the access to an ir.attachment, according to referred model
In the 'document' module, it is overriden to relax this hard rule, since
more complex ones apply there.
"""
- res_ids = {}
+ # collect the records to check (by model)
+ model_ids = defaultdict(set) # {model_name: set(ids)}
require_employee = False
- if ids:
- if isinstance(ids, (int, long)):
- ids = [ids]
- cr.execute('SELECT res_model, res_id, create_uid, public FROM ir_attachment WHERE id = ANY (%s)', (ids,))
- for rmod, rid, create_uid, public in cr.fetchall():
+ if self:
+ self._cr.execute('SELECT res_model, res_id, create_uid, public FROM ir_attachment WHERE id IN %s', [tuple(self.ids)])
+ for res_model, res_id, create_uid, public in self._cr.fetchall():
if public and mode == 'read':
continue
- if not (rmod and rid):
- if create_uid != uid:
+ if not (res_model and res_id):
+ if create_uid != self._uid:
require_employee = True
continue
- res_ids.setdefault(rmod,set()).add(rid)
- if values:
- if values.get('res_model') and values.get('res_id'):
- res_ids.setdefault(values['res_model'],set()).add(values['res_id'])
-
- ima = self.pool.get('ir.model.access')
- for model, mids in res_ids.items():
- # ignore attachments that are not attached to a resource anymore when checking access rights
- # (resource was deleted but attachment was not)
- if not self.pool.get(model):
+ model_ids[res_model].add(res_id)
+ if values and values.get('res_model') and values.get('res_id'):
+ model_ids[values['res_model']].add(values['res_id'])
+
+ # check access rights on the records
+ for res_model, res_ids in model_ids.iteritems():
+ # ignore attachments that are not attached to a resource anymore
+ # when checking access rights (resource was deleted but attachment
+ # was not)
+ if res_model not in self.env:
require_employee = True
continue
- existing_ids = self.pool[model].exists(cr, uid, mids)
- if len(existing_ids) != len(mids):
+ records = self.env[res_model].browse(res_ids).exists()
+ if len(records) < len(res_ids):
require_employee = True
# For related models, check if we can write to the model, as unlinking
# and creating attachments can be seen as an update to the model
- if (mode in ['unlink','create']):
- ima.check(cr, uid, model, 'write')
- else:
- ima.check(cr, uid, model, mode)
- self.pool[model].check_access_rule(cr, uid, existing_ids, mode, context=context)
+ records.check_access_rights('write' if mode in ('create', 'unlink') else mode)
+ records.check_access_rule(mode)
+
if require_employee:
- if not uid == SUPERUSER_ID and not self.pool['res.users'].has_group(cr, uid, 'base.group_user'):
+ if not (self.env.user._is_admin() or self.env.user.has_group('base.group_user')):
raise AccessError(_("Sorry, you are not allowed to access this document."))
- def _search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False, access_rights_uid=None):
+ @api.model
+ def _search(self, args, offset=0, limit=None, order=None, count=False, access_rights_uid=None):
# add res_field=False in domain if not present; the arg[0] trick below
# works for domain items and '&'/'|'/'!' operators too
if not any(arg[0] in ('id', 'res_field') for arg in args):
args.insert(0, ('res_field', '=', False))
- ids = super(ir_attachment, self)._search(cr, uid, args, offset=offset,
- limit=limit, order=order,
- context=context, count=False,
- access_rights_uid=access_rights_uid)
+ ids = super(IrAttachment, self)._search(args, offset=offset, limit=limit, order=order,
+ count=False, access_rights_uid=access_rights_uid)
- if uid == SUPERUSER_ID:
+ if self._uid == SUPERUSER_ID:
# rules do not apply for the superuser
return len(ids) if count else ids
@@ -336,85 +302,82 @@ class ir_attachment(osv.osv):
# the linked document.
# Use pure SQL rather than read() as it is about 50% faster for large dbs (100k+ docs),
# and the permissions are checked in super() and below anyway.
- cr.execute("""SELECT id, res_model, res_id, public FROM ir_attachment WHERE id = ANY(%s)""", (list(ids),))
- targets = cr.dictfetchall()
- model_attachments = {}
- for target_dict in targets:
- if not target_dict['res_model'] or target_dict['public']:
+ model_attachments = defaultdict(lambda: defaultdict(set)) # {res_model: {res_id: set(ids)}}
+ self._cr.execute("""SELECT id, res_model, res_id, public FROM ir_attachment WHERE id IN %s""", [tuple(ids)])
+ for row in self._cr.dictfetchall():
+ if not row['res_model'] or row['public']:
continue
- # model_attachments = { 'model': { 'res_id': [id1,id2] } }
- model_attachments.setdefault(target_dict['res_model'],{}).setdefault(target_dict['res_id'] or 0, set()).add(target_dict['id'])
+ # model_attachments = {res_model: {res_id: set(ids)}}
+ model_attachments[row['res_model']][row['res_id']].add(row['id'])
# To avoid multiple queries for each attachment found, checks are
# performed in batch as much as possible.
- ima = self.pool.get('ir.model.access')
- for model, targets in model_attachments.iteritems():
- if model not in self.pool:
+ for res_model, targets in model_attachments.iteritems():
+ if res_model not in self.env:
continue
- if not ima.check(cr, uid, model, 'read', False):
+ if not self.env[res_model].check_access_rights('read', False):
# remove all corresponding attachment ids
- for attach_id in itertools.chain(*targets.values()):
- ids.remove(attach_id)
- continue # skip ir.rule processing, these ones are out already
-
+ ids.difference_update(itertools.chain(*targets.itervalues()))
+ continue
# filter ids according to what access rules permit
- target_ids = targets.keys()
- allowed_ids = [0] + self.pool[model].search(cr, uid, [('id', 'in', target_ids)], context=context)
- disallowed_ids = set(target_ids).difference(allowed_ids)
- for res_id in disallowed_ids:
- for attach_id in targets[res_id]:
- ids.remove(attach_id)
+ target_ids = list(targets)
+ allowed = self.env[res_model].search([('id', 'in', target_ids)])
+ for res_id in set(target_ids).difference(allowed.ids):
+ ids.difference_update(targets[res_id])
# sort result according to the original sort ordering
result = [id for id in orig_ids if id in ids]
return len(result) if count else list(result)
- def read(self, cr, uid, ids, fields_to_read=None, context=None, load='_classic_read'):
- if isinstance(ids, (int, long)):
- ids = [ids]
- self.check(cr, uid, ids, 'read', context=context)
- return super(ir_attachment, self).read(cr, uid, ids, fields_to_read, context=context, load=load)
+ @api.v7
+ def read(self, cr, uid, ids, fields=None, context=None, load='_classic_read'):
+ result = IrAttachment.read(self.browse(cr, uid, ids, context), fields, load=load)
+ return result if isinstance(ids, list) else (bool(result) and result[0])
+
+ @api.v8
+ def read(self, fields=None, load='_classic_read'):
+ self.check('read')
+ return super(IrAttachment, self).read(fields, load=load)
- def write(self, cr, uid, ids, vals, context=None):
- if isinstance(ids, (int, long)):
- ids = [ids]
- self.check(cr, uid, ids, 'write', context=context, values=vals)
+ @api.multi
+ def write(self, vals):
+ self.check('write', values=vals)
# remove computed field depending of datas
- for field in ['file_size', 'checksum']:
+ for field in ('file_size', 'checksum'):
vals.pop(field, False)
- return super(ir_attachment, self).write(cr, uid, ids, vals, context)
+ return super(IrAttachment, self).write(vals)
- def copy(self, cr, uid, id, default=None, context=None):
- self.check(cr, uid, [id], 'write', context=context)
- return super(ir_attachment, self).copy(cr, uid, id, default, context)
+ @api.multi
+ def copy(self, default=None):
+ self.check('write')
+ return super(IrAttachment, self).copy(default)
- def unlink(self, cr, uid, ids, context=None):
- if isinstance(ids, (int, long)):
- ids = [ids]
- self.check(cr, uid, ids, 'unlink', context=context)
+ @api.multi
+ def unlink(self):
+ self.check('unlink')
# First delete in the database, *then* in the filesystem if the
# database allowed it. Helps avoid errors when concurrent transactions
# are deleting the same file, and some of the transactions are
# rolled back by PostgreSQL (due to concurrent updates detection).
- to_delete = set([a.store_fname for a in self.browse(cr, uid, ids, context=context)
- if a.store_fname])
- res = super(ir_attachment, self).unlink(cr, uid, ids, context)
+ to_delete = set(attach.store_fname for attach in self if attach.store_fname)
+ res = super(IrAttachment, self).unlink()
for file_path in to_delete:
- self._file_delete(cr, uid, file_path)
+ self._file_delete(file_path)
return res
- def create(self, cr, uid, values, context=None):
+ @api.model
+ def create(self, values):
# remove computed field depending of datas
- for field in ['file_size', 'checksum']:
+ for field in ('file_size', 'checksum'):
values.pop(field, False)
# if mimetype not given, compute it !
if 'mimetype' not in values:
values['mimetype'] = self._compute_mimetype(values)
- self.check(cr, uid, [], mode='write', context=context, values=values)
- return super(ir_attachment, self).create(cr, uid, values, context)
+ self.browse().check('write', values=values)
+ return super(IrAttachment, self).create(values)
- def action_get(self, cr, uid, context=None):
- return self.pool.get('ir.actions.act_window').for_xml_id(
- cr, uid, 'base', 'action_attachment', context=context)
+ @api.model
+ def action_get(self):
+ return self.env['ir.actions.act_window'].for_xml_id('base', 'action_attachment')
diff --git a/openerp/addons/base/ir/ir_autovacuum.py b/openerp/addons/base/ir/ir_autovacuum.py
index f964e14b74d..6ff496c0e9f 100644
--- a/openerp/addons/base/ir/ir_autovacuum.py
+++ b/openerp/addons/base/ir/ir_autovacuum.py
@@ -8,25 +8,27 @@ from odoo import api, models
_logger = logging.getLogger(__name__)
-class AutoVacuum(models.TransientModel):
+class AutoVacuum(models.AbstractModel):
""" Expose the vacuum method to the cron jobs mechanism. """
_name = 'ir.autovacuum'
@api.model
def _gc_transient_models(self):
- for model in self.pool.itervalues():
+ for mname in self.env:
+ model = self.env[mname]
if model.is_transient():
- model._transient_vacuum(self._cr, self._uid, force=True)
+ model._transient_vacuum(force=True)
+ @api.model
def _gc_user_logs(self):
- self.env.cr.execute("""
+ self._cr.execute("""
DELETE FROM res_users_log log1 WHERE EXISTS (
SELECT 1 FROM res_users_log log2
WHERE log1.create_uid = log2.create_uid
AND log1.create_date < log2.create_date
)
""")
- _logger.info("GC'd %d user log entries", self.env.cr.rowcount)
+ _logger.info("GC'd %d user log entries", self._cr.rowcount)
@api.model
def power_on(self):
diff --git a/openerp/addons/base/ir/ir_config_parameter.py b/openerp/addons/base/ir/ir_config_parameter.py
index f509156b084..4d6ae552cbd 100644
--- a/openerp/addons/base/ir/ir_config_parameter.py
+++ b/openerp/addons/base/ir/ir_config_parameter.py
@@ -16,14 +16,13 @@ A dictionary holding some configuration parameters to be initialized when the da
_default_parameters = {
"database.secret": lambda: (str(uuid.uuid4()), ['base.group_erp_manager']),
"database.uuid": lambda: (str(uuid.uuid1()), []),
- "database.create_date": lambda: (datetime.datetime.now().strftime(misc.DEFAULT_SERVER_DATETIME_FORMAT), ['base.group_user']),
+ "database.create_date": lambda: (fields.Datetime.now(), ['base.group_user']),
"web.base.url": lambda: ("http://localhost:%s" % config.get('xmlrpc_port'), []),
}
class IrConfigParameter(models.Model):
"""Per-database storage of configuration key-value pairs."""
-
_name = 'ir.config_parameter'
_rec_name = 'key'
@@ -35,6 +34,7 @@ class IrConfigParameter(models.Model):
('key_uniq', 'unique (key)', 'Key must be unique.')
]
+ @api.model_cr
def init(self, force=False):
"""
Initializes the parameters listed in _default_parameters.
@@ -56,17 +56,13 @@ class IrConfigParameter(models.Model):
:return: The value of the parameter, or ``default`` if it does not exist.
:rtype: string
"""
- result = self._get_param(key)
- if result:
- return result
- return default
-
- @ormcache('uid', 'key')
- def _get_param(self, cr, uid, key):
- params = self.search_read(cr, uid, [('key', '=', key)], fields=['value'], limit=1)
- if not params:
- return None
- return params[0]['value']
+ return self._get_param(key) or default
+
+ @api.model
+ @ormcache('self._uid', 'key')
+ def _get_param(self, key):
+ params = self.search_read([('key', '=', key)], fields=['value'], limit=1)
+ return params[0]['value'] if params else None
@api.model
def set_param(self, key, value, groups=()):
@@ -80,23 +76,24 @@ class IrConfigParameter(models.Model):
:rtype: string
"""
self._get_param.clear_cache(self)
- self = self.search([('key', '=', key)])
+ param = self.search([('key', '=', key)])
gids = []
for group_xml in groups:
- res_id = self.env['ir.model.data'].xmlid_to_res_id(group_xml)
- if res_id:
- gids.append((4, res_id))
+ group = self.env.ref(group_xml, raise_if_not_found=False)
+ if group:
+ gids.append((4, group.id))
vals = {'value': value}
if gids:
vals.update(group_ids=gids)
- if self.ids:
+ if param:
+ old = param.value
if value is not False and value is not None:
- self.write(vals)
+ param.write(vals)
else:
- self.unlink()
- return self.value
+ param.unlink()
+ return old
else:
vals.update(key=key)
if value is not False and value is not None:
@@ -105,10 +102,10 @@ class IrConfigParameter(models.Model):
@api.multi
def write(self, vals):
- self._get_param.clear_cache(self)
+ self.clear_caches()
return super(IrConfigParameter, self).write(vals)
@api.multi
def unlink(self):
- self._get_param.clear_cache(self)
+ self.clear_caches()
return super(IrConfigParameter, self).unlink()
diff --git a/openerp/addons/base/ir/ir_config_parameter_view.xml b/openerp/addons/base/ir/ir_config_parameter_view.xml
index dbe5eae2a91..555c7b94b1d 100644
--- a/openerp/addons/base/ir/ir_config_parameter_view.xml
+++ b/openerp/addons/base/ir/ir_config_parameter_view.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
-<openerp>
+<odoo>
<record model="ir.ui.view" id="view_ir_config_search">
<field name="model">ir.config_parameter</field>
<field name="arch" type="xml">
@@ -35,4 +35,4 @@
</record>
<act_window name="System Parameters" res_model="ir.config_parameter" id="ir_config_list_action"/>
<menuitem id="ir_config_menu" name="System Parameters" parent="menu_ir_property" action="ir_config_list_action"/>
-</openerp>
+</odoo>
diff --git a/openerp/addons/base/ir/ir_cron.py b/openerp/addons/base/ir/ir_cron.py
index b2a2f5b45d1..173c3128efe 100644
--- a/openerp/addons/base/ir/ir_cron.py
+++ b/openerp/addons/base/ir/ir_cron.py
@@ -10,8 +10,7 @@ from dateutil.relativedelta import relativedelta
import odoo
from odoo import api, fields, models, _
-from odoo.exceptions import UserError
-from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT
+from odoo.exceptions import UserError, ValidationError
from odoo.tools.safe_eval import safe_eval as eval
_logger = logging.getLogger(__name__)
@@ -56,7 +55,7 @@ class ir_cron(models.Model):
('months', 'Months')], string='Interval Unit', default='months')
numbercall = fields.Integer(string='Number of Calls', default=1, help='How many times the method is called,\na negative number indicates no limit.')
doall = fields.Boolean(string='Repeat Missed', help="Specify if missed occurrences should be executed when the server restarts.")
- nextcall = fields.Datetime(string='Next Execution Date', required=True, default=lambda *a: time.strftime(DEFAULT_SERVER_DATETIME_FORMAT), help="Next planned execution date for this job.")
+ nextcall = fields.Datetime(string='Next Execution Date', required=True, default=fields.Datetime.now, help="Next planned execution date for this job.")
model = fields.Char(string='Object', help="Model name on which the method to be called is located, e.g. 'res.partner'.")
function = fields.Char(string='Method', help="Name of the method to be called when this job is processed.")
args = fields.Text(string='Arguments', help="Arguments to be passed to the method, e.g. (uid,).")
@@ -68,18 +67,16 @@ class ir_cron(models.Model):
for this in self:
str2tuple(this.args)
except Exception:
- raise Exception('Invalid arguments')
- return True
+ raise ValidationError(_('Invalid arguments'))
- def method_direct_trigger(self, cr, uid, ids, context=None):
- if context is None:
- context = {}
- cron_obj = self.browse(cr, uid, ids, context=context)
- for cron in cron_obj:
- self._callback(cr, uid, cron_obj.model, cron_obj.function, cron_obj.args, cron_obj.id)
+ @api.multi
+ def method_direct_trigger(self):
+ for cron in self:
+ self._callback(cron.model, cron.function, cron.args, cron.id)
return True
- def _handle_callback_exception(self, cr, uid, model_name, method_name, args, job_id, job_exception):
+ @api.model
+ def _handle_callback_exception(self, model_name, method_name, args, job_id, job_exception):
""" Method called when an exception is raised by a job.
Simply logs the exception and rollback the transaction.
@@ -91,10 +88,12 @@ class ir_cron(models.Model):
:param job_exception: exception raised by the job.
"""
- cr.rollback()
- _logger.exception("Call of self.pool.get('%s').%s(cr, uid, *%r) failed in Job %s" % (model_name, method_name, args, job_id))
+ self._cr.rollback()
+ _logger.exception("Call of self.env[%r].%s(*%r) failed in Job %s",
+ model_name, method_name, args, job_id)
- def _callback(self, cr, uid, model_name, method_name, args, job_id):
+ @api.model
+ def _callback(self, model_name, method_name, args, job_id):
""" Run the method associated to a given job
It takes care of logging and exception handling.
@@ -106,28 +105,25 @@ class ir_cron(models.Model):
"""
try:
args = str2tuple(args)
- odoo.modules.registry.RegistryManager.check_registry_signaling(cr.dbname)
- registry = odoo.registry(cr.dbname)
- if model_name in registry:
- model = registry[model_name]
+ odoo.modules.registry.RegistryManager.check_registry_signaling(self._cr.dbname)
+ if model_name in self.env:
+ model = self.env[model_name]
if hasattr(model, method_name):
log_depth = (None if _logger.isEnabledFor(logging.DEBUG) else 1)
- odoo.netsvc.log(_logger, logging.DEBUG, 'cron.object.execute', (cr.dbname, uid, '*', model_name, method_name)+tuple(args), depth=log_depth)
+ odoo.netsvc.log(_logger, logging.DEBUG, 'cron.object.execute', (self._cr.dbname, self._uid, '*', model_name, method_name)+tuple(args), depth=log_depth)
if _logger.isEnabledFor(logging.DEBUG):
start_time = time.time()
- getattr(model, method_name)(cr, uid, *args)
+ getattr(model, method_name)(*args)
if _logger.isEnabledFor(logging.DEBUG):
end_time = time.time()
- _logger.debug('%.3fs (%s, %s)' % (end_time - start_time, model_name, method_name))
- odoo.modules.registry.RegistryManager.signal_caches_change(cr.dbname)
+ _logger.debug('%.3fs (%s, %s)', end_time - start_time, model_name, method_name)
+ odoo.modules.registry.RegistryManager.signal_caches_change(self._cr.dbname)
else:
- msg = "Method `%s.%s` does not exist." % (model_name, method_name)
- _logger.warning(msg)
+ _logger.warning("Method '%s.%s' does not exist.", model_name, method_name)
else:
- msg = "Model `%s` does not exist." % model_name
- _logger.warning(msg)
+ _logger.warning("Model %r does not exist.", model_name)
except Exception, e:
- self._handle_callback_exception(cr, uid, model_name, method_name, args, job_id, e)
+ self._handle_callback_exception(model_name, method_name, args, job_id, e)
def _process_job(self, job_cr, job, cron_cr):
""" Run a given job taking care of the repetition.
@@ -139,8 +135,13 @@ class ir_cron(models.Model):
"""
try:
with api.Environment.manage():
- now = odoo.osv.fields.datetime.context_timestamp(job_cr, job['user_id'], datetime.now())
- nextcall = odoo.osv.fields.datetime.context_timestamp(job_cr, job['user_id'], datetime.strptime(job['nextcall'], DEFAULT_SERVER_DATETIME_FORMAT))
+ cron = api.Environment(job_cr, job['user_id'], {})[self._name]
+ # Use the user's timezone to compare and compute datetimes,
+ # otherwise unexpected results may appear. For instance, adding
+ # 1 month in UTC to July 1st at midnight in GMT+2 gives July 30
+ # instead of August 1st!
+ now = fields.Datetime.context_timestamp(cron, datetime.now())
+ nextcall = fields.Datetime.context_timestamp(cron, fields.Datetime.from_string(job['nextcall']))
numbercall = job['numbercall']
ok = False
@@ -148,7 +149,7 @@ class ir_cron(models.Model):
if numbercall > 0:
numbercall -= 1
if not ok or job['doall']:
- self._callback(job_cr, job['user_id'], job['model'], job['function'], job['args'], job['id'])
+ cron._callback(job['model'], job['function'], job['args'], job['id'])
if numbercall:
nextcall += _intervalTypes[job['interval_type']](job['interval_number'])
ok = True
@@ -156,8 +157,8 @@ class ir_cron(models.Model):
if not numbercall:
addsql = ', active=False'
cron_cr.execute("UPDATE ir_cron SET nextcall=%s, numbercall=%s"+addsql+" WHERE id=%s",
- (nextcall.astimezone(pytz.UTC).strftime(DEFAULT_SERVER_DATETIME_FORMAT), numbercall, job['id']))
- self.invalidate_cache(job_cr, odoo.SUPERUSER_ID)
+ (fields.Datetime.to_string(nextcall.astimezone(pytz.UTC)), numbercall, job['id']))
+ cron.invalidate_cache()
finally:
job_cr.commit()
@@ -248,23 +249,20 @@ class ir_cron(models.Model):
if hasattr(threading.current_thread(), 'dbname'): # cron job could have removed it as side-effect
del threading.current_thread().dbname
+ @api.multi
def _try_lock(self):
"""Try to grab a dummy exclusive write-lock to the rows with the given ids,
to make sure a following write() or unlink() will not block due
to a process currently executing those cron tasks"""
try:
- self.env.cr.execute("""SELECT id FROM "%s" WHERE id IN %%s FOR UPDATE NOWAIT""" % self._table,
- (tuple(self.ids),), log_exceptions=False)
+ self._cr.execute("""SELECT id FROM "%s" WHERE id IN %%s FOR UPDATE NOWAIT""" % self._table,
+ [tuple(self.ids)], log_exceptions=False)
except psycopg2.OperationalError:
- self.env.cr.rollback() # early rollback to allow translations to work for the user feedback
+ self._cr.rollback() # early rollback to allow translations to work for the user feedback
raise UserError(_("Record cannot be modified right now: "
"This cron task is currently being executed and may not be modified "
"Please try again in a few minutes"))
- @api.model
- def create(self, vals):
- return super(ir_cron, self).create(vals)
-
@api.multi
def write(self, vals):
self._try_lock()
@@ -278,9 +276,9 @@ class ir_cron(models.Model):
@api.multi
def try_write(self, values):
try:
- with self.env.cr.savepoint():
- self.env.cr.execute("""SELECT id FROM "%s" WHERE id IN %%s FOR UPDATE NOWAIT""" % self._table,
- (tuple(self.ids),), log_exceptions=False)
+ with self._cr.savepoint():
+ self._cr.execute("""SELECT id FROM "%s" WHERE id IN %%s FOR UPDATE NOWAIT""" % self._table,
+ [tuple(self.ids)], log_exceptions=False)
except psycopg2.OperationalError:
pass
else:
diff --git a/openerp/addons/base/ir/ir_exports.py b/openerp/addons/base/ir/ir_exports.py
index 107acd6c83d..9b89d2e6f9a 100644
--- a/openerp/addons/base/ir/ir_exports.py
+++ b/openerp/addons/base/ir/ir_exports.py
@@ -9,7 +9,7 @@ class IrExports(models.Model):
_order = 'name'
name = fields.Char(string='Export Name')
- resource = fields.Char(select=True)
+ resource = fields.Char(index=True)
export_fields = fields.One2many('ir.exports.line', 'export_id', string='Export ID', copy=True)
@@ -18,4 +18,4 @@ class IrExportsLine(models.Model):
_order = 'id'
name = fields.Char(string='Field Name')
- export_id = fields.Many2one('ir.exports', string='Export', select=True, ondelete='cascade')
+ export_id = fields.Many2one('ir.exports', string='Export', index=True, ondelete='cascade')
diff --git a/openerp/addons/base/ir/ir_fields.py b/openerp/addons/base/ir/ir_fields.py
index 92b85b523e8..f2296e3c12a 100644
--- a/openerp/addons/base/ir/ir_fields.py
+++ b/openerp/addons/base/ir/ir_fields.py
@@ -3,21 +3,18 @@
import functools
import itertools
-import time
import psycopg2
import pytz
from odoo import api, fields, models, _
-from odoo.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT, ustr
+from odoo.tools import ustr
REFERENCING_FIELDS = {None, 'id', '.id'}
def only_ref_fields(record):
- return {k: v for k, v in record.iteritems()
- if k in REFERENCING_FIELDS}
+ return {k: v for k, v in record.iteritems() if k in REFERENCING_FIELDS}
def exclude_ref_fields(record):
- return {k: v for k, v in record.iteritems()
- if k not in REFERENCING_FIELDS}
+ return {k: v for k, v in record.iteritems() if k not in REFERENCING_FIELDS}
CREATE = lambda values: (0, False, values)
UPDATE = lambda id, values: (1, id, values)
@@ -31,7 +28,8 @@ class ImportWarning(Warning):
""" Used to send warnings upwards the stack during the import process """
pass
-class ConversionNotFound(ValueError): pass
+class ConversionNotFound(ValueError):
+ pass
class IrFieldsConverter(models.AbstractModel):
@@ -201,8 +199,8 @@ class IrFieldsConverter(models.AbstractModel):
@api.model
def _str_to_date(self, model, field, value):
try:
- time.strptime(value, DEFAULT_SERVER_DATE_FORMAT)
- return value, []
+ parsed_value = fields.Date.from_string(value)
+ return fields.Date.to_string(parsed_value), []
except ValueError:
raise self._format_import_error(
ValueError,
@@ -246,7 +244,7 @@ class IrFieldsConverter(models.AbstractModel):
input_tz = self._input_tz()# Apply input tz to the parsed naive datetime
dt = input_tz.localize(parsed_value, is_dst=False)
# And convert to UTC before reformatting for writing
- return dt.astimezone(pytz.UTC).strftime(DEFAULT_SERVER_DATETIME_FORMAT), []
+ return fields.Datetime.to_string(dt.astimezone(pytz.UTC)), []
@api.model
def _get_translations(self, types, src):
diff --git a/openerp/addons/base/ir/ir_filters.py b/openerp/addons/base/ir/ir_filters.py
index db33fef071f..70ffb48563b 100644
--- a/openerp/addons/base/ir/ir_filters.py
+++ b/openerp/addons/base/ir/ir_filters.py
@@ -1,8 +1,8 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-from odoo import exceptions
from odoo import api, fields, models, _
+from odoo.exceptions import UserError
class IrFilters(models.Model):
@@ -10,10 +10,6 @@ class IrFilters(models.Model):
_description = 'Filters'
_order = 'model_id, name, id desc'
- def _list_all_models(self):
- self.env.cr.execute("SELECT model, name FROM ir_model ORDER BY name")
- return self.env.cr.fetchall()
-
name = fields.Char(string='Filter Name', translate=True, required=True)
user_id = fields.Many2one('res.users', string='User', ondelete='cascade', default=lambda self: self._uid,
help="The user this filter is private to. When left empty the filter is public "
@@ -29,12 +25,18 @@ class IrFilters(models.Model):
"for this model.")
active = fields.Boolean(default=True)
+ @api.model
+ def _list_all_models(self):
+ self._cr.execute("SELECT model, name FROM ir_model ORDER BY name")
+ return self._cr.fetchall()
+
@api.multi
def copy(self, default=None):
self.ensure_one()
- default.update({'name': _('%s (copy)') % self.name})
+ default = dict(default or {}, name=_('%s (copy)') % self.name)
return super(IrFilters, self).copy(default)
+ @api.model
def _get_action_domain(self, action_id=None):
"""Return a domain component for matching filters that are visible in the
same context (menu/view) as the given action."""
@@ -59,12 +61,13 @@ class IrFilters(models.Model):
# available filters: private filters (user_id=uid) and public filters (uid=NULL),
# and filters for the action (action_id=action_id) or global (action_id=NULL)
action_domain = self._get_action_domain(action_id)
- filter_ids = self.search(action_domain + [('model_id', '=', model), ('user_id', 'in', [self._uid, False])])
- my_filters = filter_ids.with_context(self.env.user.context_get()).read(['name', 'is_default', 'domain', 'context', 'user_id', 'sort'])
- return my_filters
+ filters = self.search(action_domain + [('model_id', '=', model), ('user_id', 'in', [self._uid, False])])
+ user_context = self.env.user.context_get()
+ return filters.with_context(user_context).read(['name', 'is_default', 'domain', 'context', 'user_id', 'sort'])
+ @api.model
def _check_global_default(self, vals, matching_filters):
- """ _check_global_default(cursor, UID, dict, list(dict), dict) -> None
+ """ _check_global_default(dict, list(dict), dict) -> None
Checks if there is a global default for the model_id requested.
@@ -76,21 +79,22 @@ class IrFilters(models.Model):
This method should only be called if ``vals`` is trying to set
``is_default``
- :raises openerp.exceptions.Warning: if there is an existing default and
+ :raises openerp.exceptions.UserError: if there is an existing default and
we're not updating it
"""
- action_domain = self._get_action_domain(vals.get('action_id'))
- existing_default = self.search(action_domain + [
+ domain = self._get_action_domain(vals.get('action_id'))
+ defaults = self.search(domain + [
('model_id', '=', vals['model_id']),
('user_id', '=', False),
- ('is_default', '=', True)])
+ ('is_default', '=', True),
+ ])
- if not existing_default:
+ if not defaults:
return
- if matching_filters and (matching_filters[0]['id'] == existing_default.id):
+ if matching_filters and (matching_filters[0]['id'] == defaults.id):
return
- raise exceptions.Warning(_("There is already a shared filter set as default for %(model)s, delete or change it before setting a new default") % {'model': vals.get('model_id')})
+ raise UserError(_("There is already a shared filter set as default for %(model)s, delete or change it before setting a new default") % {'model': vals.get('model_id')})
@api.model
def create_or_replace(self, vals):
@@ -107,14 +111,14 @@ class IrFilters(models.Model):
if vals.get('user_id'):
# Setting new default: any other default that belongs to the user
# should be turned off
- action_domain = self._get_action_domain(action_id)
- act_ids = self.search(action_domain + [
- ('model_id', '=', vals['model_id']),
- ('user_id', '=', vals['user_id']),
- ('is_default', '=', True),
+ domain = self._get_action_domain(action_id)
+ defaults = self.search(domain + [
+ ('model_id', '=', vals['model_id']),
+ ('user_id', '=', vals['user_id']),
+ ('is_default', '=', True),
])
- if act_ids:
- act_ids.write({'is_default': False})
+ if defaults:
+ defaults.write({'is_default': False})
else:
self._check_global_default(vals, matching_filters)
@@ -127,18 +131,19 @@ class IrFilters(models.Model):
return self.create(vals)
_sql_constraints = [
- # Partial constraint, complemented by unique index (see below)
- # Still useful to keep because it provides a proper error message when a violation
- # occurs, as it shares the same prefix as the unique index.
+ # Partial constraint, complemented by unique index (see below). Still
+ # useful to keep because it provides a proper error message when a
+ # violation occurs, as it shares the same prefix as the unique index.
('name_model_uid_unique', 'unique (name, model_id, user_id, action_id)', 'Filter names must be unique'),
]
+ @api.model_cr_context
def _auto_init(self):
result = super(IrFilters, self)._auto_init()
# Use unique index to implement unique constraint on the lowercase name (not possible using a constraint)
- self.env.cr.execute("DROP INDEX IF EXISTS ir_filters_name_model_uid_unique_index") # drop old index w/o action
- self.env.cr.execute("SELECT indexname FROM pg_indexes WHERE indexname = 'ir_filters_name_model_uid_unique_action_index'")
- if not self.env.cr.fetchone():
- self.env.cr.execute("""CREATE UNIQUE INDEX "ir_filters_name_model_uid_unique_action_index" ON ir_filters
- (lower(name), model_id, COALESCE(user_id,-1), COALESCE(action_id,-1))""")
+ self._cr.execute("DROP INDEX IF EXISTS ir_filters_name_model_uid_unique_index") # drop old index w/o action
+ self._cr.execute("SELECT indexname FROM pg_indexes WHERE indexname = 'ir_filters_name_model_uid_unique_action_index'")
+ if not self._cr.fetchone():
+ self._cr.execute("""CREATE UNIQUE INDEX "ir_filters_name_model_uid_unique_action_index" ON ir_filters
+ (lower(name), model_id, COALESCE(user_id,-1), COALESCE(action_id,-1))""")
return result
diff --git a/openerp/addons/base/ir/ir_http.py b/openerp/addons/base/ir/ir_http.py
index 2c3dd1b9b13..84400b84c68 100644
--- a/openerp/addons/base/ir/ir_http.py
+++ b/openerp/addons/base/ir/ir_http.py
@@ -19,8 +19,8 @@ import werkzeug.urls
import werkzeug.utils
import odoo
-from odoo import models, tools
-from odoo import http
+from odoo import api, http, models, tools, SUPERUSER_ID
+from odoo.exceptions import AccessDenied, AccessError
from odoo.http import request, STATIC_CACHE
from odoo.modules.module import get_resource_path, get_module_path
@@ -34,12 +34,11 @@ class ModelConverter(werkzeug.routing.BaseConverter):
def __init__(self, url_map, model=False):
super(ModelConverter, self).__init__(url_map)
self.model = model
- self.regex = '([0-9]+)'
+ self.regex = r'([0-9]+)'
def to_python(self, value):
- m = re.match(self.regex, value)
- return request.registry[self.model].browse(
- request.cr, UID_PLACEHOLDER, int(m.group(1)), context=request.context)
+ env = api.Environment(request.cr, UID_PLACEHOLDER, request.context)
+ return env[self.model].browse(int(value))
def to_url(self, value):
return value.id
@@ -51,13 +50,14 @@ class ModelsConverter(werkzeug.routing.BaseConverter):
super(ModelsConverter, self).__init__(url_map)
self.model = model
# TODO add support for slug in the form [A-Za-z0-9-] bla-bla-89 -> id 89
- self.regex = '([0-9,]+)'
+ self.regex = r'([0-9,]+)'
def to_python(self, value):
- return request.registry[self.model].browse(request.cr, UID_PLACEHOLDER, [int(i) for i in value.split(',')], context=request.context)
+ env = api.Environment(request.cr, UID_PLACEHOLDER, request.context)
+ return env[self.model].browse(map(int, value.split(',')))
def to_url(self, value):
- return ",".join(i.id for i in value)
+ return ",".join(value.ids)
class SignedIntConverter(werkzeug.routing.NumberConverter):
@@ -85,7 +85,7 @@ class IrHttp(models.AbstractModel):
def _auth_method_public(self):
if not request.session.uid:
- dummy, request.uid = self.env['ir.model.data'].sudo().get_object_reference(request.cr, 'base', 'public_user')
+ request.uid = request.env.ref('base.public_user').id
else:
request.uid = request.session.uid
@@ -97,22 +97,24 @@ class IrHttp(models.AbstractModel):
# what if error in security.check()
# -> res_users.check()
# -> res_users.check_credentials()
- except (odoo.exceptions.AccessDenied, http.SessionExpiredException):
+ except (AccessDenied, http.SessionExpiredException):
# All other exceptions mean undetermined status (e.g. connection pool full),
# let them bubble up
request.session.logout(keep_db=True)
if request.uid is None:
getattr(self, "_auth_method_%s" % auth_method)()
- except (odoo.exceptions.AccessDenied, http.SessionExpiredException, werkzeug.exceptions.HTTPException):
+ except (AccessDenied, http.SessionExpiredException, werkzeug.exceptions.HTTPException):
raise
except Exception:
_logger.info("Exception during request Authentication.", exc_info=True)
- raise odoo.exceptions.AccessDenied()
+ raise AccessDenied()
return auth_method
def _serve_attachment(self):
+ env = api.Environment(request.cr, SUPERUSER_ID, request.context)
domain = [('type', '=', 'binary'), ('url', '=', request.httprequest.path)]
- attach = self.pool['ir.attachment'].search_read(request.cr, odoo.SUPERUSER_ID, domain, ['__last_update', 'datas', 'name', 'mimetype', 'checksum'], context=request.context)
+ fields = ['__last_update', 'datas', 'name', 'mimetype', 'checksum']
+ attach = env['ir.attachment'].search_read(domain, fields)
if attach:
wdate = attach[0]['__last_update']
datas = attach[0]['datas'] or ''
@@ -124,7 +126,7 @@ class IrHttp(models.AbstractModel):
return werkzeug.utils.redirect(name, 301)
response = werkzeug.wrappers.Response()
- server_format = tools.misc.DEFAULT_SERVER_DATETIME_FORMAT
+ server_format = tools.DEFAULT_SERVER_DATETIME_FORMAT
try:
response.last_modified = datetime.datetime.strptime(wdate, server_format + '.%f')
except ValueError:
@@ -156,7 +158,7 @@ class IrHttp(models.AbstractModel):
raise
try:
return request._handle_exception(exception)
- except odoo.exceptions.AccessDenied:
+ except AccessDenied:
return werkzeug.exceptions.Forbidden()
def _dispatch(self):
@@ -191,11 +193,9 @@ class IrHttp(models.AbstractModel):
def _postprocess_args(self, arguments, rule):
""" post process arg to set uid on browse records """
for name, arg in arguments.items():
- if isinstance(arg, odoo.osv.orm.browse_record) and arg._uid is UID_PLACEHOLDER:
+ if isinstance(arg, models.BaseModel) and arg._uid is UID_PLACEHOLDER:
arguments[name] = arg.sudo(request.uid)
- try:
- arg.exists()
- except models.MissingError:
+ if not arg.exists():
return self._handle_exception(werkzeug.exceptions.NotFound())
def routing_map(self):
@@ -256,7 +256,7 @@ class IrHttp(models.AbstractModel):
# check read access
try:
last_update = obj['__last_update']
- except odoo.exceptions.AccessError:
+ except AccessError:
return (403, [], None)
status, headers, content = None, [], None
@@ -341,5 +341,5 @@ def convert_exception_to(to_type, with_message=False):
message = str(with_message)
raise to_type, message, tb
- except to_type, e:
+ except to_type as e:
return e
diff --git a/openerp/addons/base/ir/ir_logging.py b/openerp/addons/base/ir/ir_logging.py
index d6833e27c88..0397bff7425 100644
--- a/openerp/addons/base/ir/ir_logging.py
+++ b/openerp/addons/base/ir/ir_logging.py
@@ -9,9 +9,9 @@ class IrLogging(models.Model):
create_date = fields.Datetime(readonly=True)
create_uid = fields.Integer(string='Uid', readonly=True) # Integer not m2o is intentionnal
name = fields.Char(required=True)
- type = fields.Selection([('client', 'Client'), ('server', 'Server')], required=True, select=True)
- dbname = fields.Char(string='Database Name', select=True)
- level = fields.Char(select=True)
+ type = fields.Selection([('client', 'Client'), ('server', 'Server')], required=True, index=True)
+ dbname = fields.Char(string='Database Name', index=True)
+ level = fields.Char(index=True)
message = fields.Text(required=True)
path = fields.Char(required=True)
func = fields.Char(string='Function', required=True)
diff --git a/openerp/addons/base/ir/ir_mail_server.py b/openerp/addons/base/ir/ir_mail_server.py
index ad4c4326712..726fa3a968c 100644
--- a/openerp/addons/base/ir/ir_mail_server.py
+++ b/openerp/addons/base/ir/ir_mail_server.py
@@ -14,13 +14,8 @@ import smtplib
import threading
from odoo import api, fields, models, tools, _
-from odoo.exceptions import except_orm
-from odoo.exceptions import UserError
-from odoo.tools import html2text
-
-# ustr was originally from tools.misc.
-# it is moved to loglevels until we refactor tools.
-from odoo.loglevels import ustr
+from odoo.exceptions import except_orm, UserError
+from odoo.tools import html2text, ustr
_logger = logging.getLogger(__name__)
_test_logger = logging.getLogger('openerp.tests')
@@ -75,14 +70,13 @@ def encode_header(header_text):
return ""
# convert anything to utf-8, suitable for testing ASCIIness, as 7-bit chars are
# encoded as ASCII in utf-8
- header_text_utf8 = tools.ustr(header_text).encode('utf-8')
+ header_text_utf8 = ustr(header_text).encode('utf-8')
header_text_ascii = try_coerce_ascii(header_text_utf8)
# if this header contains non-ASCII characters,
# we'll need to wrap it up in a message.header.Header
# that will take care of RFC2047-encoding it as
# 7-bit string.
- return header_text_ascii if header_text_ascii\
- else Header(header_text_utf8, 'utf-8')
+ return header_text_ascii or Header(header_text_utf8, 'utf-8')
def encode_header_param(param_text):
@@ -101,15 +95,12 @@ def encode_header_param(param_text):
# For details see the encode_header() method that uses the same logic
if not param_text:
return ""
- param_text_utf8 = tools.ustr(param_text).encode('utf-8')
+ param_text_utf8 = ustr(param_text).encode('utf-8')
param_text_ascii = try_coerce_ascii(param_text_utf8)
- return param_text_ascii if param_text_ascii\
- else Charset('utf8').header_encode(param_text_utf8)
+ return param_text_ascii or Charset('utf8').header_encode(param_text_utf8)
-# TODO master, remove me, no longer used internaly
-name_with_email_pattern = re.compile(r'("[^<@>]+")\s*<([^ ,<@]+@[^> ,]+)>')
-address_pattern = re.compile(r'([^ ,<@]+@[^> ,]+)')
+address_pattern = re.compile(r'([^ ,<@]+@[^> ,]+)')
def extract_rfc2822_addresses(text):
"""Returns a list of valid RFC2822 addresses
@@ -118,7 +109,7 @@ def extract_rfc2822_addresses(text):
"""
if not text:
return []
- candidates = address_pattern.findall(tools.ustr(text).encode('utf-8'))
+ candidates = address_pattern.findall(ustr(text).encode('utf-8'))
return filter(try_coerce_ascii, candidates)
@@ -135,7 +126,7 @@ def encode_rfc2822_address_header(header_text):
name = str(Header(name, 'utf-8'))
return formataddr((name, email))
- addresses = getaddresses([tools.ustr(header_text).encode('utf-8')])
+ addresses = getaddresses([ustr(header_text).encode('utf-8')])
return COMMASPACE.join(map(encode_addr, addresses))
@@ -176,17 +167,18 @@ class IrMailServer(models.Model):
@api.multi
def name_get(self):
- return [(a.id, "(%s)" % (a.name)) for a in self]
+ return [(server.id, "(%s)" % server.name) for server in self]
+ @api.multi
def test_smtp_connection(self):
- for smtp_server in self:
+ for server in self:
smtp = False
try:
- smtp = self.connect(smtp_server.smtp_host, smtp_server.smtp_port, user=smtp_server.smtp_user,
- password=smtp_server.smtp_pass, encryption=smtp_server.smtp_encryption,
- smtp_debug=smtp_server.smtp_debug)
- except Exception, e:
- raise UserError(_("Connection Test Failed! Here is what we got instead:\n %s") % tools.ustr(e))
+ smtp = self.connect(server.smtp_host, server.smtp_port, user=server.smtp_user,
+ password=server.smtp_pass, encryption=server.smtp_encryption,
+ smtp_debug=server.smtp_debug)
+ except Exception as e:
+ raise UserError(_("Connection Test Failed! Here is what we got instead:\n %s") % ustr(e))
finally:
try:
if smtp:
@@ -231,8 +223,8 @@ class IrMailServer(models.Model):
# The user/password must be converted to bytestrings in order to be usable for
# certain hashing schemes, like HMAC.
# See also bug #597143 and python issue #5285
- user = tools.ustr(user).encode('utf-8')
- password = tools.ustr(password).encode('utf-8')
+ user = ustr(user).encode('utf-8')
+ password = ustr(password).encode('utf-8')
connection.login(user, password)
return connection
@@ -275,11 +267,10 @@ class IrMailServer(models.Model):
# Note: we must force all strings to to 8-bit utf-8 when crafting message,
# or use encode_header() for headers, which does it automatically.
- headers = headers or {} # need valid dict later
-
- if not email_cc: email_cc = []
- if not email_bcc: email_bcc = []
- if not body: body = u''
+ headers = headers or {} # need valid dict later
+ email_cc = email_cc or []
+ email_bcc = email_bcc or []
+ body = body or u''
email_body_utf8 = ustr(body).encode('utf-8')
email_text_part = MIMEText(email_body_utf8, _subtype=subtype, _charset='utf-8')
@@ -343,6 +334,7 @@ class IrMailServer(models.Model):
msg.attach(part)
return msg
+ @api.model
def _get_default_bounce_address(self):
'''Compute the default bounce address.
@@ -362,6 +354,7 @@ class IrMailServer(models.Model):
if postmaster and domain:
return '%s@%s' % (postmaster, domain)
+ @api.model
def send_email(self, message, mail_server_id=None, smtp_server=None, smtp_port=None,
smtp_user=None, smtp_password=None, smtp_encryption=None, smtp_debug=False):
"""Sends an email directly (no queuing).
@@ -392,11 +385,7 @@ class IrMailServer(models.Model):
# Use the default bounce address **only if** no Return-Path was
# provided by caller. Caller may be using Variable Envelope Return
# Path (VERP) to detect no-longer valid email addresses.
- smtp_from = message['Return-Path']
- if not smtp_from:
- smtp_from = self._get_default_bounce_address()
- if not smtp_from:
- smtp_from = message['From']
+ smtp_from = message['Return-Path'] or self._get_default_bounce_address() or message['From']
assert smtp_from, "The Return-Path or From header is required for any outbound email"
# The email's "Envelope From" (Return-Path), and all recipient addresses must only contain ASCII characters.
@@ -416,7 +405,7 @@ class IrMailServer(models.Model):
if x_forge_to:
# `To:` header forged, e.g. for posting on mail.channels, to avoid confusion
del message['X-Forge-To']
- del message['To'] # avoid multiple To: headers!
+ del message['To'] # avoid multiple To: headers!
message['To'] = x_forge_to
# Do not actually send emails in testing mode!
@@ -445,7 +434,7 @@ class IrMailServer(models.Model):
smtp_user = smtp_user or tools.config.get('smtp_user')
smtp_password = smtp_password or tools.config.get('smtp_password')
if smtp_encryption is None and tools.config.get('smtp_ssl'):
- smtp_encryption = 'starttls' # STARTTLS is the new meaning of the smtp_ssl flag as of v7.0
+ smtp_encryption = 'starttls' # STARTTLS is the new meaning of the smtp_ssl flag as of v7.0
if not smtp_server:
raise UserError(_("Missing SMTP Server") + "\n" + _("Please define at least one SMTP server, or provide the SMTP parameters explicitly."))
@@ -468,20 +457,23 @@ class IrMailServer(models.Model):
finally:
if smtp is not None:
smtp.quit()
- except Exception, e:
- msg = _("Mail delivery failed via SMTP server '%s'.\n%s: %s") % (tools.ustr(smtp_server),
- e.__class__.__name__,
- tools.ustr(e))
+ except Exception as e:
+ params = (ustr(smtp_server), e.__class__.__name__, ustr(e))
+ msg = _("Mail delivery failed via SMTP server '%s'.\n%s: %s") % params
_logger.info(msg)
raise MailDeliveryException(_("Mail Delivery Failed"), msg)
return message_id
- def on_change_encryption(self, cr, uid, ids, smtp_encryption):
- if smtp_encryption == 'ssl':
- result = {'value': {'smtp_port': 465}}
+ @api.onchange('smtp_encryption')
+ def _onchange_encryption(self):
+ result = {}
+ if self.smtp_encryption == 'ssl':
+ self.smtp_port = 465
if not 'SMTP_SSL' in smtplib.__all__:
- result['warning'] = {'title': _('Warning'),
- 'message': _('Your server does not seem to support SSL, you may want to try STARTTLS instead')}
+ result['warning'] = {
+ 'title': _('Warning'),
+ 'message': _('Your server does not seem to support SSL, you may want to try STARTTLS instead'),
+ }
else:
- result = {'value': {'smtp_port': 25}}
+ self.smtp_port = 25
return result
diff --git a/openerp/addons/base/ir/ir_mail_server_view.xml b/openerp/addons/base/ir/ir_mail_server_view.xml
index 35bf5bb117b..5e98d256cf9 100644
--- a/openerp/addons/base/ir/ir_mail_server_view.xml
+++ b/openerp/addons/base/ir/ir_mail_server_view.xml
@@ -16,7 +16,7 @@
<field name="smtp_debug" groups="base.group_no_one"/>
</group>
<group string="Security and Authentication" colspan="4">
- <field name="smtp_encryption" on_change="on_change_encryption(smtp_encryption)"/>
+ <field name="smtp_encryption"/>
<field name="smtp_user"/>
<field name="smtp_pass" password="True"/>
<button name="test_smtp_connection" type="object" string="Test Connection" icon="gtk-network"/>
diff --git a/openerp/addons/base/ir/ir_model.py b/openerp/addons/base/ir/ir_model.py
index a1a9f773fb7..91d8cf15c12 100644
--- a/openerp/addons/base/ir/ir_model.py
+++ b/openerp/addons/base/ir/ir_model.py
@@ -1,20 +1,13 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
-import re
-import time
-import types
from collections import defaultdict
-import odoo
from odoo import api, fields, models, SUPERUSER_ID, tools, _
-from odoo.exceptions import AccessError, UserError
+from odoo.exceptions import AccessError, UserError, ValidationError
from odoo.modules import init_models
from odoo.modules.registry import RegistryManager
-from odoo.osv import osv
-from odoo.osv.orm import BaseModel, MAGIC_COLUMNS
-from odoo.tools import config
-from odoo.tools.safe_eval import safe_eval as eval
+from odoo.tools.safe_eval import safe_eval
_logger = logging.getLogger(__name__)
@@ -33,7 +26,8 @@ class Base(models.AbstractModel):
""" The base model, which is implicitly inherited by all models. """
_name = 'base'
-class unknown(models.AbstractModel):
+
+class Unknown(models.AbstractModel):
"""
Abstract model used as a substitute for relational fields with an unknown
comodel.
@@ -58,25 +52,23 @@ class IrModel(models.Model):
modules = fields.Char(compute='_in_modules', string='In Apps', help='List of modules in which the object is defined or inherited')
view_ids = fields.One2many('ir.ui.view', compute='_view_ids', string='Views')
- @api.depends('inherited_model_ids')
+ @api.depends()
def _inherited_models(self):
for model in self:
- inherited_models = [model_name for model_name in self.env[model.model]._inherits]
- if inherited_models:
- model.inherited_model_ids = self.search([('model', 'in', inherited_models)])
+ parent_names = list(self.env[model.model]._inherits)
+ if parent_names:
+ model.inherited_model_ids = self.search([('model', 'in', parent_names)])
- @api.depends('modules')
+ @api.depends()
def _in_modules(self):
- #pseudo-method used by fields.function in ir.model/ir.model.fields
- module = self.env['ir.module.module']
- installed_module_ids = module.search([('state', '=', 'installed')])
- installed_module_names = installed_module_ids.read(['name'])
- installed_modules = set(x['name'] for x in installed_module_names)
- xml_ids = models.Model._get_xml_ids(self)
- for key, value in xml_ids.iteritems():
- self.modules = ', '.join(sorted(installed_modules & set(xml_id.split('.')[0] for xml_id in value)))
-
- @api.depends('view_ids')
+ installed_modules = self.env['ir.module.module'].search([('state', '=', 'installed')])
+ installed_names = set(installed_modules.mapped('name'))
+ xml_ids = models.Model._get_external_ids(self)
+ for model in self:
+ module_names = set(xml_id.split('.')[0] for xml_id in xml_ids[model.id])
+ model.modules = ", ".join(sorted(installed_names & module_names))
+
+ @api.depends()
def _view_ids(self):
for model in self:
model.view_ids = self.env['ir.ui.view'].search([('model', '=', model.model)])
@@ -86,71 +78,67 @@ class IrModel(models.Model):
for model in self:
if model.state == 'manual':
if not model.model.startswith('x_'):
- return self._model_name_msg()
- if not re.match('^[a-z_A-Z0-9.]+$', model.model):
- return self._model_name_msg()
- return True
-
- def _model_name_msg(self):
- return _('The Object name must start with x_ and not contain any special character !')
+ raise ValidationError(_("The model name must start with 'x_'."))
+ if not models.check_object_name(model.model):
+ raise ValidationError(_("The model name can only contain lowercase characters, digits, underscores and dots."))
_sql_constraints = [
('obj_name_uniq', 'unique (model)', 'Each model must be unique!'),
]
- # overridden to allow searching both on model name (model field)
- # and model description (name field)
+ # overridden to allow searching both on model name (field 'model') and model
+ # description (field 'name')
@api.model
def _name_search(self, name='', args=None, operator='ilike', limit=100):
if args is None:
args = []
- domain = args + ['|', ('model', operator, self.name), ('name', operator, self.name)]
+ domain = args + ['|', ('model', operator, name), ('name', operator, name)]
return super(IrModel, self).search(domain, limit=limit).name_get()
def _drop_table(self):
for model in self:
- model_pool = self.env[model.model]
- self.env.cr.execute('select relkind from pg_class where relname=%s', (model_pool._table,))
- result = self.env.cr.fetchone()
+ table = self.env[model.model]._table
+ self._cr.execute('select relkind from pg_class where relname=%s', (table,))
+ result = self._cr.fetchone()
if result and result[0] == 'v':
- self.env.cr.execute('DROP view %s' % (model_pool._table,))
+ self._cr.execute('DROP view %s' % table)
elif result and result[0] == 'r':
- self.env.cr.execute('DROP TABLE %s CASCADE' % (model_pool._table,))
+ self._cr.execute('DROP TABLE %s CASCADE' % table)
return True
@api.multi
def unlink(self):
# Prevent manual deletion of module tables
- if not self.env.context.get(MODULE_UNINSTALL_FLAG):
+ if not self._context.get(MODULE_UNINSTALL_FLAG):
for model in self:
if model.state != 'manual':
- raise UserError(_("Model '%s' contains module data and cannot be removed!") % (model.name,))
+ raise UserError(_("Model '%s' contains module data and cannot be removed!") % model.name)
self._drop_table()
res = super(IrModel, self).unlink()
- if not self.env.context.get(MODULE_UNINSTALL_FLAG):
- # only reload pool for normal unlink. For module uninstall the
- # reload is done independently in odoo.modules.loading
- self.env.cr.commit() # must be committed before reloading registry in new cursor
+
+ # Reload registry for normal unlink only. For module uninstall, the
+ # reload is done independently in odoo.modules.loading.
+ if not self._context.get(MODULE_UNINSTALL_FLAG):
+ self._cr.commit() # must be committed before reloading registry in new cursor
api.Environment.reset()
- RegistryManager.new(self.env.cr.dbname)
- RegistryManager.signal_registry_change(self.env.cr.dbname)
+ RegistryManager.new(self._cr.dbname)
+ RegistryManager.signal_registry_change(self._cr.dbname)
return res
@api.multi
def write(self, vals):
- if self.env.context:
- self.env.context = dict(self.env.context)
- self.env.context.pop('__last_update', None)
+ if '__last_update' in self._context:
+ self = self.with_context({k: v for k, v in self._context.iteritems() if k != '__last_update'})
if 'model' in vals:
raise UserError(_('Field "Model" cannot be modified on models.'))
if 'state' in vals:
raise UserError(_('Field "Type" cannot be modified on models.'))
if 'transient' in vals:
raise UserError(_('Field "Transient Model" cannot be modified on models.'))
- # Filter out operations 4 link from field id, because odoo-web
- # always write (4,id,False) even for non dirty items
+ # Filter out operations 4 from field id, because the web client always
+ # writes (4,id,False) even for non dirty items.
if 'field_id' in vals:
vals['field_id'] = [op for op in vals['field_id'] if op[0] != 4]
return super(IrModel, self).write(vals)
@@ -160,11 +148,11 @@ class IrModel(models.Model):
res = super(IrModel, self).create(vals)
if vals.get('state', 'manual') == 'manual':
# setup models; this automatically adds model in registry
- self.pool.setup_models(self.env.cr, partial=(not self.pool.ready))
+ self.pool.setup_models(self._cr, partial=(not self.pool.ready))
# update database schema
model = self.pool[vals['model']]
- init_models([model], self.env.cr, dict(self.env.context, update_custom_fields=True))
- RegistryManager.signal_registry_change(self.env.cr.dbname)
+ init_models([model], self._cr, dict(self._context, update_custom_fields=True))
+ RegistryManager.signal_registry_change(self._cr.dbname)
return res
@api.model
@@ -181,23 +169,12 @@ class IrModel(models.Model):
CustomModel._build_model(self.pool, self._cr)
-class IrModelFields(osv.osv):
+class IrModelFields(models.Model):
_name = 'ir.model.fields'
_description = "Fields"
_order = "name"
_rec_name = 'field_description'
- @api.depends('ttype')
- def _get_fields_type(self):
- # Avoid too many nested `if`s below, as RedHat's Python 2.6
- # break on it. See bug 939653.
- return sorted([(k,k) for k,v in odoo.osv.fields.__dict__.iteritems()
- if type(v) == types.TypeType and \
- issubclass(v, odoo.osv.fields._column) and \
- v != odoo.osv.fields._column and \
- not v._deprecated and \
- not issubclass(v, odoo.osv.fields.function)])
-
name = fields.Char(string='Field Name', default='x_', required=True, index=True)
complete_name = fields.Char(index=True)
model = fields.Char(string='Object Name', required=True, index=True,
@@ -209,7 +186,7 @@ class IrModelFields(osv.osv):
help="The model this field belongs to")
field_description = fields.Char(string='Field Label', default='', required=True, translate=True)
help = fields.Text(string='Field Help', translate=True)
- ttype = fields.Selection(selection='_get_fields_type', string='Field Type', required=True)
+ ttype = fields.Selection(selection='_get_field_types', string='Field Type', required=True)
selection = fields.Char(string='Selection Options', default="",
help="List of options for a selection field, "
"specified as a Python expression defining a list of (key, label) pairs. "
@@ -248,42 +225,35 @@ class IrModelFields(osv.osv):
" name, partner_id.name")
store = fields.Boolean(string='Stored', default=True, help="Whether the value is stored in the database.")
- @api.depends('modules')
+ @api.model
+ def _get_field_types(self):
+ # retrieve the possible field types from the field classes' metaclass
+ return sorted((key, key) for key in fields.MetaField.by_type)
+
+ @api.depends()
def _in_modules(self):
- #pseudo-method used by fields.function in ir.model/ir.model.fields
- module = self.env['ir.module.module']
- installed_module_ids = module.search([('state', '=', 'installed')])
- installed_module_names = installed_module_ids.read(['name'])
- installed_modules = set(x['name'] for x in installed_module_names)
- xml_ids = models.Model._get_xml_ids(self)
- for key, value in xml_ids.iteritems():
- self.modules = ', '.join(sorted(installed_modules & set(xml_id.split('.')[0] for xml_id in value)))
+ installed_modules = self.env['ir.module.module'].search([('state', '=', 'installed')])
+ installed_names = set(installed_modules.mapped('name'))
+ xml_ids = models.Model._get_external_ids(self)
+ for field in self:
+ module_names = set(xml_id.split('.')[0] for xml_id in xml_ids[field.id])
+ field.modules = ", ".join(sorted(installed_names & module_names))
+ @api.model
def _check_selection(self, selection):
try:
- selection_list = eval(selection)
+ items = safe_eval(selection)
+ if not (isinstance(items, (tuple, list)) and
+ all(isinstance(item, (tuple, list)) and len(item) == 2 for item in items)):
+ raise ValueError(selection)
except Exception:
_logger.info('Invalid selection list definition for fields.selection', exc_info=True)
raise UserError(_("The Selection Options expression is not a valid Pythonic expression."
"Please provide an expression in the [('key','Label'), ...] format."))
- check = True
- if not (isinstance(selection_list, list) and selection_list):
- check = False
- else:
- for item in selection_list:
- if not (isinstance(item, (tuple, list)) and len(item) == 2):
- check = False
- break
-
- if not check:
- raise UserError(_("The Selection Options expression is must be in the [('key','Label'), ...] format!"))
- return True
-
- def _size_gt_zero_msg(self, cr, user, ids, context=None):
- return _('Size of the field can never be less than 0 !')
-
- _sql_constraints = [('size_gt_zero', 'CHECK (size>=0)', _size_gt_zero_msg)]
+ _sql_constraints = [
+ ('size_gt_zero', 'CHECK (size>=0)', 'Size of the field cannot be negative.'),
+ ]
def _related_field(self):
""" Return the ``Field`` instance corresponding to ``self.related``. """
@@ -378,47 +348,51 @@ class IrModelFields(osv.osv):
tables_to_drop = set()
for field in self:
- if field.name in MAGIC_COLUMNS:
+ if field.name in models.MAGIC_COLUMNS:
continue
- model = self.pool[field.model]
- self.env.cr.execute('SELECT relkind FROM pg_class WHERE relname=%s', (model._table,))
- result = self.env.cr.fetchone()
- self.env.cr.execute("""SELECT column_name FROM information_schema.columns
- WHERE table_name=%s AND column_name=%s""", (model._table, field.name))
- column_name = self.env.cr.fetchone()
- if column_name and (result and result[0] == 'r'):
- self.env.cr.execute('ALTER table "%s" DROP column "%s" cascade' % (model._table, field.name))
+ model = self.env[field.model]
+ self._cr.execute('SELECT relkind FROM pg_class WHERE relname=%s', (model._table,))
+ relkind = self._cr.fetchone()
+ self._cr.execute("""SELECT column_name FROM information_schema.columns
+ WHERE table_name=%s AND column_name=%s""",
+ (model._table, field.name))
+ column_name = self._cr.fetchone()
+ if column_name and (relkind and relkind[0] == 'r'):
+ self._cr.execute('ALTER table "%s" DROP column "%s" cascade' % (model._table, field.name))
if field.state == 'manual' and field.ttype == 'many2many':
rel_name = field.relation_table or model._fields[field.name].relation
tables_to_drop.add(rel_name)
- model._pop_field(self.env.cr, self.env.uid, field.name, context=self.env.context)
+ model._pop_field(field.name)
if tables_to_drop:
# drop the relation tables that are not used by other fields
- self.env.cr.execute("""SELECT relation_table FROM ir_model_fields
- WHERE relation_table IN %s AND id NOT IN %s""", (tuple(tables_to_drop), tuple(self.ids)))
- tables_to_keep = set(row[0] for row in self.env.cr.fetchall())
+ self._cr.execute("""SELECT relation_table FROM ir_model_fields
+ WHERE relation_table IN %s AND id NOT IN %s""",
+ (tuple(tables_to_drop), tuple(self.ids)))
+ tables_to_keep = set(row[0] for row in self._cr.fetchall())
for rel_name in tables_to_drop - tables_to_keep:
- self.env.cr.execute('DROP TABLE "%s"' % rel_name)
+ self._cr.execute('DROP TABLE "%s"' % rel_name)
return True
@api.multi
def unlink(self):
# Prevent manual deletion of module columns
- if not self.env.context.get(MODULE_UNINSTALL_FLAG) and \
+ if not self._context.get(MODULE_UNINSTALL_FLAG) and \
any(field.state != 'manual' for field in self):
raise UserError(_("This column contains module data and cannot be removed!"))
self._drop_column()
res = super(IrModelFields, self).unlink()
- if not self.env.context.get(MODULE_UNINSTALL_FLAG):
- # The field we just deleted might be inherited, and the registry is
- # inconsistent in this case; therefore we reload the registry.
- self.env.cr.commit()
+
+ # The field we just deleted might be inherited, and the registry is
+ # inconsistent in this case; therefore we reload the registry.
+ if not self._context.get(MODULE_UNINSTALL_FLAG):
+ self._cr.commit()
api.Environment.reset()
- RegistryManager.new(self.env.cr.dbname)
- RegistryManager.signal_registry_change(self.env.cr.dbname)
+ RegistryManager.new(self._cr.dbname)
+ RegistryManager.signal_registry_change(self._cr.dbname)
+
return res
@api.model
@@ -426,19 +400,21 @@ class IrModelFields(osv.osv):
if 'model_id' in vals:
model_data = self.env['ir.model'].browse(vals['model_id'])
vals['model'] = model_data.model
- if vals.get('ttype', False) == 'selection':
- if not vals.get('selection', False):
+ if vals.get('ttype') == 'selection':
+ if not vals.get('selection'):
raise UserError(_('For selection fields, the Selection Options must be given!'))
self._check_selection(vals['selection'])
+
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', False) and not self.env['ir.model'].search([('model', '=', vals['relation'])]):
+ if vals.get('relation') and not self.env['ir.model'].search([('model', '=', vals['relation'])]):
raise UserError(_("Model %s does not exist!") % vals['relation'])
- if vals.get('ttype', False) == 'one2many':
+ if vals.get('ttype') == 'one2many':
if not self.search([('model_id', '=', vals['relation']), ('name', '=', vals['relation_field']), ('ttype', '=', 'many2one')]):
raise UserError(_("Many2one %s on model %s does not exist!") % (vals['relation_field'], vals['relation']))
@@ -446,17 +422,18 @@ class IrModelFields(osv.osv):
if vals['model'] in self.pool:
# setup models; this re-initializes model in registry
- self.pool.setup_models(self.env.cr, partial=(not self.pool.ready))
+ self.pool.setup_models(self._cr, partial=(not self.pool.ready))
# update database schema
model = self.pool[vals['model']]
- init_models([model], self.env.cr, dict(self.env.context, update_custom_fields=True))
- RegistryManager.signal_registry_change(self.env.cr.dbname)
+ init_models([model], self._cr, dict(self._context, update_custom_fields=True))
+ RegistryManager.signal_registry_change(self._cr.dbname)
return res
@api.multi
def write(self, vals):
- #For the moment renaming a sparse field or changing the storing system is not allowed. This may be done later
+ # For the moment renaming a sparse field or changing the storing system
+ # is not allowed. This may be done later
if 'serialization_field_id' in vals or 'name' in vals:
for field in self:
if 'serialization_field_id' in vals and field.serialization_field_id.id != vals['serialization_field_id']:
@@ -470,7 +447,7 @@ class IrModelFields(osv.osv):
# names of the models to patch
patched_models = set()
- if vals and self.ids:
+ if vals and self:
# check selection if given
if vals.get('selection'):
self._check_selection(vals['selection'])
@@ -520,21 +497,21 @@ class IrModelFields(osv.osv):
if column_rename:
# rename column in database, and its corresponding index if present
table, oldname, newname, index = column_rename
- self.env.cr.execute('ALTER TABLE "%s" RENAME COLUMN "%s" TO "%s"' % (table, oldname, newname))
+ self._cr.execute('ALTER TABLE "%s" RENAME COLUMN "%s" TO "%s"' % (table, oldname, newname))
if index:
- self.env.cr.execute('ALTER INDEX "%s_%s_index" RENAME TO "%s_%s_index"' % (table, oldname, table, newname))
+ self._cr.execute('ALTER INDEX "%s_%s_index" RENAME TO "%s_%s_index"' % (table, oldname, table, newname))
if column_rename or patched_models:
# setup models, this will reload all manual fields in registry
- self.pool.setup_models(self.env.cr, partial=(not self.pool.ready))
+ self.pool.setup_models(self._cr, partial=(not self.pool.ready))
if patched_models:
# update the database schema of the models to patch
models = [self.pool[name] for name in patched_models]
- init_models(models, self.env.cr, dict(self.env.context, update_custom_fields=True))
+ init_models(models, self._cr, dict(self._context, update_custom_fields=True))
if column_rename or patched_models:
- RegistryManager.signal_registry_change(self.env.cr.dbname)
+ RegistryManager.signal_registry_change(self._cr.dbname)
return res
@@ -559,7 +536,7 @@ class IrModelConstraint(models.Model):
_sql_constraints = [
('module_name_uniq', 'unique(name, module)',
- 'Constraints with the same name are unique per module.'),
+ 'Constraints with the same name are unique per module.'),
]
@api.multi
@@ -567,41 +544,39 @@ class IrModelConstraint(models.Model):
"""
Delete PostgreSQL foreign keys and constraints tracked by this model.
"""
-
- if self.env.uid != SUPERUSER_ID and not self.env['ir.model.access'].check_groups("base.group_system"):
+ if not (self._uid == SUPERUSER_ID or self.env.user.has_group('base.group_system')):
raise AccessError(_('Administrator access is required to uninstall a module'))
ids_set = set(self.ids)
- self.ids.sort()
- self.ids.reverse()
- for data in self:
- model = data.model.model
- model_obj = self.env[model]
+ for data in self.sorted(key='id', reverse=True):
+ model = self.env[data.model.model]
name = tools.ustr(data.name)
typ = data.type
# double-check we are really going to delete all the owners of this schema element
- self.env.cr.execute("""SELECT id from ir_model_constraint where name=%s""", (data.name,))
- external_ids = [x[0] for x in self.env.cr.fetchall()]
- if set(external_ids)-ids_set:
+ self._cr.execute("""SELECT id from ir_model_constraint where name=%s""", (data.name,))
+ external_ids = set(x[0] for x in self._cr.fetchall())
+ if external_ids - ids_set:
# as installed modules have defined this element we must not delete it!
continue
if typ == 'f':
# test if FK exists on this table (it could be on a related m2m table, in which case we ignore it)
- self.env.cr.execute("""SELECT 1 from pg_constraint cs JOIN pg_class cl ON (cs.conrelid = cl.oid)
- WHERE cs.contype=%s and cs.conname=%s and cl.relname=%s""", ('f', name, model_obj._table))
- if self.env.cr.fetchone():
- self.env.cr.execute('ALTER TABLE "%s" DROP CONSTRAINT "%s"' % (model_obj._table, name),)
- _logger.info('Dropped FK CONSTRAINT %s@%s', name, model)
+ self._cr.execute("""SELECT 1 from pg_constraint cs JOIN pg_class cl ON (cs.conrelid = cl.oid)
+ WHERE cs.contype=%s and cs.conname=%s and cl.relname=%s""",
+ ('f', name, model._table))
+ if self._cr.fetchone():
+ self._cr.execute('ALTER TABLE "%s" DROP CONSTRAINT "%s"' % (model._table, name),)
+ _logger.info('Dropped FK CONSTRAINT %s@%s', name, data.model.model)
if typ == 'u':
# test if constraint exists
- self.env.cr.execute("""SELECT 1 from pg_constraint cs JOIN pg_class cl ON (cs.conrelid = cl.oid)
- WHERE cs.contype=%s and cs.conname=%s and cl.relname=%s""", ('u', name, model_obj._table))
- if self.env.cr.fetchone():
- self.env.cr.execute('ALTER TABLE "%s" DROP CONSTRAINT "%s"' % (model_obj._table, name),)
- _logger.info('Dropped CONSTRAINT %s@%s', name, model)
+ self._cr.execute("""SELECT 1 from pg_constraint cs JOIN pg_class cl ON (cs.conrelid = cl.oid)
+ WHERE cs.contype=%s and cs.conname=%s and cl.relname=%s""",
+ ('u', name, model._table))
+ if self._cr.fetchone():
+ self._cr.execute('ALTER TABLE "%s" DROP CONSTRAINT "%s"' % (model._table, name),)
+ _logger.info('Dropped CONSTRAINT %s@%s', name, data.model.model)
self.unlink()
@@ -624,37 +599,34 @@ class IrModelRelation(models.Model):
def _module_data_uninstall(self):
"""
Delete PostgreSQL many2many relations tracked by this model.
- """
-
- if self.env.uid != SUPERUSER_ID and not self.env['ir.model.access'].check_groups("base.group_system"):
+ """
+ if not (self._uid == SUPERUSER_ID or self.env.user.has_group('base.group_system')):
raise AccessError(_('Administrator access is required to uninstall a module'))
ids_set = set(self.ids)
- to_drop_table = []
- self.ids.sort()
- self.ids.reverse()
- for data in self:
+ to_drop = tools.OrderedSet()
+ for data in self.sorted(key='id', reverse=True):
name = tools.ustr(data.name)
# double-check we are really going to delete all the owners of this schema element
- self.env.cr.execute("""SELECT id from ir_model_relation where name = %s""", (data.name,))
- external_ids = [x[0] for x in self.env.cr.fetchall()]
- if set(external_ids)-ids_set:
+ self._cr.execute("""SELECT id from ir_model_relation where name = %s""", (data.name,))
+ external_ids = set(x[0] for x in self._cr.fetchall())
+ if external_ids - ids_set:
# as installed modules have defined this element we must not delete it!
continue
- self.env.cr.execute("SELECT 1 FROM information_schema.tables WHERE table_name=%s", (name,))
- if self.env.cr.fetchone() and not name in to_drop_table:
- to_drop_table.append(name)
+ self._cr.execute("SELECT 1 FROM information_schema.tables WHERE table_name=%s", (name,))
+ if self._cr.fetchone():
+ to_drop.add(name)
self.unlink()
# drop m2m relation tables
- for table in to_drop_table:
- self.env.cr.execute('DROP TABLE %s CASCADE' % table,)
+ for table in to_drop:
+ self._cr.execute('DROP TABLE %s CASCADE' % table,)
_logger.info('Dropped table %s', table)
- self.env.cr.commit()
+ self._cr.commit()
class IrModelAccess(models.Model):
@@ -671,18 +643,22 @@ class IrModelAccess(models.Model):
@api.model
def check_groups(self, group):
+ """ Check whether the current user has the given group. """
grouparr = group.split('.')
if not grouparr:
return False
- self.env.cr.execute("select 1 from res_groups_users_rel where uid=%s and gid IN (select res_id from ir_model_data where module=%s and name=%s)", (self.env.uid, grouparr[0], grouparr[1],))
- return bool(self.env.cr.fetchone())
+ self._cr.execute("""SELECT 1 FROM res_groups_users_rel
+ WHERE uid=%s AND gid IN (
+ SELECT res_id FROM ir_model_data WHERE module=%s AND name=%s)""",
+ (self._uid, grouparr[0], grouparr[1],))
+ return bool(self._cr.fetchone())
@api.model
def check_group(self, model, mode, group_ids):
""" Check if a specific group has the access mode to the specified model"""
- assert mode in ['read', 'write', 'create', 'unlink'], 'Invalid access mode'
+ assert mode in ('read', 'write', 'create', 'unlink'), 'Invalid access mode'
- if isinstance(model, BaseModel):
+ if isinstance(model, models.BaseModel):
assert model._name == 'ir.model', 'Invalid model object'
model_name = model.name
else:
@@ -690,64 +666,45 @@ class IrModelAccess(models.Model):
if isinstance(group_ids, (int, long)):
group_ids = [group_ids]
- for group_id in group_ids:
- self.env.cr.execute("SELECT perm_" + mode + " "
- " FROM ir_model_access a "
- " JOIN ir_model m ON (m.id = a.model_id) "
- " WHERE m.model = %s AND a.active IS True "
- " AND a.group_id = %s", (model_name, group_id)
- )
- r = self.env.cr.fetchone()
- if r is None:
- self.env.cr.execute("SELECT perm_" + mode + " "
- " FROM ir_model_access a "
- " JOIN ir_model m ON (m.id = a.model_id) "
- " WHERE m.model = %s AND a.active IS True "
- " AND a.group_id IS NULL", (model_name, )
- )
- r = self.env.cr.fetchone()
-
- access = bool(r and r[0])
- if access:
- return True
- # pass no groups -> no access
- return False
- @api.model
+ query = """ SELECT 1 FROM ir_model_access a
+ JOIN ir_model m ON (m.id = a.model_id)
+ WHERE a.active AND a.perm_{mode} AND
+ m.model=%s AND (a.group_id IN %s OR a.group_id IS NULL)
+ """.format(mode=mode)
+ self._cr.execute(query, (model_name, tuple(group_ids)))
+ return bool(self._cr.rowcount)
+
+ @api.model_cr
def group_names_with_access(self, model_name, access_mode):
- """Returns the names of visible groups which have been granted ``access_mode`` on
- the model ``model_name``.
+ """ Return the names of visible groups which have been granted
+ ``access_mode`` on the model ``model_name``.
:rtype: list
"""
- assert access_mode in ['read', 'write', 'create', 'unlink'], 'Invalid access mode: %s' % access_mode
- self.env.cr.execute('''SELECT
- c.name, g.name
- FROM
- ir_model_access a
- JOIN ir_model m ON (a.model_id=m.id)
- JOIN res_groups g ON (a.group_id=g.id)
- LEFT JOIN ir_module_category c ON (c.id=g.category_id)
- WHERE
- m.model=%s AND
- a.active IS True AND
- a.perm_''' + access_mode, (model_name,))
- return [('%s/%s' % x) if x[0] else x[1] for x in self.env.cr.fetchall()]
+ assert access_mode in ('read', 'write', 'create', 'unlink'), 'Invalid access mode'
+ self._cr.execute("""SELECT c.name, g.name
+ FROM ir_model_access a
+ JOIN ir_model m ON (a.model_id=m.id)
+ JOIN res_groups g ON (a.group_id=g.id)
+ LEFT JOIN ir_module_category c ON (c.id=g.category_id)
+ WHERE m.model=%s AND a.active IS TRUE AND a.perm_""" + access_mode,
+ (model_name,))
+ return [('%s/%s' % x) if x[0] else x[1] for x in self._cr.fetchall()]
# The context parameter is useful when the method translates error messages.
# But as the method raises an exception in that case, the key 'lang' might
# not be really necessary as a cache key, unless the `ormcache_context`
# decorator catches the exception (it does not at the moment.)
@api.model
- @tools.ormcache_context('self.env.uid', 'model', 'mode', 'raise_exception', keys=('lang',))
+ @tools.ormcache_context('self._uid', 'model', 'mode', 'raise_exception', keys=('lang',))
def check(self, model, mode='read', raise_exception=True):
- if self.env.uid == 1:
+ if self._uid == 1:
# User root have all accesses
- # TODO: exclude xml-rpc requests
return True
- assert mode in ['read', 'write', 'create', 'unlink'], 'Invalid access mode'
+ assert mode in ('read', 'write', 'create', 'unlink'), 'Invalid access mode'
- if isinstance(model, BaseModel):
+ if isinstance(model, models.BaseModel):
assert model._name == 'ir.model', 'Invalid model object'
model_name = model.model
else:
@@ -755,29 +712,31 @@ class IrModelAccess(models.Model):
# TransientModel records have no access rights, only an implicit access rule
if model_name not in self.env:
- _logger.error('Missing model %s' % (model_name, ))
- elif self.pool[model_name].is_transient():
+ _logger.error('Missing model %s', model_name)
+ elif self.env[model_name].is_transient():
return True
# We check if a specific rule exists
- self.env.cr.execute('SELECT MAX(CASE WHEN perm_' + mode + ' THEN 1 ELSE 0 END) '
- ' FROM ir_model_access a '
- ' JOIN ir_model m ON (m.id = a.model_id) '
- ' JOIN res_groups_users_rel gu ON (gu.gid = a.group_id) '
- ' WHERE m.model = %s '
- ' AND gu.uid = %s '
- ' AND a.active IS True ', (model_name, self.env.uid,))
- r = self.env.cr.fetchone()[0]
-
- if r is None:
+ self._cr.execute("""SELECT MAX(CASE WHEN perm_{mode} THEN 1 ELSE 0 END)
+ FROM ir_model_access a
+ JOIN ir_model m ON (m.id = a.model_id)
+ JOIN res_groups_users_rel gu ON (gu.gid = a.group_id)
+ WHERE m.model = %s
+ AND gu.uid = %s
+ AND a.active IS TRUE""".format(mode=mode),
+ (model_name, self._uid,))
+ r = self._cr.fetchone()[0]
+
+ if not r:
# there is no specific rule. We check the generic rule
- self.env.cr.execute('SELECT MAX(CASE WHEN perm_' + mode + ' THEN 1 ELSE 0 END) '
- ' FROM ir_model_access a '
- ' JOIN ir_model m ON (m.id = a.model_id) '
- ' WHERE a.group_id IS NULL '
- ' AND m.model = %s '
- ' AND a.active IS True ', (model_name,))
- r = self.env.cr.fetchone()[0]
+ self._cr.execute("""SELECT MAX(CASE WHEN perm_{mode} THEN 1 ELSE 0 END)
+ FROM ir_model_access a
+ JOIN ir_model m ON (m.id = a.model_id)
+ WHERE a.group_id IS NULL
+ AND m.model = %s
+ AND a.active IS TRUE""".format(mode=mode),
+ (model_name,))
+ r = self._cr.fetchone()[0]
if not r and raise_exception:
groups = '\n\t'.join('- %s' % g for g in self.group_names_with_access(model_name, mode))
@@ -794,9 +753,10 @@ class IrModelAccess(models.Model):
else:
msg_tail = _("Please contact your system administrator if you think this is an error.") + "\n\n(" + _("Document model") + ": %s)"
msg_params = (model_name,)
- _logger.info('Access Denied by ACLs for operation: %s, uid: %s, model: %s', mode, self.env.uid, model_name)
+ _logger.info('Access Denied by ACLs for operation: %s, uid: %s, model: %s', mode, self._uid, model_name)
msg = '%s %s' % (msg_heads[mode], msg_tail)
- raise odoo.exceptions.AccessError(msg % msg_params)
+ raise AccessError(msg % msg_params)
+
return bool(r)
__cache_clearing_methods = []
@@ -806,32 +766,32 @@ class IrModelAccess(models.Model):
def unregister_cache_clearing_method(self, model, method):
try:
- i = self.__cache_clearing_methods.index((model, method))
- del self.__cache_clearing_methods[i]
+ self.__cache_clearing_methods.remove((model, method))
except ValueError:
pass
- def call_cache_clearing_methods(self, cr):
- self.invalidate_cache(cr, SUPERUSER_ID)
+ @api.model_cr
+ def call_cache_clearing_methods(self):
+ self.invalidate_cache()
self.check.clear_cache(self) # clear the cache of check function
for model, method in self.__cache_clearing_methods:
- if model in self.pool:
- getattr(self.pool[model], method)()
+ if model in self.env:
+ getattr(self.env[model], method)()
#
# Check rights on actions
#
- @api.multi
- def write(self, values):
- self.call_cache_clearing_methods()
- return super(IrModelAccess, self).write(values)
-
@api.model
def create(self, values):
self.call_cache_clearing_methods()
return super(IrModelAccess, self).create(values)
@api.multi
+ def write(self, values):
+ self.call_cache_clearing_methods()
+ return super(IrModelAccess, self).write(values)
+
+ @api.multi
def unlink(self):
self.call_cache_clearing_methods()
return super(IrModelAccess, self).unlink()
@@ -854,7 +814,7 @@ class IrModelData(models.Model):
name = fields.Char(string='External Identifier', required=True,
help="External Key/Identifier that can be used for "
"data integration with third-party systems")
- complete_name = fields.Char(compute='_complete_name_get', string='Complete ID')
+ complete_name = fields.Char(compute='_compute_complete_name', string='Complete ID')
model = fields.Char(string='Model Name', required=True)
module = fields.Char(default='', required=True)
res_id = fields.Integer(string='Record ID', help="ID of the target record in the database")
@@ -862,10 +822,10 @@ class IrModelData(models.Model):
date_update = fields.Datetime(string='Update Date', default=fields.Datetime.now)
date_init = fields.Datetime(string='Init Date', default=fields.Datetime.now)
- @api.depends('complete_name')
- def _complete_name_get(self):
+ @api.depends('module', 'name')
+ def _compute_complete_name(self):
for res in self:
- res.complete_name = (res.module and (res.module + '.') or '') + res.name
+ res.complete_name = ".".join(filter(None, [res.module, res.name]))
def __init__(self, pool, cr):
models.Model.__init__(self, pool, cr)
@@ -875,51 +835,50 @@ class IrModelData(models.Model):
# put loads on the class, in order to share it among all instances
type(self).loads = self.pool.model_data_reference_ids
+ @api.model_cr_context
def _auto_init(self):
super(IrModelData, self)._auto_init()
- self.env.cr.execute("SELECT indexname FROM pg_indexes WHERE indexname = 'ir_model_data_module_name_uniq_index'")
- if not self.env.cr.fetchone():
- self.env.cr.execute('CREATE UNIQUE INDEX ir_model_data_module_name_uniq_index ON ir_model_data (module, name)')
- self.env.cr.execute("SELECT indexname FROM pg_indexes WHERE indexname = 'ir_model_data_model_res_id_index'")
- if not self.env.cr.fetchone():
- self.env.cr.execute('CREATE INDEX ir_model_data_model_res_id_index ON ir_model_data (model, res_id)')
+ self._cr.execute("SELECT indexname FROM pg_indexes WHERE indexname = 'ir_model_data_module_name_uniq_index'")
+ if not self._cr.fetchone():
+ self._cr.execute('CREATE UNIQUE INDEX ir_model_data_module_name_uniq_index ON ir_model_data (module, name)')
+ self._cr.execute("SELECT indexname FROM pg_indexes WHERE indexname = 'ir_model_data_model_res_id_index'")
+ if not self._cr.fetchone():
+ self._cr.execute('CREATE INDEX ir_model_data_model_res_id_index ON ir_model_data (model, res_id)')
@api.multi
def name_get(self):
- bymodel = defaultdict(dict)
- names = {}
-
- for res in self:
- bymodel[res.model][res.res_id] = res
- names[res.id] = res.complete_name
- #result[res.model][res.res_id] = res.id
+ model_id_name = defaultdict(dict) # {res_model: {res_id: name}}
+ for xid in self:
+ model_id_name[xid.model][xid.res_id] = None
- for model, id_map in bymodel.iteritems():
+ # fill in model_id_name with name_get() of corresponding records
+ for model, id_name in model_id_name.iteritems():
try:
- ng = dict(self.env[model].name_get(id_map.keys()))
+ ng = self.env[model].browse(id_name).name_get()
+ id_name.update(ng)
except Exception:
pass
- else:
- for r in id_map.itervalues():
- names[r.id] = ng.get(r.res_id, r.complete_name)
- return [(i, names[i]) for i in self.ids]
+ # return results, falling back on complete_name
+ return [(xid.id, model_id_name[xid.model][xid.res_id] or xid.complete_name)
+ for xid in self]
# NEW V8 API
+ @api.model
@tools.ormcache('xmlid')
- def xmlid_lookup(self, cr, uid, xmlid):
+ def xmlid_lookup(self, xmlid):
"""Low level xmlid lookup
Return (id, res_model, res_id) or raise ValueError if not found
"""
module, name = xmlid.split('.', 1)
- ids = self.search(cr, uid, [('module', '=', module), ('name', '=', name)])
- if not ids:
- raise ValueError('External ID not found in the system: %s' % (xmlid))
+ xid = self.search([('module', '=', module), ('name', '=', name)])
+ if not xid:
+ raise ValueError('External ID not found in the system: %s' % xmlid)
# the sql constraints ensure us we have only one result
- res = self.read(cr, uid, ids[0], ['model', 'res_id'])
+ res = xid.read(['model', 'res_id'])[0]
if not res['res_id']:
- raise ValueError('External ID not found in the system: %s' % (xmlid))
- return ids[0], res['model'], res['res_id']
+ raise ValueError('External ID not found in the system: %s' % xmlid)
+ return res['id'], res['model'], res['res_id']
@api.model
def xmlid_to_res_model_res_id(self, xmlid, raise_if_not_found=False):
@@ -984,21 +943,20 @@ class IrModelData(models.Model):
@api.model
def _update_dummy(self, model, module, xml_id=False, store=True):
- if not xml_id:
- return False
- try:
- # One step to check the ID is defined and the record actually exists
- record = self.get_object(module, xml_id)
- if record:
- record_id = record.id
- self.loads[(module, xml_id)] = (model, self.id)
- for table, inherit_field in self.pool[model]._inherits.iteritems():
- parent_id = record[inherit_field].id
- parent_xid = '%s_%s' % (xml_id, table.replace('.', '_'))
- self.loads[(module, parent_xid)] = (table, parent_id)
- except Exception:
- pass
- return record_id
+ 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.iteritems():
+ 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
def clear_caches(self):
""" Clears all orm caches on the object's methods
@@ -1014,112 +972,105 @@ class IrModelData(models.Model):
self.clear_caches()
return super(IrModelData, self).unlink()
- def _update(self,cr, uid, model, module, values, xml_id=False, store=True, noupdate=False, mode='init', res_id=False, context=None):
- model_obj = self.pool[model]
- if not context:
- context = {}
+ @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
- context = dict(context, install_mode=True)
+ self = self.with_context(install_mode=True)
+
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
+ 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('.')
- action_id = False
+
+ action = self.browse()
+ record = self.env[model].browse(res_id)
+
if xml_id:
- 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''' % model_obj._table,
- (module, xml_id))
- results = cr.fetchall()
- for imd_id2,res_id2,real_id2,real_model,noupdate_imd in results:
+ 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 noupdate_imd:
- return res_id2
- if not real_id2:
+ if mode == 'update' and imd_noupdate:
+ return imd_res_id
+ if not real_id:
self.clear_caches()
- cr.execute('delete from ir_model_data where id=%s', (imd_id2,))
- res_id = False
+ self._cr.execute('DELETE FROM ir_model_data WHERE id=%s', (imd_id,))
+ record = record.browse()
else:
- assert model == real_model, "External ID conflict, %s already refers to a `%s` record,"\
- " you can't define a `%s` record with this ID." % (xml_id, real_model, model)
- res_id,action_id = res_id2,imd_id2
-
- if action_id and res_id:
- model_obj.write(cr, uid, [res_id], values, context=context)
- self.write(cr, SUPERUSER_ID, [action_id], {
- 'date_update': time.strftime('%Y-%m-%d %H:%M:%S'),
- },context=context)
- elif res_id:
- model_obj.write(cr, uid, [res_id], values, context=context)
+ 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:
- if model_obj._inherits:
- for table in model_obj._inherits:
- inherit_id = model_obj.browse(cr, uid,
- res_id,context=context)[model_obj._inherits[table]]
- self.create(cr, SUPERUSER_ID, {
- 'name': xml_id + '_' + table.replace('.', '_'),
- 'model': table,
- 'module': module,
- 'res_id': inherit_id.id,
- 'noupdate': noupdate,
- },context=context)
- self.create(cr, SUPERUSER_ID, {
+ for parent_model, parent_field in record._inherits.iteritems():
+ 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':res_id,
+ 'module': module,
+ 'res_id': record.id,
'noupdate': noupdate,
- },context=context)
- else:
- if mode=='init' or (mode=='update' and xml_id):
- inherit_xml_ids = []
- if xml_id:
- for table, field_name in model_obj._inherits.items():
- xml_ids = self.pool['ir.model.data'].search(cr, uid, [
- ('module', '=', module),
- ('name', '=', xml_id + '_' + table.replace('.', '_')),
- ], context=context)
- # XML ID found in the database, try to recover an existing record
- if xml_ids:
- found_xml_id = self.pool['ir.model.data'].browse(cr, uid, xml_ids[0], context=context)
- record = self.pool[found_xml_id.model].browse(cr, uid, [found_xml_id.res_id], context=context)[0]
- # The record exists, store the id and don't recreate the XML ID
- if record.exists():
- inherit_xml_ids.append(found_xml_id.model)
- values[field_name] = found_xml_id.res_id
- # Orphan XML ID, delete it
- else:
- found_xml_id.unlink()
-
- res_id = model_obj.create(cr, uid, values, context=context)
- if xml_id:
- if model_obj._inherits:
- for table in model_obj._inherits:
- if table in inherit_xml_ids:
- continue
-
- inherit_id = model_obj.browse(cr, uid,
- res_id,context=context)[model_obj._inherits[table]]
- self.create(cr, SUPERUSER_ID, {
- 'name': xml_id + '_' + table.replace('.', '_'),
- 'model': table,
- 'module': module,
- 'res_id': inherit_id.id,
- 'noupdate': noupdate,
- },context=context)
- self.create(cr, SUPERUSER_ID, {
- 'name': xml_id,
- 'model': model,
+ })
+
+ 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.iteritems():
+ xid = self.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:
+ for parent_model, parent_field in record._inherits.iteritems():
+ if parent_model in existing_parents:
+ continue
+ self.sudo().create({
+ 'name': xml_id + '_' + parent_model.replace('.', '_'),
+ 'model': parent_model,
'module': module,
- 'res_id': res_id,
- 'noupdate': noupdate
- },context=context)
- if xml_id and res_id:
- self.loads[(module, xml_id)] = (model, res_id)
- for table, inherit_field in model_obj._inherits.iteritems():
- inherit_id = model_obj.read(cr, uid, [res_id],
- [inherit_field])[0][inherit_field]
- self.loads[(module, xml_id + '_' + table.replace('.', '_'))] = (table, inherit_id)
- return res_id
+ 'res_id': record[parent_field].id,
+ 'noupdate': noupdate,
+ })
+ self.sudo().create({
+ 'name': xml_id,
+ 'model': model,
+ 'module': module,
+ 'res_id': record.id,
+ 'noupdate': noupdate
+ })
+
+ if xml_id and record:
+ self.loads[(module, xml_id)] = (model, record.id)
+ for parent_model, parent_field in record._inherits.iteritems():
+ parent_xml_id = xml_id + '_' + parent_model.replace('.', '_')
+ self.loads[(module, parent_xml_id)] = (parent_model, record[parent_field].id)
+
+ return record.id
@api.model
def ir_set(self, key, key2, name, models, value, replace=True, isobject=False, meta=None, xml_id=False):
@@ -1138,47 +1089,42 @@ class IrModelData(models.Model):
the chance of gracefully deleting all records.
This step is performed as part of the full uninstallation of a module.
"""
-
- records = self.search([('module', 'in', modules_to_remove)]).ids
-
- if self.env.uid != 1 and not self.env['ir.model.access'].check_groups("base.group_system"):
+ if not (self._uid == SUPERUSER_ID or self.env.user.has_group('base.group_system')):
raise AccessError(_('Administrator access is required to uninstall a module'))
- self.env.context = dict(self.env.context or {})
- self.env.context[MODULE_UNINSTALL_FLAG] = True # enable model/field deletion
+ # enable model/field deletion
+ self = self.with_context(**{MODULE_UNINSTALL_FLAG: True})
- ids_set = set(records)
+ datas = self.search([('module', 'in', modules_to_remove)])
wkf_todo = []
- to_unlink = []
- records.sort()
- records.reverse()
- for data in self:
+ to_unlink = tools.OrderedSet()
+
+ for data in datas.sorted(key='id', reverse=True):
model = data.model
res_id = data.res_id
- pair_to_unlink = (model, res_id)
- if pair_to_unlink not in to_unlink:
- to_unlink.append(pair_to_unlink)
+ to_unlink.add((model, res_id))
if model == 'workflow.activity':
# Special treatment for workflow activities: temporarily revert their
# incoming transition and trigger an update to force all workflow items
# to move out before deleting them
- self.env.cr.execute('select res_type,res_id from wkf_instance where id IN (select inst_id from wkf_workitem where act_id=%s)', (res_id,))
- wkf_todo.extend(self.env.cr.fetchall())
- self.env.cr.execute("update wkf_transition set condition='True', group_id=NULL, signal=NULL,act_to=act_from,act_from=%s where act_to=%s", (res_id, res_id))
+ self._cr.execute('SELECT res_type, res_id FROM wkf_instance WHERE id IN (SELECT inst_id FROM wkf_workitem WHERE act_id=%s)', (res_id,))
+ wkf_todo.extend(self._cr.fetchall())
+ self._cr.execute("UPDATE wkf_transition SET condition='True', group_id=NULL, signal=NULL, act_to=act_from, act_from=%s WHERE act_to=%s", (res_id, res_id))
self.invalidate_cache()
for model, res_id in wkf_todo:
try:
- odoo.workflow.trg_write(self.env.uid, model, res_id, self.env.cr)
+ record = self.env[model].browse(res_id)
+ record.step_workflow()
except Exception:
_logger.info('Unable to force processing of workflow for item %s@%s in order to leave activity to be deleted', res_id, model, exc_info=True)
def unlink_if_refcount(to_unlink):
for model, res_id in to_unlink:
external_ids = self.search([('model', '=', model), ('res_id', '=', res_id)])
- if set(external_ids)-ids_set:
+ if external_ids - datas:
# if other modules have defined this record, we must not delete it
continue
if model == 'ir.model.fields':
@@ -1189,36 +1135,36 @@ class IrModelData(models.Model):
_logger.info('Deleting orphan external_ids %s', external_ids)
external_ids.unlink()
continue
- if field.name in models.LOG_ACCESS_COLUMNS and self.pool[field.model]._log_access:
+ if field.name in models.LOG_ACCESS_COLUMNS and self.env[field.model]._log_access:
continue
if field.name == 'id':
continue
_logger.info('Deleting %s@%s', res_id, model)
try:
- self.env.cr.execute('SAVEPOINT record_unlink_save')
+ self._cr.execute('SAVEPOINT record_unlink_save')
self.env[model].browse(res_id).unlink()
except Exception:
_logger.info('Unable to delete %s@%s', res_id, model, exc_info=True)
- self.env.cr.execute('ROLLBACK TO SAVEPOINT record_unlink_save')
+ self._cr.execute('ROLLBACK TO SAVEPOINT record_unlink_save')
else:
- self.env.cr.execute('RELEASE SAVEPOINT record_unlink_save')
+ self._cr.execute('RELEASE SAVEPOINT record_unlink_save')
# Remove non-model records first, then model fields, and finish with models
- unlink_if_refcount((model, res_id) for model, res_id in to_unlink if model not in ('ir.model', 'ir.model.fields', 'ir.model.constraint'))
- unlink_if_refcount((model, res_id) for model, res_id in to_unlink if model == 'ir.model.constraint')
+ unlink_if_refcount(item for item in to_unlink if item[0] not in ('ir.model', 'ir.model.fields', 'ir.model.constraint'))
+ unlink_if_refcount(item for item in to_unlink if item[0] == 'ir.model.constraint')
- modules_to_remove_ids = self.env['ir.module.module'].search([('name', 'in', modules_to_remove)]).ids
- constraint_ids = self.env['ir.model.constraint'].search([('module', 'in', modules_to_remove_ids)])
- constraint_ids._module_data_uninstall()
+ modules = self.env['ir.module.module'].search([('name', 'in', modules_to_remove)])
+ constraints = self.env['ir.model.constraint'].search([('module', 'in', modules.ids)])
+ constraints._module_data_uninstall()
- unlink_if_refcount((model, res_id) for model, res_id in to_unlink if model == 'ir.model.fields')
+ unlink_if_refcount(item for item in to_unlink if item[0] == 'ir.model.fields')
- relation_ids = self.env['ir.model.relation'].search([('module', 'in', modules_to_remove_ids)])
- relation_ids._module_data_uninstall()
+ relations = self.env['ir.model.relation'].search([('module', 'in', modules.ids)])
+ relations._module_data_uninstall()
- unlink_if_refcount((model, res_id) for model, res_id in to_unlink if model == 'ir.model')
+ unlink_if_refcount(item for item in to_unlink if item[0] == 'ir.model')
- self.env.cr.commit()
+ self._cr.commit()
self.unlink()
@@ -1231,23 +1177,27 @@ class IrModelData(models.Model):
and a module in ir_model_data and noupdate set to false, but not
present in self.loads.
"""
- if not modules or config.get('import_partial'):
+ if not modules or tools.config.get('import_partial'):
return True
bad_imd_ids = []
- self.env.cr.execute("""SELECT id, name, model, res_id, module FROM ir_model_data
- WHERE module IN %s AND res_id IS NOT NULL AND noupdate=%s ORDER BY id DESC
- """, (tuple(modules), False))
- for (rec_id, name, model, res_id, module) in self.env.cr.fetchall():
+ self = self.with_context({MODULE_UNINSTALL_FLAG: True})
+
+ query = """ SELECT id, name, model, res_id, module FROM ir_model_data
+ WHERE module IN %s AND res_id IS NOT NULL AND noupdate=%s ORDER BY id DESC
+ """
+ self._cr.execute(query, (tuple(modules), False))
+ for (id, name, model, res_id, module) in self._cr.fetchall():
if (module, name) not in self.loads:
- if model in self.pool:
+ if model in self.env:
_logger.info('Deleting %s@%s (%s.%s)', res_id, model, module, name)
- if self.pool[model].exists(self.env.cr, self.env.uid, [res_id], context=self.env.context):
- self.pool[model].unlink(self.env.cr, self.env.uid, [res_id], context=self.env.context)
+ record = self.env[model].browse(res_id)
+ if record.exists():
+ record.unlink()
else:
- bad_imd_ids.append(self.id)
+ bad_imd_ids.append(id)
if bad_imd_ids:
- self.browse(bad_imd_ids).with_context({MODULE_UNINSTALL_FLAG: True}).unlink()
+ self.browse(bad_imd_ids).unlink()
self.loads.clear()
@@ -1260,14 +1210,13 @@ class WizardModelMenu(models.TransientModel):
@api.multi
def menu_create(self):
for menu in self:
- model = self.env['ir.model'].browse(self.env.context.get('model_id'))
- val = {
+ model = self.env['ir.model'].browse(self._context.get('model_id'))
+ vals = {
'name': menu.name,
'res_model': model.model,
- 'view_type': 'form',
- 'view_mode': 'tree,form'
+ 'view_mode': 'tree,form',
}
- action_id = self.env['ir.actions.act_window'].create(val)
+ action_id = self.env['ir.actions.act_window'].create(vals)
self.env['ir.ui.menu'].create({
'name': menu.name,
'parent_id': menu.menu_id.id,
diff --git a/openerp/addons/base/ir/ir_needaction.py b/openerp/addons/base/ir/ir_needaction.py
index 765c8bb9da0..a4f97538a8d 100644
--- a/openerp/addons/base/ir/ir_needaction.py
+++ b/openerp/addons/base/ir/ir_needaction.py
@@ -20,7 +20,6 @@ class IrNeedactionMixin(models.AbstractModel):
This class also offers several global services:
- ``_needaction_count``: returns the number of actions uid has to perform
"""
-
_name = 'ir.needaction_mixin'
_needaction = True
diff --git a/openerp/addons/base/ir/ir_qweb.py b/openerp/addons/base/ir/ir_qweb.py
index 7f879a7ec37..c04d6d9bab4 100644
--- a/openerp/addons/base/ir/ir_qweb.py
+++ b/openerp/addons/base/ir/ir_qweb.py
@@ -1,8 +1,4 @@
# -*- coding: utf-8 -*-
-import collections
-import copy
-import cStringIO
-import datetime
import hashlib
import itertools
import json
@@ -13,6 +9,10 @@ import re
import sys
import textwrap
import uuid
+from collections import defaultdict, Mapping, Sized
+from copy import deepcopy
+from cStringIO import StringIO
+from datetime import datetime
from subprocess import Popen, PIPE
from urlparse import urlparse
@@ -23,16 +23,11 @@ import werkzeug
from lxml import etree, html
from PIL import Image
-import odoo.http
-import odoo.tools
-import odoo.tools.lru
-from odoo import api, models, _
+from odoo import api, fields, models, tools, SUPERUSER_ID, _
from odoo.exceptions import QWebException
from odoo.http import request
from odoo.modules.module import get_resource_path
-from odoo.tools import html_escape as escape
-from odoo.tools.func import lazy_property
-from odoo.tools.misc import find_in_path
+from odoo.tools import html_escape as escape, find_in_path, lazy_property, posix_to_ldml
from odoo.tools.safe_eval import safe_eval as eval
_logger = logging.getLogger(__name__)
@@ -54,17 +49,17 @@ def raise_qweb_exception(etype=None, **kw):
try:
raise etype, original, tb
except etype, e:
- for k, v in kw.items():
- e.qweb[k] = v
# Will use `raise foo from bar` in python 3 and rename cause to __cause__
- e.qweb['cause'] = original
+ e.qweb.update(kw, cause=original)
raise
def _build_attribute(name, value):
value = escape(value)
- if isinstance(name, unicode): name = name.encode('utf-8')
- if isinstance(value, unicode): value = value.encode('utf-8')
+ if isinstance(name, unicode):
+ name = name.encode('utf-8')
+ if isinstance(value, unicode):
+ value = value.encode('utf-8')
return ' %s="%s"' % (name, value)
@@ -84,35 +79,35 @@ class FileSystemLoader(object):
for node in self.doc:
if node.get('t-name') == name:
root = etree.Element('templates')
- root.append(copy.deepcopy(node))
+ root.append(deepcopy(node))
arch = etree.tostring(root, encoding='utf-8', xml_declaration=True)
return arch
class QWebContext(dict):
- def __init__(self, cr, uid, data, loader=None, context=None):
- self.cr = cr
- self.uid = uid
+ def __init__(self, env, data, loader=None):
+ self.env = env
self.loader = loader
- self.context = context
- dic = dict(data)
- super(QWebContext, self).__init__(dic)
+ super(QWebContext, self).__init__(data)
self['defined'] = lambda key: key in self
+ # deprecated, use 'env' instead
+ cr = property(lambda self: self.env.cr)
+ uid = property(lambda self: self.env.uid)
+ context = property(lambda self: self.env.context)
+
def safe_eval(self, expr):
- locals_dict = collections.defaultdict(lambda: None)
+ locals_dict = defaultdict(lambda: None)
locals_dict.update(self)
locals_dict.pop('cr', None)
locals_dict.pop('loader', None)
return eval(expr, None, locals_dict, nocopy=True, locals_builtins=True)
def copy(self):
- """ Clones the current context, conserving all data and metadata
+ """ Clone the current context, conserving all data and metadata
(loader, template cache, ...)
"""
- return QWebContext(self.cr, self.uid, dict.copy(self),
- loader=self.loader,
- context=self.context)
+ return QWebContext(self.env, dict(self), loader=self.loader)
def __copy__(self):
return self.copy()
@@ -137,20 +132,13 @@ class QWeb(models.AbstractModel):
u'area', u'base', u'br', u'col', u'embed', u'hr', u'img', u'input',
u'keygen', u'link', u'menuitem', u'meta', u'param', u'source',
u'track', u'wbr'])
- _format_regex = re.compile(
- '(?:'
- # ruby-style pattern
- '#\{(.+?)\}'
- ')|(?:'
- # jinja-style pattern
- '\{\{(.+?)\}\}'
- ')')
+ # regex for ruby-style pattern or jinja-style pattern
+ _format_regex = re.compile(r'(?:#\{(.+?)\})|(?:\{\{(.+?)\}\})')
def __init__(self, pool, cr):
super(QWeb, self).__init__(pool, cr)
-
- self._render_tag = self.prefixed_methods('render_tag_')
- self._render_att = self.prefixed_methods('render_att_')
+ type(self)._render_tag = self.prefixed_methods('render_tag_')
+ type(self)._render_att = self.prefixed_methods('render_att_')
def prefixed_methods(self, prefix):
""" Extracts all methods prefixed by ``prefix``, and returns a mapping
@@ -229,9 +217,9 @@ class QWeb(models.AbstractModel):
def eval_bool(self, expr, qwebcontext):
return int(bool(self.eval(expr, qwebcontext)))
- @api.cr_uid_ids_context
- def render(self, cr, uid, id_or_xml_id, qwebcontext=None, loader=None, context=None):
- """ render(cr, uid, id_or_xml_id, qwebcontext=None, loader=None, context=None)
+ @api.model
+ def render(self, template, qwebcontext=None, loader=None):
+ """ render(template, qwebcontext=None, loader=None)
Renders the template specified by the provided template name
@@ -244,17 +232,16 @@ class QWeb(models.AbstractModel):
qwebcontext = {}
if not isinstance(qwebcontext, QWebContext):
- qwebcontext = QWebContext(cr, uid, qwebcontext, loader=loader, context=context)
+ qwebcontext = QWebContext(self.env, qwebcontext, loader=loader)
- qwebcontext['__template__'] = id_or_xml_id
- stack = qwebcontext.get('__stack__', [])
+ qwebcontext['__template__'] = template
+ stack = qwebcontext.setdefault('__stack__', [])
if stack:
qwebcontext['__caller__'] = stack[-1]
- stack.append(id_or_xml_id)
- qwebcontext['__stack__'] = stack
- qwebcontext['xmlid'] = str(stack[0]) # Temporary fix
+ stack.append(template)
+ qwebcontext['xmlid'] = str(stack[0]) # Temporary fix
- element = self.get_template(id_or_xml_id, qwebcontext)
+ element = self.get_template(template, qwebcontext)
element.attrib.pop("name", False)
return self.render_node(element, qwebcontext, generated_attributes=qwebcontext.pop('generated_attributes', ''))
@@ -264,7 +251,7 @@ class QWeb(models.AbstractModel):
debugger = element.get('t-debug')
if debugger is not None:
- if odoo.tools.config['dev_mode']:
+ if tools.config['dev_mode']:
__import__(debugger).set_trace() # pdb, ipdb, pudb, ...
else:
_logger.warning("@t-debug in template '%s' is only available in --dev mode" % qwebcontext['__template__'])
@@ -272,9 +259,8 @@ class QWeb(models.AbstractModel):
for (attribute_name, attribute_value) in element.attrib.iteritems():
attribute_name = unicode(attribute_name)
if attribute_name == "groups":
- cr = qwebcontext.get('request') and qwebcontext['request'].cr or None
- uid = qwebcontext.get('request') and qwebcontext['request'].uid or None
- can_see = self.user_has_groups(cr, uid, groups=attribute_value) if cr and uid else False
+ can_see = (qwebcontext.cr and qwebcontext.uid and
+ qwebcontext.env['ir.qweb'].user_has_groups(attribute_value))
if not can_see:
return element.tail and self.render_tail(element.tail, element, qwebcontext) or ''
@@ -286,7 +272,8 @@ class QWeb(models.AbstractModel):
attrs = self._render_att[attribute](
self, element, attribute_name, attribute_value, qwebcontext)
for att, val in attrs:
- if not val: continue
+ if not val:
+ continue
generated_attributes += self.render_attribute(element, att, val, qwebcontext)
break
else:
@@ -322,7 +309,7 @@ class QWeb(models.AbstractModel):
for current_node in element.iterchildren(tag=etree.Element):
try:
g_inner.append(self.render_node(current_node, qwebcontext,
- generated_attributes= name == "t" and generated_attributes or ''))
+ generated_attributes=(name == "t" and generated_attributes or '')))
except QWebException:
raise
except Exception:
@@ -366,7 +353,7 @@ class QWeb(models.AbstractModel):
return [(attribute_name[6:], self.eval(attribute_value, qwebcontext))]
result = self.eval_object(attribute_value, qwebcontext)
- if isinstance(result, collections.Mapping):
+ if isinstance(result, Mapping):
return result.iteritems()
# assume tuple
return [result]
@@ -383,7 +370,7 @@ class QWeb(models.AbstractModel):
return self.render_element(element, template_attributes, generated_attributes, qwebcontext, inner)
def _iterate(self, iterable):
- if isinstance(iterable, collections.Mapping):
+ if isinstance(iterable, Mapping):
return iterable.iteritems()
return itertools.izip(*itertools.tee(iterable))
@@ -401,7 +388,7 @@ class QWeb(models.AbstractModel):
copy_qwebcontext = qwebcontext.copy()
size = None
- if isinstance(enum, collections.Sized):
+ if isinstance(enum, Sized):
size = len(enum)
copy_qwebcontext["%s_size" % varname] = size
@@ -444,15 +431,13 @@ class QWeb(models.AbstractModel):
d = qwebcontext.copy()
if 'lang' in template_attributes:
- init_lang = d.context.get('lang', 'en_US')
+ # add 'lang' in the context of d
lang = template_attributes['lang']
- d.context['lang'] = self.eval(lang, d) or lang
- if not self.pool['res.lang'].search(d.cr, d.uid, [('code', '=', lang)], count=True, context=d.context):
+ d.env = d.env(context=dict(d.context, lang=self.eval(lang, d) or lang))
+ if not d.env['res.lang'].search_count([('code', '=', lang)]):
_logger.info("'%s' is not a valid language code, is an empty field or is not installed, falling back to en_US", lang)
d[0] = self.render_element(element, template_attributes, generated_attributes, d)
- cr = d.get('request') and d['request'].cr or None
- uid = d.get('request') and d['request'].uid or None
template = self.eval_format(template_attributes["call"], d)
try:
@@ -461,11 +446,7 @@ class QWeb(models.AbstractModel):
pass
d['generated_attributes'] = generated_attributes
- res = self.render(cr, uid, template, d)
-
- # we need to reset the lang after the rendering
- if 'lang' in template_attributes:
- d.context['lang'] = init_lang
+ res = d.env['ir.qweb'].render(template, d)
return res
@@ -478,8 +459,7 @@ class QWeb(models.AbstractModel):
template = qwebcontext.get('__template__')
raise QWebException("t-call-assets cannot contain children nodes", template=template)
xmlid = template_attributes['call-assets']
- cr, uid, context = [getattr(qwebcontext, attr) for attr in ('cr', 'uid', 'context')]
- bundle = AssetsBundle(xmlid, cr=cr, uid=uid, context=context, registry=self.pool)
+ bundle = AssetsBundle(xmlid, env=qwebcontext.env)
css = self.get_attr_bool(template_attributes.get('css'), default=True)
js = self.get_attr_bool(template_attributes.get('js'), default=True)
async = self.get_attr_bool(template_attributes.get('async'), default=False)
@@ -505,36 +485,41 @@ class QWeb(models.AbstractModel):
record, field_name = template_attributes["field"].rsplit('.', 1)
record = self.eval_object(record, qwebcontext)
- field = record._fields[field_name]
+
foptions = self.eval_format(template_attributes.get('field-options') or '{}', qwebcontext)
options = json.loads(foptions)
-
- field_type = get_field_type(field, options)
+ field_type = get_field_type(record._fields[field_name], options)
converter = self.get_converter_for(field_type)
-
- return converter.to_html(qwebcontext.cr, qwebcontext.uid, field_name, record, options,
- element, template_attributes, generated_attributes, qwebcontext, context=qwebcontext.context)
+ return converter.to_html(field_name, record, options, element,
+ template_attributes, generated_attributes, qwebcontext)
def get_converter_for(self, field_type):
- """ returns a :class:`~openerp.models.Model` used to render a
- ``t-field``.
+ """ Return a model instance to render a ``t-field``. The result is of
+ the same kind (old/new api) as ``self``
By default, tries to get the model named
:samp:`ir.qweb.field.{field_type}`, falling back on ``ir.qweb.field``.
:param str field_type: type or widget of field to render
"""
- return self.pool.get('ir.qweb.field.' + field_type, self.pool['ir.qweb.field'])
+ registry = getattr(self, 'env', self.pool)
+ try:
+ return registry['ir.qweb.field.' + field_type]
+ except KeyError:
+ return registry['ir.qweb.field']
def get_widget_for(self, widget):
- """ returns a :class:`~openerp.models.Model` used to render a
- ``t-esc``
+ """ Return a model instance to render a ``t-esc``. The result is of the
+ same kind (old/new api) as ``self``
:param str widget: name of the widget to use, or ``None``
"""
- widget_model = ('ir.qweb.widget.' + widget) if widget else 'ir.qweb.widget'
- return self.pool.get(widget_model) or self.pool['ir.qweb.widget']
+ registry = getattr(self, 'env', self.pool)
+ try:
+ return registry['ir.qweb.widget.' + (widget or '')]
+ except KeyError:
+ return registry['ir.qweb.widget']
def get_attr_bool(self, attr, default=False):
if attr:
@@ -563,9 +548,8 @@ class FieldConverter(models.AbstractModel):
_name = 'ir.qweb.field'
@api.model
- def attributes(self, field_name, record, options,
- source_element, g_att, t_att, qweb_context):
- """ attributes(cr, uid, field_name, record, options, source_element, g_att, t_att, qweb_context, context=None)
+ def attributes(self, field_name, record, options, source_element, g_att, t_att, qweb_context):
+ """ attributes(field_name, record, options, source_element, g_att, t_att, qweb_context)
Generates the metadata attributes (prefixed by ``data-oe-`` for the
root node of the field conversion. Attribute values are escaped by the
@@ -600,16 +584,15 @@ class FieldConverter(models.AbstractModel):
@api.model
def value_to_html(self, value, field, options=None):
- """ value_to_html(cr, uid, value, field, options=None, context=None)
+ """ value_to_html(value, field, options=None)
Converts a single value to its HTML version/output
"""
- if not value: return ''
- return value
+ return value or ''
@api.model
def record_to_html(self, field_name, record, options=None):
- """ record_to_html(cr, uid, field_name, record, options=None, context=None)
+ """ record_to_html(field_name, record, options=None)
Converts the specified field of the browse_record ``record`` to HTML
"""
@@ -617,9 +600,8 @@ class FieldConverter(models.AbstractModel):
return self.value_to_html(record[field_name], field, options=options)
@api.model
- def to_html(self, field_name, record, options,
- source_element, t_att, g_att, qweb_context):
- """ to_html(cr, uid, field_name, record, options, source_element, t_att, g_att, qweb_context, context=None)
+ def to_html(self, field_name, record, options, source_element, t_att, g_att, qweb_context):
+ """ to_html(field_name, record, options, source_element, t_att, g_att, qweb_context)
Converts a ``t-field`` to its HTML output. A ``t-field`` may be
extended by a ``t-field-options``, which is a JSON-serialized mapping
@@ -639,10 +621,10 @@ class FieldConverter(models.AbstractModel):
field_name, record._name, exc_info=True)
content = None
- inherit_branding = self.env.context and self.env.context.get('inherit_branding')
- if not inherit_branding and self.env.context and self.env.context.get('inherit_branding_auto'):
- inherit_branding = self.env['ir.model.access'].check(record._name, 'write', False)
- translate = self.env.context and self.env.context.get('edit_translations') and self.env.context.get('translatable') and getattr(record._fields[field_name], 'translate', False)
+ inherit_branding = self._context.get('inherit_branding')
+ if not inherit_branding and self._context.get('inherit_branding_auto'):
+ inherit_branding = record.check_access_rights('write', False)
+ translate = self._context.get('edit_translations') and self._context.get('translatable') and getattr(record._fields[field_name], 'translate', False)
if inherit_branding or translate:
# add branding attributes
@@ -653,36 +635,29 @@ class FieldConverter(models.AbstractModel):
source_element, g_att, t_att, qweb_context)
)
- return self.render_element(source_element, t_att, g_att,
- qweb_context, content)
-
- def qweb_object(self):
- return self.pool['ir.qweb']
+ return self.render_element(source_element, t_att, g_att, qweb_context, content)
- def render_element(self, cr, uid, source_element, t_att, g_att,
- qweb_context, content):
- """ render_element(cr, uid, source_element, t_att, g_att, qweb_context, content)
+ @api.model
+ def render_element(self, source_element, t_att, g_att, qweb_context, content):
+ """ render_element(source_element, t_att, g_att, qweb_context, content)
Final rendering hook, by default just calls ir.qweb's ``render_element``
"""
- return self.qweb_object().render_element(
+ return self.env['ir.qweb'].render_element(
source_element, t_att, g_att, qweb_context, content or '')
@api.model
def user_lang(self):
- """ user_lang(cr, uid, context)
+ """ user_lang()
- Fetches the res.lang object corresponding to the language code stored
+ Fetches the res.lang record corresponding to the language code stored
in the user's context. Fallbacks to en_US if no lang is present in the
context *or the language code is not valid*.
:returns: res.lang browse_record
"""
-
- lang_code = self.env.context.get('lang') or 'en_US'
- Lang = self.env['res.lang']
-
- return Lang.browse(Lang._lang_get(lang_code))
+ lang_code = self._context.get('lang') or 'en_US'
+ return self.env['res.lang']._lang_get(lang_code)
class IntegerConverter(models.AbstractModel):
@@ -691,9 +666,7 @@ class IntegerConverter(models.AbstractModel):
@api.model
def value_to_html(self, value, field, options=None):
-
- lang_code = self.env.context.get('lang') or 'en_US'
- return self.pool['res.lang'].format(self.env.cr, self.env.uid, [lang_code], '%d', value, grouping=True)
+ return self.user_lang().format('%d', value, grouping=True)
class FloatConverter(models.AbstractModel):
@@ -709,10 +682,8 @@ class FloatConverter(models.AbstractModel):
def value_to_html(self, value, field, options=None):
precision = self.precision(field, options=options)
fmt = '%f' if precision is None else '%.{precision}f'
-
- lang_code = self.env.context.get('lang') or 'en_US'
- lang = self.pool['res.lang']
- formatted = lang.format(self.env.cr, self.env.uid, [lang_code], fmt.format(precision=precision), value, grouping=True)
+ lang = self.user_lang()
+ formatted = lang.format(fmt.format(precision=precision), value, grouping=True)
# %f does not strip trailing zeroes. %g does but its precision causes
# it to switch to scientific notation starting at a million *and* to
@@ -729,23 +700,21 @@ class DateConverter(models.AbstractModel):
@api.model
def value_to_html(self, value, field, options=None):
- if not value or len(value) < 10: return ''
+ if not value or len(value) < 10:
+ return ''
lang = self.user_lang()
locale = babel.Locale.parse(lang.code)
if isinstance(value, basestring):
- value = datetime.datetime.strptime(
- value[:10], odoo.tools.DEFAULT_SERVER_DATE_FORMAT)
+ value = fields.Datetime.from_string(value[:10])
if options and 'format' in options:
pattern = options['format']
else:
strftime_pattern = lang.date_format
- pattern = odoo.tools.posix_to_ldml(strftime_pattern, locale=locale)
+ pattern = posix_to_ldml(strftime_pattern, locale=locale)
- return babel.dates.format_date(
- value, format=pattern,
- locale=locale)
+ return babel.dates.format_date(value, format=pattern, locale=locale)
class DateTimeConverter(models.AbstractModel):
@@ -754,21 +723,21 @@ class DateTimeConverter(models.AbstractModel):
@api.model
def value_to_html(self, value, field, options=None):
- if not value: return ''
+ if not value:
+ return ''
lang = self.user_lang()
locale = babel.Locale.parse(lang.code)
if isinstance(value, basestring):
- value = datetime.datetime.strptime(
- value, odoo.tools.DEFAULT_SERVER_DATETIME_FORMAT)
+ value = fields.Datetime.from_string(value)
- value = odoo.osv.fields.datetime.context_timestamp(self.env.cr, self.env.uid, timestamp=value, context=self.env.context)
+ value = fields.Datetime.context_timestamp(self, value)
if options and 'format' in options:
pattern = options['format']
else:
strftime_pattern = (u"%s %s" % (lang.date_format, lang.time_format))
- pattern = odoo.tools.posix_to_ldml(strftime_pattern, locale=locale)
+ pattern = posix_to_ldml(strftime_pattern, locale=locale)
if options and options.get('hide_seconds'):
pattern = pattern.replace(":ss", "").replace(":s", "")
@@ -785,9 +754,7 @@ class TextConverter(models.AbstractModel):
"""
Escapes the value and converts newlines to br. This is bullshit.
"""
- if not value: return ''
-
- return nl2br(value, options=options)
+ return nl2br(value, options=options) if value else ''
class SelectionConverter(models.AbstractModel):
@@ -797,7 +764,8 @@ class SelectionConverter(models.AbstractModel):
@api.model
def record_to_html(self, field_name, record, options=None):
value = record[field_name]
- if not value: return ''
+ if not value:
+ return ''
field = record._fields[field_name]
selection = dict(field.get_description(record.env)['selection'])
return self.value_to_html(selection[value], field, options=options)
@@ -809,10 +777,8 @@ class ManyToOneConverter(models.AbstractModel):
@api.model
def record_to_html(self, field_name, record, options=None):
- [read] = record.read([field_name])
- if not read[field_name]: return ''
- _, value = read[field_name]
- return nl2br(value, options=options)
+ value = record[field_name].sudo().display_name
+ return nl2br(value, options=options) if value else ''
class HTMLConverter(models.AbstractModel):
@@ -839,11 +805,11 @@ class ImageConverter(models.AbstractModel):
@api.model
def value_to_html(self, value, field, options=None):
try:
- image = Image.open(cStringIO.StringIO(value.decode('base64')))
+ image = Image.open(StringIO(value.decode('base64')))
image.verify()
except IOError:
raise ValueError("Non-image binary fields can not be converted to HTML")
- except: # image.verify() throws "suitable exceptions", I have no idea what they are
+ except: # image.verify() throws "suitable exceptions", I have no idea what they are
raise ValueError("Invalid image content")
return HTMLSafe('<img src="data:%s;base64,%s">' % (Image.MIME[image.format], value))
@@ -867,8 +833,7 @@ class MonetaryConverter(models.AbstractModel):
_inherit = 'ir.qweb.field'
@api.model
- def to_html(self, field_name, record, options,
- source_element, t_att, g_att, qweb_context):
+ def to_html(self, field_name, record, options, source_element, t_att, g_att, qweb_context):
options['_qweb_context'] = qweb_context
return super(MonetaryConverter, self).to_html(
field_name, record, options,
@@ -876,7 +841,6 @@ class MonetaryConverter(models.AbstractModel):
@api.model
def record_to_html(self, field_name, record, options):
- Currency = self.pool['res.currency']
cur_field = record._fields[field_name]
display_currency = False
#currency should be specified by monetary field
@@ -897,12 +861,11 @@ class MonetaryConverter(models.AbstractModel):
if options.get('from_currency'):
from_currency = self.display_currency(options['from_currency'], options)
- from_amount = Currency.compute(self.env.cr, self.env.uid, from_currency.id, display_currency.id, from_amount)
+ from_amount = from_currency.compute(from_amount, display_currency)
- lang_code = self.env.context.get('lang') or 'en_US'
- lang = self.pool['res.lang']
- formatted_amount = lang.format(self.env.cr, self.env.uid, [lang_code],
- fmt, Currency.round(self.env.cr, self.env.uid, display_currency, from_amount), grouping=True, monetary=True)
+ lang = self.user_lang()
+ formatted_amount = lang.format(fmt, display_currency.round(from_amount),
+ grouping=True, monetary=True)
pre = post = u''
if display_currency.position == 'before':
@@ -911,14 +874,15 @@ class MonetaryConverter(models.AbstractModel):
post = u'\N{NO-BREAK SPACE}{symbol}'
return HTMLSafe(u'{pre}<span class="oe_currency_value">{0}</span>{post}'.format(
- formatted_amount,
- pre=pre, post=post,
+ formatted_amount, pre=pre, post=post,
).format(
symbol=display_currency.symbol,
))
+ @api.model
def display_currency(self, currency, options):
- return self.qweb_object().eval_object(currency, options['_qweb_context'])
+ return self.env['ir.qweb'].eval_object(currency, options['_qweb_context'])
+
TIMEDELTA_UNITS = (
('year', 3600 * 24 * 365),
@@ -961,7 +925,8 @@ class DurationConverter(models.AbstractModel):
r = value * factor
for unit, secs_per_unit in TIMEDELTA_UNITS:
v, r = divmod(r, secs_per_unit)
- if not v: continue
+ if not v:
+ continue
section = babel.dates.format_timedelta(
v*secs_per_unit, threshold=1, locale=locale)
if section:
@@ -975,14 +940,13 @@ class RelativeDatetimeConverter(models.AbstractModel):
@api.model
def value_to_html(self, value, field, options=None):
- parse_format = odoo.tools.DEFAULT_SERVER_DATETIME_FORMAT
locale = babel.Locale.parse(self.user_lang().code)
if isinstance(value, basestring):
- value = datetime.datetime.strptime(value, parse_format)
+ value = fields.Datetime.from_string(value)
# value should be a naive datetime in UTC. So is fields.Datetime.now()
- reference = datetime.datetime.strptime(field.now(), parse_format)
+ reference = fields.Datetime.from_string(field.now())
return babel.dates.format_timedelta(value - reference, add_direction=True, locale=locale)
@@ -1018,7 +982,8 @@ class Contact(models.AbstractModel):
'options': options
}
- html = self.env['ir.ui.view']._model.render(self.env.cr, self.env.uid, "base.contact", val, engine='ir.qweb', context=self.env.context).decode('utf8')
+ view = self.env.ref('base.contact')
+ html = view.render(val, engine='ir.qweb').decode('utf8')
return HTMLSafe(html)
@@ -1034,13 +999,12 @@ class QwebView(models.AbstractModel):
view = getattr(record, field_name)
- if view._model._name != "ir.ui.view":
+ if view._name != "ir.ui.view":
_logger.warning("%s.%s must be a 'ir.ui.view' model." % (record, field_name))
return None
- ctx = (self.env.context or {}).copy()
- ctx['object'] = record
- html = view._model.render(self.env.cr, self.env.uid, ctx, engine='ir.qweb', context=self.env.ctx).decode('utf8')
+ view = view.with_context(object=record)
+ html = view.render(view._context, engine='ir.qweb').decode('utf8')
return HTMLSafe(html)
@@ -1060,14 +1024,13 @@ class QwebWidgetMonetary(models.AbstractModel):
_inherit = 'ir.qweb.widget'
def _format(self, inner, options, qwebcontext):
- inner = self.pool['ir.qweb'].eval(inner, qwebcontext)
- display = self.pool['ir.qweb'].eval_object(options['display_currency'], qwebcontext)
+ inner = qwebcontext.env['ir.qweb'].eval(inner, qwebcontext)
+ display = qwebcontext.env['ir.qweb'].eval_object(options['display_currency'], qwebcontext)
precision = int(round(math.log10(display.rounding)))
fmt = "%.{0}f".format(-precision if precision < 0 else 0)
lang_code = qwebcontext.context.get('lang') or 'en_US'
- formatted_amount = self.pool['res.lang'].format(
- qwebcontext.cr, qwebcontext.uid, [lang_code], fmt, inner, grouping=True, monetary=True
- )
+ lang = qwebcontext.env['res.lang']._lang_get(lang_code)
+ formatted_amount = lang.format(fmt, inner, grouping=True, monetary=True)
pre = post = u''
if display.position == 'before':
pre = u'{symbol}\N{NO-BREAK SPACE}'
@@ -1116,7 +1079,8 @@ def nl2br(string, options=None):
:param dict options:
:rtype: HTMLSafe
"""
- if options is None: options = {}
+ if options is None:
+ options = {}
if options.get('html-escape', True):
string = escape(string)
@@ -1141,12 +1105,9 @@ class AssetsBundle(object):
rx_preprocess_imports = re.compile("""(@import\s?['"]([^'"]+)['"](;?))""")
rx_css_split = re.compile("\/\*\! ([a-f0-9-]+) \*\/")
- def __init__(self, xmlid, cr=None, uid=None, context=None, registry=None, max_css_rules=MAX_CSS_RULES):
+ def __init__(self, xmlid, env=None, max_css_rules=MAX_CSS_RULES):
self.xmlid = xmlid
- self.cr = request.cr if cr is None else cr
- self.uid = request.uid if uid is None else uid
- self.context = request.context if context is None else context
- self.registry = request.registry if registry is None else registry
+ self.env = request.env if env is None else env
self.max_css_rules = max_css_rules
self.javascripts = []
self.stylesheets = []
@@ -1154,11 +1115,11 @@ class AssetsBundle(object):
self.remains = []
self._checksum = None
- context = self.context.copy()
- context['inherit_branding'] = False
- context['inherit_branding_auto'] = False
- context['rendering_bundle'] = True
- self.html = self.registry['ir.ui.view'].render(self.cr, self.uid, xmlid, context=context)
+ env = self.env(context=dict(self.env.context,
+ inherit_branding=False,
+ inherit_branding_auto=False,
+ rendering_bundle=True))
+ self.html = env.ref(xmlid).render()
self.parse()
def parse(self):
@@ -1219,13 +1180,13 @@ class AssetsBundle(object):
response.append(jscript.to_html())
else:
if qwebcontext is None:
- qwebcontext = QWebContext(self.cr, self.uid, {})
+ qwebcontext = QWebContext(self.env(context={}), {})
if css and self.stylesheets:
css_attachments = self.css()
if not self.css_errors:
for attachment in css_attachments:
el = etree.fromstring('<link href="%s" rel="stylesheet"/>' % attachment.url)
- response.append(self.registry['ir.qweb'].render_node(el, qwebcontext))
+ response.append(self.env['ir.qweb'].render_node(el, qwebcontext))
else:
msg = '\n'.join(self.css_errors)
self.stylesheets.append(StylesheetAsset(self, inline=self.css_message(msg)))
@@ -1233,7 +1194,7 @@ class AssetsBundle(object):
response.append(style.to_html())
if js and self.javascripts:
el = etree.fromstring('<script %s type="text/javascript" src="%s"></script>' % (async and 'async="async"' or '', self.js().url))
- response.append(self.registry['ir.qweb'].render_node(el, qwebcontext))
+ response.append(self.env['ir.qweb'].render_node(el, qwebcontext))
response.extend(self.remains)
return sep + sep.join(response)
@@ -1272,22 +1233,20 @@ class AssetsBundle(object):
of an ir.attachment unlink (because we cannot rollback a removal on the filestore), thus we
must exclude the current bundle.
"""
- ira = self.registry['ir.attachment']
+ ira = self.env['ir.attachment']
domain = [
('url', '=like', '/web/content/%-%/{0}%.{1}'.format(self.xmlid, type)), # The wilcards are id, version and pagination number (if any)
'!', ('url', '=like', '/web/content/%-{}/%'.format(self.version))
]
- attachment_ids = ira.search(self.cr, odoo.SUPERUSER_ID, domain, context=self.context)
- return ira.unlink(self.cr, odoo.SUPERUSER_ID, attachment_ids, context=self.context)
+ return ira.sudo().search(domain).unlink()
def get_attachments(self, type, inc=None):
- ira = self.registry['ir.attachment']
+ ira = self.env['ir.attachment']
domain = [('url', '=like', '/web/content/%%-%s/%s%s.%s' % (self.version, self.xmlid, ('%%' if inc is None else '.%s' % inc), type))]
- attachment_ids = ira.search(self.cr, odoo.SUPERUSER_ID, domain, order='name asc', context=self.context)
- return ira.browse(self.cr, odoo.SUPERUSER_ID, attachment_ids, context=self.context)
+ return ira.sudo().search(domain, order='name asc')
def save_attachment(self, type, content, inc=None):
- ira = self.registry['ir.attachment']
+ ira = self.env['ir.attachment']
fname = '%s%s.%s' % (self.xmlid, ('' if inc is None else '.%s' % inc), type)
values = {
@@ -1299,21 +1258,21 @@ class AssetsBundle(object):
'public': True,
'datas': content.encode('utf8').encode('base64'),
}
- attachment_id = ira.create(self.cr, odoo.SUPERUSER_ID, values, context=self.context)
+ attachment = ira.sudo().create(values)
- url = '/web/content/%s-%s/%s' % (attachment_id, self.version, fname)
+ url = '/web/content/%s-%s/%s' % (attachment.id, self.version, fname)
values = {
'name': url,
'url': url,
}
- ira.write(self.cr, odoo.SUPERUSER_ID, attachment_id, values, context=self.context)
+ attachment.write(values)
- if self.context.get('commit_assetsbundle') is True:
- self.cr.commit()
+ if self.env.context.get('commit_assetsbundle') is True:
+ self.env.cr.commit()
self.clean_attachments(type)
- return ira.browse(self.cr, odoo.SUPERUSER_ID, attachment_id, context=self.context)
+ return attachment
def js(self):
attachments = self.get_attachments('js')
@@ -1371,29 +1330,27 @@ class AssetsBundle(object):
""" % message
def is_css_preprocessed(self):
- uid = openerp.SUPERUSER_ID
preprocessed = True
for atype in (SassStylesheetAsset, LessStylesheetAsset):
outdated = False
assets = dict((asset.html_url % asset.url, asset) for asset in self.stylesheets if isinstance(asset, atype))
if assets:
assets_domain = [('url', 'in', assets.keys())]
- ira_ids = self.registry['ir.attachment'].search(self.cr, uid, assets_domain)
- ira_records = self.registry['ir.attachment'].browse(self.cr, uid, ira_ids)
- for ira_record in ira_records:
- asset = assets[ira_record.url]
- if asset.last_modified > Datetime.from_string(ira_record['__last_update']):
+ attachments = self.env['ir.attachment'].sudo().search(assets_domain)
+ for attachment in attachments:
+ asset = assets[attachment.url]
+ if asset.last_modified > fields.Datetime.from_string(attachment['__last_update']):
outdated = True
break
if asset._content is None:
- asset._content = ira_record.datas and ira_record.datas.decode('base64').decode('utf8')
+ asset._content = attachment.datas and attachment.datas.decode('base64').decode('utf8')
if any(asset._content is None for asset in assets.itervalues()):
outdated = True
if outdated:
- if ira_ids:
- self.registry['ir.attachment'].unlink(self.cr, uid, ira_ids)
+ if attachments:
+ attachments.unlink()
preprocessed = False
return preprocessed
@@ -1422,11 +1379,10 @@ class AssetsBundle(object):
if debug:
try:
- ira = self.registry['ir.attachment']
fname = os.path.basename(asset.url)
url = asset.html_url % asset.url
- with self.cr.savepoint():
- ira.create(self.cr, openerp.SUPERUSER_ID, dict(
+ with self.env.cr.savepoint():
+ self.env['ir.attachment'].sudo().create(dict(
datas=asset.content.encode('utf8').encode('base64'),
mimetype='text/css',
type='binary',
@@ -1435,10 +1391,10 @@ class AssetsBundle(object):
datas_fname=fname,
res_model=False,
res_id=False,
- ), context=self.context)
+ ))
- if self.context.get('commit_assetsbundle') is True:
- self.cr.commit()
+ if self.env.context.get('commit_assetsbundle') is True:
+ self.env.cr.commit()
except psycopg2.Error:
pass
@@ -1497,10 +1453,7 @@ class WebAsset(object):
self.bundle = bundle
self.inline = inline
self.url = url
- self.cr = bundle.cr
- self.uid = bundle.uid
- self.registry = bundle.registry
- self.context = bundle.context
+ self.env = bundle.env
self._content = None
self._filename = None
self._ir_attach = None
@@ -1519,8 +1472,7 @@ class WebAsset(object):
# Test url against ir.attachments
fields = ['__last_update', 'datas', 'mimetype']
domain = [('type', '=', 'binary'), ('url', '=', self.url)]
- ira = self.registry['ir.attachment']
- attach = ira.search_read(self.cr, odoo.SUPERUSER_ID, domain, fields, context=self.context)
+ attach = self.env['ir.attachment'].sudo().search_read(domain, fields)
self._ir_attach = attach[0]
except Exception:
raise AssetNotFound("Could not find %s" % self.name)
@@ -1533,17 +1485,17 @@ class WebAsset(object):
try:
self.stat()
if self._filename:
- return datetime.datetime.fromtimestamp(os.path.getmtime(self._filename))
+ return datetime.fromtimestamp(os.path.getmtime(self._filename))
elif self._ir_attach:
- server_format = odoo.tools.misc.DEFAULT_SERVER_DATETIME_FORMAT
+ server_format = tools.DEFAULT_SERVER_DATETIME_FORMAT
last_update = self._ir_attach['__last_update']
try:
- return datetime.datetime.strptime(last_update, server_format + '.%f')
+ return datetime.strptime(last_update, server_format + '.%f')
except ValueError:
- return datetime.datetime.strptime(last_update, server_format)
+ return datetime.strptime(last_update, server_format)
except Exception:
pass
- return datetime.datetime(1970, 1, 1)
+ return datetime(1970, 1, 1)
@property
def content(self):
diff --git a/openerp/addons/base/ir/ir_rule.py b/openerp/addons/base/ir/ir_rule.py
index 6a475cf8dcf..5e571c1347b 100644
--- a/openerp/addons/base/ir/ir_rule.py
+++ b/openerp/addons/base/ir/ir_rule.py
@@ -1,45 +1,34 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import time
+from collections import defaultdict
-import odoo
-from odoo import api, fields, models, _
-from odoo import SUPERUSER_ID
-from odoo import tools
+from odoo import api, fields, models, tools, SUPERUSER_ID, _
+from odoo.exceptions import ValidationError
from odoo.osv import expression
from odoo.tools.safe_eval import safe_eval as eval
-from odoo.tools.misc import unquote as unquote
class IrRule(models.Model):
_name = 'ir.rule'
- _order = 'name, model_id DESC'
+ _order = 'model_id DESC'
_MODES = ['read', 'write', 'create', 'unlink']
- def _get_value(self, cr, uid, ids, field_name, arg, context=None):
- res = {}
- for rule in self.browse(cr, uid, ids, context):
- if not rule.groups:
- res[rule.id] = True
- else:
- res[rule.id] = False
- return res
-
- _columns = {'global': odoo.osv.fields.function(_get_value, string='Global', type='boolean', store=True, help="If no group is specified the rule is global and applied to everyone")}
-
name = fields.Char(index=True)
active = fields.Boolean(default=True, help="If you uncheck the active field, it will disable the record rule without deleting it (if you delete a native record rule, it may be re-created when you reload the module).")
model_id = fields.Many2one('ir.model', string='Object', index=True, required=True, ondelete="cascade")
groups = fields.Many2many('res.groups', 'rule_group_rel', 'rule_group_id', 'group_id')
domain_force = fields.Text(string='Domain')
- domain = fields.Binary(compute='_domain_force_get', string='Domain')
+ domain = fields.Binary(compute='_force_domain', string='Domain')
perm_read = fields.Boolean(string='Apply for Read', default=True)
perm_write = fields.Boolean(string='Apply for Write', default=True)
perm_create = fields.Boolean(string='Apply for Create', default=True)
perm_unlink = fields.Boolean(string='Apply for Delete', default=True)
_sql_constraints = [
- ('no_access_rights', 'CHECK (perm_read!=False or perm_write!=False or perm_create!=False or perm_unlink!=False)', 'Rule must have at least one checked access right !'),
+ ('no_access_rights',
+ 'CHECK (perm_read!=False or perm_write!=False or perm_create!=False or perm_unlink!=False)',
+ 'Rule must have at least one checked access right !'),
]
def _eval_context_for_combinations(self):
@@ -47,8 +36,8 @@ class IrRule(models.Model):
ir.rule domains, when the goal is to obtain python lists
that are easier to parse and combine, but not to
actually execute them."""
- return {'user': unquote('user'),
- 'time': unquote('time')}
+ return {'user': tools.unquote('user'),
+ 'time': tools.unquote('time')}
@api.model
def _eval_context(self):
@@ -56,8 +45,8 @@ class IrRule(models.Model):
ir.rule domains."""
return {'user': self.env.user, 'time': time}
- @api.depends('domain')
- def _domain_force_get(self):
+ @api.depends('domain_force')
+ def _force_domain(self):
eval_context = self._eval_context()
for rule in self:
if rule.domain_force:
@@ -65,56 +54,66 @@ class IrRule(models.Model):
else:
rule.domain = []
+ @api.depends('groups')
+ def _compute_global(self):
+ for rule in self:
+ rule['global'] = not rule.groups
+
@api.constrains('model_id')
- def _check_model_obj(self):
- if any(self.pool[rule.model_id.model].is_transient() for rule in self):
- raise UserWarning(_('Rules can not be applied on Transient models.'))
+ def _check_model_transience(self):
+ if any(self.env[rule.model_id.model].is_transient() for rule in self):
+ raise ValidationError(_('Rules can not be applied on Transient models.'))
@api.constrains('model_id')
def _check_model_name(self):
# Don't allow rules on rules records (this model).
if any(rule.model_id.model == self._name for rule in self):
- raise UserWarning(_('Rules can not be applied on the Record Rules model.'))
+ raise ValidationError(_('Rules can not be applied on the Record Rules model.'))
- @tools.ormcache('uid', 'model_name', 'mode')
- def _compute_domain(self, cr, uid, model_name, mode="read"):
+ @api.model
+ @tools.ormcache('self._uid', 'model_name', 'mode')
+ def _compute_domain(self, model_name, mode="read"):
if mode not in self._MODES:
raise ValueError('Invalid mode: %r' % (mode,))
- if uid == SUPERUSER_ID:
+ if self._uid == SUPERUSER_ID:
return None
- cr.execute("""SELECT r.id
- FROM ir_rule r
- JOIN ir_model m ON (r.model_id = m.id)
- WHERE m.model = %s
- AND r.active is True
- AND r.perm_""" + mode + """
- AND (r.id IN (SELECT rule_group_id FROM rule_group_rel g_rel
- JOIN res_groups_users_rel u_rel ON (g_rel.group_id = u_rel.gid)
- WHERE u_rel.uid = %s) OR r.global)""", (model_name, uid))
- rule_ids = [x[0] for x in cr.fetchall()]
- if rule_ids:
- # browse user as super-admin root to avoid access errors!
- user = self.pool.get('res.users').browse(cr, SUPERUSER_ID, uid)
- global_domains = [] # list of domains
- group_domains = {} # map: group -> list of domains
- for rule in self.browse(cr, SUPERUSER_ID, rule_ids):
- # read 'domain' as UID to have the correct eval context for the rule.
- rule_domain = self.read(cr, uid, [rule.id], ['domain'])[0]['domain']
- dom = expression.normalize_domain(rule_domain)
- for group in rule.groups:
- if group in user.groups_id:
- group_domains.setdefault(group, []).append(dom)
- if not rule.groups:
- global_domains.append(dom)
- # combine global domains and group domains
- if group_domains:
- group_domain = expression.OR(map(expression.OR, group_domains.values()))
- else:
- group_domain = []
- domain = expression.AND(global_domains + [group_domain])
- return domain
- return []
+
+ query = """ SELECT r.id FROM ir_rule r JOIN ir_model m ON (r.model_id=m.id)
+ WHERE m.model=%s AND r.active AND r.perm_{mode}
+ AND (r.id IN (SELECT rule_group_id FROM rule_group_rel rg
+ JOIN res_groups_users_rel gu ON (rg.group_id=gu.gid)
+ WHERE gu.uid=%s)
+ OR r.global)
+ """.format(mode=mode)
+ self._cr.execute(query, (model_name, self._uid))
+ rule_ids = [row[0] for row in self._cr.fetchall()]
+ if not rule_ids:
+ return []
+
+ # read 'domain' as self._uid to have the correct eval context for the rules.
+ rules = self.browse(rule_ids)
+ rule_domain = {vals['id']: vals['domain'] for vals in rules.read(['domain'])}
+
+ # browse user and rules as SUPERUSER_ID to avoid access errors!
+ user = self.env.user
+ global_domains = [] # list of domains
+ group_domains = defaultdict(list) # {group: list of domains}
+ for rule in rules.sudo():
+ dom = expression.normalize_domain(rule_domain[rule.id])
+ for group in rule.groups:
+ if group in user.groups_id:
+ group_domains[group].append(dom)
+ if not rule.groups:
+ global_domains.append(dom)
+
+ # combine global domains and group domains
+ if group_domains:
+ group_domain = expression.OR(map(expression.OR, group_domains.values()))
+ else:
+ group_domain = []
+ domain = expression.AND(global_domains + [group_domain])
+ return domain
@api.model
def clear_cache(self):
@@ -131,7 +130,7 @@ class IrRule(models.Model):
# references another object the user can't see).
query = self.env[model_name].sudo()._where_calc(dom, active_test=False)
return query.where_clause, query.where_clause_params, query.tables
- return [], [], ['"' + self.env[model_name]._table + '"']
+ return [], [], ['"%s"' % self.env[model_name]._table]
@api.multi
def unlink(self):
@@ -150,3 +149,12 @@ class IrRule(models.Model):
res = super(IrRule, self).write(vals)
self.clear_caches()
return res
+
+#
+# Hack for field 'global': this field cannot be defined like others, because
+# 'global' is a Python keyword. Therefore, we add it to the class by assignment.
+# Note that the attribute '_module' is normally added by the class' metaclass.
+#
+setattr(IrRule, 'global',
+ fields.Boolean(compute='_compute_global', store=True, _module=IrRule._module,
+ help="If no group is specified the rule is global and applied to everyone"))
diff --git a/openerp/addons/base/ir/ir_sequence.py b/openerp/addons/base/ir/ir_sequence.py
index ea139b4cb3a..a6997836c19 100644
--- a/openerp/addons/base/ir/ir_sequence.py
+++ b/openerp/addons/base/ir/ir_sequence.py
@@ -11,38 +11,26 @@ _logger = logging.getLogger(__name__)
def _create_sequence(cr, seq_name, number_increment, number_next):
- """ Create a PostreSQL sequence.
-
- There is no access rights check.
- """
+ """ Create a PostreSQL sequence. """
if number_increment == 0:
raise UserError(_('Step must not be zero.'))
sql = "CREATE SEQUENCE %s INCREMENT BY %%s START WITH %%s" % seq_name
cr.execute(sql, (number_increment, number_next))
-def _drop_sequence(cr, seq_names):
- """ Drop the PostreSQL sequence if it exists.
-
- There is no access rights check.
- """
- names = []
- for n in seq_names:
- names.append(n)
- names = ','.join(names)
+def _drop_sequences(cr, seq_names):
+ """ Drop the PostreSQL sequences if they exist. """
+ names = ','.join(seq_names)
# RESTRICT is the default; it prevents dropping the sequence if an
# object depends on it.
cr.execute("DROP SEQUENCE IF EXISTS %s RESTRICT " % names)
def _alter_sequence(cr, seq_name, number_increment=None, number_next=None):
- """ Alter a PostreSQL sequence.
-
- There is no access rights check.
- """
+ """ Alter a PostreSQL sequence. """
if number_increment == 0:
raise UserError(_("Step must not be zero."))
- cr.execute("SELECT relname FROM pg_class WHERE relkind = %s AND relname=%s", ('S', seq_name))
+ cr.execute("SELECT relname FROM pg_class WHERE relkind=%s AND relname=%s", ('S', seq_name))
if not cr.fetchone():
# sequence is not created yet, we're inside create() so ignore it, will be set later
return
@@ -61,8 +49,8 @@ def _select_nextval(cr, seq_name):
def _update_nogap(self, number_increment):
number_next = self.number_next
- self.env.cr.execute("SELECT number_next FROM %s WHERE id=%s FOR UPDATE NOWAIT" % (self._table, self.id))
- self.env.cr.execute("UPDATE %s SET number_next=number_next+%s WHERE id=%s " % (self._table, number_increment, self.id))
+ self._cr.execute("SELECT number_next FROM %s WHERE id=%s FOR UPDATE NOWAIT" % (self._table, self.id))
+ self._cr.execute("UPDATE %s SET number_next=number_next+%s WHERE id=%s " % (self._table, number_increment, self.id))
self.invalidate_cache(['number_next'], [self.id])
return number_next
@@ -81,26 +69,26 @@ class IrSequence(models.Model):
def _get_number_next_actual(self):
'''Return number from ir_sequence row when no_gap implementation,
and number from postgres sequence when standard implementation.'''
- for element in self:
- if element.implementation != 'standard':
- element.number_next_actual = element.number_next
+ for seq in self:
+ if seq.implementation != 'standard':
+ seq.number_next_actual = seq.number_next
else:
# get number from postgres sequence. Cannot use currval, because that might give an error when
# not having used nextval before.
- query = "SELECT last_value, increment_by, is_called FROM ir_sequence_%03d" % element.id
- self.env.cr.execute(query)
- (last_value, increment_by, is_called) = self.env.cr.fetchone()
+ query = "SELECT last_value, increment_by, is_called FROM ir_sequence_%03d" % seq.id
+ self._cr.execute(query)
+ (last_value, increment_by, is_called) = self._cr.fetchone()
if is_called:
- element.number_next_actual = last_value + increment_by
+ seq.number_next_actual = last_value + increment_by
else:
- element.number_next_actual = last_value
+ seq.number_next_actual = last_value
def _set_number_next_actual(self):
- for record in self:
- record.write({'number_next': record.number_next_actual or 0})
+ for seq in self:
+ seq.write({'number_next': seq.number_next_actual or 0})
name = fields.Char(required=True)
- code = fields.Char(strring='Sequence Code')
+ code = fields.Char(string='Sequence Code')
implementation = fields.Selection([('standard', 'Standard'), ('no_gap', 'No gap')],
string='Implementation', required=True, default='standard',
help="Two sequence object implementations are offered: Standard "
@@ -124,18 +112,20 @@ class IrSequence(models.Model):
use_date_range = fields.Boolean(string='Use subsequences per date_range')
date_range_ids = fields.One2many('ir.sequence.date_range', 'sequence_id', string='Subsequences')
- def init(self, cr):
+ @api.model_cr
+ def init(self):
return # Don't do the following index yet.
+
# CONSTRAINT/UNIQUE INDEX on (code, company_id)
# /!\ The unique constraint 'unique_name_company_id' is not sufficient, because SQL92
# only support field names in constraint definitions, and we need a function here:
# we need to special-case company_id to treat all NULL company_id as equal, otherwise
# we would allow duplicate (code, NULL) ir_sequences.
- self.env.cr.execute("""
+ self._cr.execute("""
SELECT indexname FROM pg_indexes WHERE indexname =
'ir_sequence_unique_code_company_id_idx'""")
- if not self.env.cr.fetchone():
- self.env.cr.execute("""
+ if not self._cr.fetchone():
+ self._cr.execute("""
CREATE UNIQUE INDEX ir_sequence_unique_code_company_id_idx
ON ir_sequence (code, (COALESCE(company_id,-1)))""")
@@ -145,12 +135,12 @@ class IrSequence(models.Model):
"""
seq = super(IrSequence, self).create(values)
if values.get('implementation', 'standard') == 'standard':
- _create_sequence(self.env.cr, "ir_sequence_%03d" % seq.id, values.get('number_increment', 1), values.get('number_next', 1))
+ _create_sequence(self._cr, "ir_sequence_%03d" % seq.id, values.get('number_increment', 1), values.get('number_next', 1))
return seq
@api.multi
def unlink(self):
- _drop_sequence(self.env.cr, ["ir_sequence_%03d" % x.id for x in self])
+ _drop_sequences(self._cr, ["ir_sequence_%03d" % x.id for x in self])
return super(IrSequence, self).unlink()
@api.multi
@@ -165,52 +155,50 @@ class IrSequence(models.Model):
# Implementation has NOT changed.
# Only change sequence if really requested.
if values.get('number_next'):
- _alter_sequence(self.env.cr, "ir_sequence_%03d" % seq.id, number_next=n)
+ _alter_sequence(self._cr, "ir_sequence_%03d" % seq.id, number_next=n)
if seq.number_increment != i:
- _alter_sequence(self.env.cr, "ir_sequence_%03d" % seq.id, number_increment=i)
+ _alter_sequence(self._cr, "ir_sequence_%03d" % seq.id, number_increment=i)
seq.date_range_ids._alter_sequence(number_increment=i)
else:
- _drop_sequence(self.env.cr, ["ir_sequence_%03d" % seq.id])
+ _drop_sequences(self._cr, ["ir_sequence_%03d" % seq.id])
for sub_seq in seq.date_range_ids:
- _drop_sequence(self.env.cr, ["ir_sequence_%03d_%03d" % (seq.id, sub_seq.id)])
+ _drop_sequences(self._cr, ["ir_sequence_%03d_%03d" % (seq.id, sub_seq.id)])
else:
if new_implementation in ('no_gap', None):
pass
else:
- _create_sequence(self.env.cr, "ir_sequence_%03d" % seq.id, i, n)
+ _create_sequence(self._cr, "ir_sequence_%03d" % seq.id, i, n)
for sub_seq in seq.date_range_ids:
- _create_sequence(self.env.cr, "ir_sequence_%03d_%03d" % (seq.id, sub_seq.id), i, n)
+ _create_sequence(self._cr, "ir_sequence_%03d_%03d" % (seq.id, sub_seq.id), i, n)
return super(IrSequence, self).write(values)
def _next_do(self):
if self.implementation == 'standard':
- number_next = _select_nextval(self.env.cr, 'ir_sequence_%03d' % self.id)
+ number_next = _select_nextval(self._cr, 'ir_sequence_%03d' % self.id)
else:
number_next = _update_nogap(self, self.number_increment)
return self.get_next_char(number_next)
def get_next_char(self, number_next):
def _interpolate(s, d):
- if s:
- return s % d
- return ''
+ return (s % d) if s else ''
def _interpolation_dict():
- now = range_date = effective_date = datetime.now(pytz.timezone(self.env.context.get('tz') or 'UTC'))
- if self.env.context.get('ir_sequence_date'):
- effective_date = datetime.strptime(self.env.context.get('ir_sequence_date'), '%Y-%m-%d')
- if self.env.context.get('ir_sequence_date_range'):
- range_date = datetime.strptime(self.env.context.get('ir_sequence_date_range'), '%Y-%m-%d')
+ now = range_date = effective_date = datetime.now(pytz.timezone(self._context.get('tz') or 'UTC'))
+ if self._context.get('ir_sequence_date'):
+ effective_date = datetime.strptime(self._context.get('ir_sequence_date'), '%Y-%m-%d')
+ if self._context.get('ir_sequence_date_range'):
+ range_date = datetime.strptime(self._context.get('ir_sequence_date_range'), '%Y-%m-%d')
sequences = {
'year': '%Y', 'month': '%m', 'day': '%d', 'y': '%y', 'doy': '%j', 'woy': '%W',
'weekday': '%w', 'h24': '%H', 'h12': '%I', 'min': '%M', 'sec': '%S'
}
res = {}
- for key, sequence in sequences.iteritems():
- res[key] = effective_date.strftime(sequence)
- res['range_' + key] = range_date.strftime(sequence)
- res['current_' + key] = now.strftime(sequence)
+ for key, format in sequences.iteritems():
+ res[key] = effective_date.strftime(format)
+ res['range_' + key] = range_date.strftime(format)
+ res['current_' + key] = now.strftime(format)
return res
@@ -247,8 +235,8 @@ class IrSequence(models.Model):
return self._next_do()
# date mode
dt = fields.Date.today()
- if self.env.context.get('ir_sequence_date'):
- dt = self.env.context.get('ir_sequence_date')
+ if self._context.get('ir_sequence_date'):
+ dt = self._context.get('ir_sequence_date')
seq_date = self.env['ir.sequence.date_range'].search([('sequence_id', '=', self.id), ('date_from', '<=', dt), ('date_to', '>=', dt)], limit=1)
if not seq_date:
seq_date = self._create_date_range_seq(dt)
@@ -278,7 +266,7 @@ class IrSequence(models.Model):
seq_ids = self.search(['&', ('code', '=', sequence_code), ('company_id', 'in', company_ids)])
if not seq_ids:
return False
- force_company = self.env.context.get('force_company')
+ force_company = self._context.get('force_company')
if not force_company:
force_company = self.env.user.company_id.id
preferred_sequences = [s for s in seq_ids if s.company_id and s.company_id.id == force_company]
@@ -317,22 +305,22 @@ class IrSequenceDateRange(models.Model):
def _get_number_next_actual(self):
'''Return number from ir_sequence row when no_gap implementation,
and number from postgres sequence when standard implementation.'''
- for element in self:
- if element.sequence_id.implementation != 'standard':
- element.number_next_actual = element.number_next
+ for seq in self:
+ if seq.sequence_id.implementation != 'standard':
+ seq.number_next_actual = seq.number_next
else:
# get number from postgres sequence. Cannot use currval, because that might give an error when
# not having used nextval before.
- self.env.cr.execute("SELECT last_value, increment_by, is_called FROM ir_sequence_%03d_%03d" % (element.sequence_id.id, element.id))
- (last_value, increment_by, is_called) = self.env.cr.fetchone()
+ self._cr.execute("SELECT last_value, increment_by, is_called FROM ir_sequence_%03d_%03d" % (seq.sequence_id.id, seq.id))
+ (last_value, increment_by, is_called) = self._cr.fetchone()
if is_called:
- element.number_next_actual = last_value + increment_by
+ seq.number_next_actual = last_value + increment_by
else:
- element.number_next_actual = last_value
+ seq.number_next_actual = last_value
def _set_number_next_actual(self):
- for record in self:
- record.write({'number_next': record.number_next_actual or 0})
+ for seq in self:
+ seq.write({'number_next': seq.number_next_actual or 0})
date_from = fields.Date(string='From', required=True)
date_to = fields.Date(string='To', required=True)
@@ -345,7 +333,7 @@ class IrSequenceDateRange(models.Model):
def _next(self):
if self.sequence_id.implementation == 'standard':
- number_next = _select_nextval(self.env.cr, 'ir_sequence_%03d_%03d' % (self.sequence_id.id, self.id))
+ number_next = _select_nextval(self._cr, 'ir_sequence_%03d_%03d' % (self.sequence_id.id, self.id))
else:
number_next = _update_nogap(self, self.sequence_id.number_increment)
return self.sequence_id.get_next_char(number_next)
@@ -353,7 +341,7 @@ class IrSequenceDateRange(models.Model):
@api.multi
def _alter_sequence(self, number_increment=None, number_next=None):
for seq in self:
- _alter_sequence(self.env.cr, "ir_sequence_%03d_%03d" % (seq.sequence_id.id, seq.id), number_increment=number_increment, number_next=number_next)
+ _alter_sequence(self._cr, "ir_sequence_%03d_%03d" % (seq.sequence_id.id, seq.id), number_increment=number_increment, number_next=number_next)
@api.model
def create(self, values):
@@ -362,12 +350,12 @@ class IrSequenceDateRange(models.Model):
seq = super(IrSequenceDateRange, self).create(values)
main_seq = seq.sequence_id
if main_seq.implementation == 'standard':
- _create_sequence(self.env.cr, "ir_sequence_%03d_%03d" % (main_seq.id, seq.id), main_seq.number_increment, values.get('number_next_actual', 1))
+ _create_sequence(self._cr, "ir_sequence_%03d_%03d" % (main_seq.id, seq.id), main_seq.number_increment, values.get('number_next_actual', 1))
return seq
@api.multi
def unlink(self):
- _drop_sequence(self.env.cr, ["ir_sequence_%03d_%03d" % (x.sequence_id.id, x.id) for x in self])
+ _drop_sequences(self._cr, ["ir_sequence_%03d_%03d" % (x.sequence_id.id, x.id) for x in self])
return super(IrSequenceDateRange, self).unlink()
@api.multi
diff --git a/openerp/addons/base/ir/ir_translation.py b/openerp/addons/base/ir/ir_translation.py
index 0a9c29fdb21..4ebe00543bc 100644
--- a/openerp/addons/base/ir/ir_translation.py
+++ b/openerp/addons/base/ir/ir_translation.py
@@ -5,9 +5,9 @@ import logging
from collections import defaultdict
from difflib import get_close_matches
-import odoo
from odoo import api, fields, models, tools, _
from odoo.exceptions import AccessError, UserError, ValidationError
+from odoo.modules import get_module_path, get_module_resource
_logger = logging.getLogger(__name__)
@@ -24,38 +24,35 @@ TRANSLATION_TYPE = [
]
-class IrTranslationImportCursor(object):
- """Temporary cursor for optimizing mass insert into ir.translation
+class IrTranslationImport(object):
+ """ Temporary cursor for optimizing mass insert into model 'ir.translation'.
Open it (attached to a sql cursor), feed it with translation data and
finish() it in order to insert multiple translations in a batch.
"""
- _table_name = 'tmp_ir_translation_import'
+ _table = 'tmp_ir_translation_import'
- def __init__(self, cr, uid, parent, context):
- """ Initializer
-
- Store some values, and also create a temporary SQL table to accept
+ def __init__(self, model):
+ """ Store some values, and also create a temporary SQL table to accept
the data.
- @param parent an instance of ir.translation ORM model
+
+ :param model: the model to insert the data into (as a recordset)
"""
- self._cr = cr
- self._uid = uid
- self._context = context
- self._overwrite = context.get('overwrite', False)
+ self._cr = model._cr
+ self._model_table = model._table
+ self._overwrite = model._context.get('overwrite', False)
self._debug = False
- self._parent_table = parent._table
# Note that Postgres will NOT inherit the constraints or indexes
# of ir_translation, so this copy will be much faster.
- cr.execute('''CREATE TEMP TABLE %s(
- imd_model VARCHAR(64),
- imd_name VARCHAR(128)
- ) INHERITS (%s) ''' % (self._table_name, self._parent_table))
+ query = """ CREATE TEMP TABLE %s (
+ imd_model VARCHAR(64),
+ imd_name VARCHAR(128)
+ ) INHERITS (%s) """ % (self._table, self._model_table)
+ self._cr.execute(query)
def push(self, trans_dict):
- """Feed a translation, as a dictionary, into the cursor
- """
+ """ Feed a translation, as a dictionary, into the cursor """
if not trans_dict['value']:
return
@@ -92,38 +89,37 @@ class IrTranslationImportCursor(object):
params['name'] = 'ir.ui.view,arch_db'
params['imd_model'] = "ir.ui.view"
- self._cr.execute("""INSERT INTO %s (name, lang, res_id, src, type, imd_model, module, imd_name, value, state, comments)
- VALUES (%%(name)s, %%(lang)s, %%(res_id)s, %%(src)s, %%(type)s, %%(imd_model)s, %%(module)s,
- %%(imd_name)s, %%(value)s, %%(state)s, %%(comments)s)""" % self._table_name,
- params)
+ query = """ INSERT INTO %s (name, lang, res_id, src, type, imd_model, module, imd_name, value, state, comments)
+ VALUES (%%(name)s, %%(lang)s, %%(res_id)s, %%(src)s, %%(type)s, %%(imd_model)s, %%(module)s,
+ %%(imd_name)s, %%(value)s, %%(state)s, %%(comments)s) """ % self._table
+ self._cr.execute(query, params)
def finish(self):
- """ Transfer the data from the temp table to ir.translation
- """
+ """ Transfer the data from the temp table to ir.translation """
cr = self._cr
if self._debug:
- cr.execute("SELECT count(*) FROM %s" % self._table_name)
- c = cr.fetchone()[0]
- _logger.debug("ir.translation.cursor: We have %d entries to process", c)
+ cr.execute("SELECT count(*) FROM %s" % self._table)
+ count = cr.fetchone()[0]
+ _logger.debug("ir.translation.cursor: We have %d entries to process", count)
# Step 1: resolve ir.model.data references to res_ids
- cr.execute("""UPDATE %s AS ti
- SET res_id = imd.res_id
- FROM ir_model_data AS imd
- WHERE ti.res_id IS NULL
- AND ti.module IS NOT NULL AND ti.imd_name IS NOT NULL
- AND ti.module = imd.module AND ti.imd_name = imd.name
- AND ti.imd_model = imd.model; """ % self._table_name)
+ cr.execute(""" UPDATE %s AS ti
+ SET res_id = imd.res_id
+ FROM ir_model_data AS imd
+ WHERE ti.res_id IS NULL
+ AND ti.module IS NOT NULL AND ti.imd_name IS NOT NULL
+ AND ti.module = imd.module AND ti.imd_name = imd.name
+ AND ti.imd_model = imd.model; """ % self._table)
if self._debug:
- cr.execute("SELECT module, imd_name, imd_model FROM %s " \
- "WHERE res_id IS NULL AND module IS NOT NULL" % self._table_name)
+ cr.execute(""" SELECT module, imd_name, imd_model FROM %s
+ WHERE res_id IS NULL AND module IS NOT NULL """ % self._table)
for row in cr.fetchall():
_logger.info("ir.translation.cursor: missing res_id for %s.%s <%s> ", *row)
# Records w/o res_id must _not_ be inserted into our db, because they are
# referencing non-existent data.
- cr.execute("DELETE FROM %s WHERE res_id IS NULL AND module IS NOT NULL" % self._table_name)
+ cr.execute("DELETE FROM %s WHERE res_id IS NULL AND module IS NOT NULL" % self._table)
find_expr = """
irt.lang = ti.lang
@@ -140,31 +136,31 @@ class IrTranslationImportCursor(object):
# Step 2: update existing (matching) translations
if self._overwrite:
- cr.execute("""UPDATE ONLY %s AS irt
- SET value = ti.value,
- src = ti.src,
- state = 'translated'
- FROM %s AS ti
- WHERE %s AND ti.value IS NOT NULL AND ti.value != ''
- """ % (self._parent_table, self._table_name, find_expr))
+ cr.execute(""" UPDATE ONLY %s AS irt
+ SET value = ti.value,
+ src = ti.src,
+ state = 'translated'
+ FROM %s AS ti
+ WHERE %s AND ti.value IS NOT NULL AND ti.value != ''
+ """ % (self._model_table, self._table, find_expr))
# Step 3: insert new translations
- cr.execute("""INSERT INTO %s(name, lang, res_id, src, type, value, module, state, comments)
- SELECT name, lang, res_id, src, type, value, module, state, comments
- FROM %s AS ti
- WHERE NOT EXISTS(SELECT 1 FROM ONLY %s AS irt WHERE %s);
- """ % (self._parent_table, self._table_name, self._parent_table, find_expr))
+ cr.execute(""" INSERT INTO %s(name, lang, res_id, src, type, value, module, state, comments)
+ SELECT name, lang, res_id, src, type, value, module, state, comments
+ FROM %s AS ti
+ WHERE NOT EXISTS(SELECT 1 FROM ONLY %s AS irt WHERE %s);
+ """ % (self._model_table, self._table, self._model_table, find_expr))
if self._debug:
- cr.execute('SELECT COUNT(*) FROM ONLY %s' % self._parent_table)
- c1 = cr.fetchone()[0]
- cr.execute('SELECT COUNT(*) FROM ONLY %s AS irt, %s AS ti WHERE %s' % \
- (self._parent_table, self._table_name, find_expr))
- c = cr.fetchone()[0]
- _logger.debug("ir.translation.cursor: %d entries now in ir.translation, %d common entries with tmp", c1, c)
+ cr.execute("SELECT COUNT(*) FROM ONLY %s" % self._model_table)
+ total = cr.fetchone()[0]
+ cr.execute("SELECT COUNT(*) FROM ONLY %s AS irt, %s AS ti WHERE %s" % \
+ (self._model_table, self._table, find_expr))
+ count = cr.fetchone()[0]
+ _logger.debug("ir.translation.cursor: %d entries now in ir.translation, %d common entries with tmp", total, count)
# Step 4: cleanup
- cr.execute("DROP TABLE %s" % self._table_name)
+ cr.execute("DROP TABLE %s" % self._table)
return True
@@ -172,18 +168,13 @@ class IrTranslation(models.Model):
_name = "ir.translation"
_log_access = False
- @api.model
- def _get_language(self):
- lang_model = self.env['res.lang']
- lang = lang_model.search([('translatable', '=', True)])
- return [(data.code, data.name) for data in lang]
-
name = fields.Char(string='Translated field', required=True)
res_id = fields.Integer(string='Record ID', index=True)
- lang = fields.Selection(selection='_get_language', string='Language')
+ lang = fields.Selection(selection='_get_languages', string='Language')
type = fields.Selection(TRANSLATION_TYPE, string='Type', index=True)
src = fields.Text(string='Internal Source') # stored in database, kept for backward compatibility
- source = fields.Text(compute='_get_src', inverse='_set_src', search='_search_src', string='Source term')
+ source = fields.Text(string='Source term', compute='_compute_source',
+ inverse='_inverse_source', search='_search_source')
value = fields.Text(string='Translation Value')
module = fields.Char(index=True, help="Module this term belongs to")
@@ -197,60 +188,65 @@ class IrTranslation(models.Model):
# cfr: http://www.gnu.org/savannah-checkouts/gnu/gettext/manual/html_node/PO-Files.html
comments = fields.Text(string='Translation comments', index=True)
- _sql_constraints = [('lang_fkey_res_lang', 'FOREIGN KEY(lang) REFERENCES res_lang(code)',
- 'Language code of translation item must be among known languages')]
+ _sql_constraints = [
+ ('lang_fkey_res_lang', 'FOREIGN KEY(lang) REFERENCES res_lang(code)',
+ 'Language code of translation item must be among known languages'),
+ ]
+
+ @api.model
+ def _get_languages(self):
+ langs = self.env['res.lang'].search([('translatable', '=', True)])
+ return [(lang.code, lang.name) for lang in langs]
+
+ @api.depends('type', 'name', 'res_id')
+ def _compute_source(self):
+ ''' Get source name for the translation. If object type is model, return
+ the value stored in db. Otherwise, return value store in src field.
+ '''
+ for record in self:
+ record.source = record.src
+ if record.type != 'model':
+ continue
+ model_name, field_name = record.name.split(',')
+ if model_name not in self.env:
+ continue
+ model = self.env[model_name]
+ field = model._fields.get(field_name)
+ if field is None:
+ continue
+ if not callable(field.translate):
+ # Pass context without lang, need to read real stored field, not translation
+ result = model.browse(record.res_id).with_context(lang=None).read([field_name])
+ record.source = result[0][field_name] if result else False
- @api.depends('source')
- def _get_src(self):
- ''' Get source name for the translation. If object type is model then
- return the value store in db. Otherwise return value store in src field
+ def _inverse_source(self):
+ ''' When changing source term of a translation, change its value in db
+ for the associated object, and the src field.
'''
for record in self:
if record.type == 'model':
model_name, field_name = record.name.split(',')
model = self.env[model_name]
- if model is None:
- continue
- field = model._fields.get(field_name)
- if field is None:
- continue
+ field = model._fields[field_name]
if not callable(field.translate):
- # Pass context without lang, need to read real stored field, not translation
- context_no_lang = dict(self.env.context, lang=None)
- result = model.with_context(context_no_lang).browse(record.res_id).read([field_name])
- record.source = result[0][field_name] if result else False
-
- def _set_src(self):
- ''' When changing source term of a translation, change its value in db for
- the associated object, and the src field
- '''
- if self.type == 'model':
- model_name, field_name = self.name.split(',')
- model = self.env[model_name]
- field = model._fields[field_name]
- if not callable(field.translate):
- # Make a context without language information, because we want
- # to write on the value stored in db and not on the one
- # associated with the current language. Also not removing lang
- # from context trigger an error when lang is different.
- context_wo_lang = self.env.context.copy()
- context_wo_lang.pop('lang', None)
- model.with_context(context_wo_lang).browse(self.res_id).write({field_name: self.source})
+ # Make a context without language information, because we want
+ # to write on the value stored in db and not on the one
+ # associated with the current language. Also not removing lang
+ # from context trigger an error when lang is different.
+ model.browse(record.res_id).with_context(lang=None).write({field_name: record.source})
return self.write({'src': self.source})
- @api.model
- def _search_src(self, operator, operand):
+ def _search_source(self, operator, value):
''' the source term is stored on 'src' field '''
- res = []
- for field, operator, value in operand:
- res.append(('src', operator, value))
- return res
+ return [('src', operator, value)]
+ @api.model_cr_context
def _auto_init(self):
super(IrTranslation, self)._auto_init()
+ cr = self._cr
- self.env.cr.execute("SELECT indexname FROM pg_indexes WHERE indexname LIKE 'ir_translation_%'")
- indexes = [row[0] for row in self.env.cr.fetchall()]
+ cr.execute("SELECT indexname FROM pg_indexes WHERE indexname LIKE 'ir_translation_%'")
+ indexes = [row[0] for row in cr.fetchall()]
# Removed because there is a size limit on btree indexed values (problem with column src):
# cr.execute('CREATE INDEX ir_translation_ltns ON ir_translation (name, lang, type, src)')
@@ -259,17 +255,17 @@ class IrTranslation(models.Model):
# Removed because hash indexes are not compatible with postgres streaming replication:
# cr.execute('CREATE INDEX ir_translation_src_hash_idx ON ir_translation USING hash (src)')
if set(indexes) & set(['ir_translation_ltns', 'ir_translation_lts', 'ir_translation_src_hash_idx']):
- self.env.cr.execute('DROP INDEX IF EXISTS ir_translation_ltns, ir_translation_lts, ir_translation_src_hash_idx')
- self.env.cr.commit()
+ cr.execute('DROP INDEX IF EXISTS ir_translation_ltns, ir_translation_lts, ir_translation_src_hash_idx')
+ cr.commit()
# Add separate md5 index on src (no size limit on values, and good performance).
if 'ir_translation_src_md5' not in indexes:
- self.env.cr.execute('CREATE INDEX ir_translation_src_md5 ON ir_translation (md5(src))')
- self.env.cr.commit()
+ cr.execute('CREATE INDEX ir_translation_src_md5 ON ir_translation (md5(src))')
+ cr.commit()
if 'ir_translation_ltn' not in indexes:
- self.env.cr.execute('CREATE INDEX ir_translation_ltn ON ir_translation (name, lang, type)')
- self.env.cr.commit()
+ cr.execute('CREATE INDEX ir_translation_ltn ON ir_translation (name, lang, type)')
+ cr.commit()
@api.model
def _check_selection_field_value(self, field, value):
@@ -278,54 +274,64 @@ class IrTranslation(models.Model):
return super(IrTranslation, self)._check_selection_field_value(field, value)
@api.model
- def _get_ids(self, name, tt, lang, in_ids):
- if in_ids:
- self.env.cr.execute('select res_id,value '
- 'from ir_translation '
- 'where lang=%s '
- 'and type=%s '
- 'and name=%s '
- 'and res_id IN %s',
- (lang, tt, name, tuple(in_ids)))
- for res_id, value in self.env.cr.fetchall():
- self.res_id = value
+ def _get_ids(self, name, tt, lang, ids):
+ """ Return the translations of records.
+
+ :param name: a string defined as "<model_name>,<field_name>"
+ :param tt: the type of translation (should always be "model")
+ :param lang: the language code
+ :param ids: the ids of the given records
+ """
+ translations = dict.fromkeys(ids, False)
+ if ids:
+ self._cr.execute("""SELECT res_id, value FROM ir_translation
+ WHERE lang=%s AND type=%s AND name=%s AND res_id IN %s""",
+ (lang, tt, name, tuple(ids)))
+ for res_id, value in self._cr.fetchall():
+ translations[res_id] = value
+ return translations
@api.model
- def _set_ids(self, name, tt, lang, rec_ids, value, src=None):
+ def _set_ids(self, name, tt, lang, ids, value, src=None):
+ """ Update the translations of records.
+
+ :param name: a string defined as "<model_name>,<field_name>"
+ :param tt: the type of translation (should always be "model")
+ :param lang: the language code
+ :param ids: the ids of the given records
+ :param value: the value of the translation
+ :param src: the source of the translation
+ """
self.clear_caches()
- self.env.cr.execute('update ir_translation '
- 'set value=%s '
- ' , src=%s '
- ' , state=%s '
- 'where lang=%s '
- 'and type=%s '
- 'and name=%s '
- 'and res_id IN %s '
- 'returning res_id',
- (value, src, 'translated', lang, tt, name, tuple(rec_ids),))
-
- existing_ids = [x[0] for x in self.env.cr.fetchall()]
-
- for id in list(set(rec_ids) - set(existing_ids)):
- self.create({'lang': lang,
- 'type': tt,
- 'name': name,
- 'res_id': id,
- 'value': value,
- 'src': src,
- 'state': 'translated'}).id
- return len(rec_ids)
+
+ # update existing translations
+ self._cr.execute("""UPDATE ir_translation
+ SET value=%s, src=%s, state=%s
+ WHERE lang=%s AND type=%s AND name=%s AND res_id IN %s
+ RETURNING res_id""",
+ (value, src, 'translated', lang, tt, name, tuple(ids)))
+ existing_ids = [row[0] for row in self._cr.fetchall()]
+
+ # create missing translations
+ for res_id in set(ids) - set(existing_ids):
+ self.create({
+ 'lang': lang,
+ 'type': tt,
+ 'name': name,
+ 'res_id': res_id,
+ 'value': value,
+ 'src': src,
+ 'state': 'translated',
+ })
+ return len(ids)
@api.model
def _get_source_query(self, name, types, lang, source, res_id):
if source:
# Note: the extra test on md5(src) is a hint for postgres to use the
# index ir_translation_src_md5
- query = """SELECT value
- FROM ir_translation
- WHERE lang=%s
- AND type in %s
- AND src=%s AND md5(src)=md5(%s)"""
+ query = """SELECT value FROM ir_translation
+ WHERE lang=%s AND type in %s AND src=%s AND md5(src)=md5(%s)"""
source = tools.ustr(source)
params = (lang or '', types, source, source)
if res_id:
@@ -335,12 +341,8 @@ class IrTranslation(models.Model):
query += " AND name=%s"
params += (tools.ustr(name),)
else:
- query = """SELECT value
- FROM ir_translation
- WHERE lang=%s
- AND type in %s
- AND name=%s"""
-
+ query = """ SELECT value FROM ir_translation
+ WHERE lang=%s AND type in %s AND name=%s """
params = (lang or '', types, tools.ustr(name))
return (query, params)
@@ -349,8 +351,8 @@ class IrTranslation(models.Model):
def __get_source(self, name, types, lang, source, res_id):
# res_id is a tuple or None, otherwise ormcache cannot cache it!
query, params = self._get_source_query(name, types, lang, source, res_id)
- self.env.cr.execute(query, params)
- res = self.env.cr.fetchone()
+ self._cr.execute(query, params)
+ res = self._cr.fetchone()
trad = res and res[0] or u''
if source and not trad:
return tools.ustr(source)
@@ -358,10 +360,9 @@ class IrTranslation(models.Model):
@api.model
def _get_source(self, name, types, lang, source=None, res_id=None):
- """
- Returns the translation for the given combination of name, type, language
- and source. All values passed to this method should be unicode (not byte strings),
- especially ``source``.
+ """ Return the translation for the given combination of ``name``,
+ ``type``, ``language`` and ``source``. All values passed to this method
+ should be unicode (not byte strings), especially ``source``.
:param name: identification of the term to translate, such as field name (optional if source is passed)
:param types: single string defining type of term to translate (see ``type`` field on ir.translation), or sequence of allowed types (strings)
@@ -372,8 +373,8 @@ class IrTranslation(models.Model):
:return: the request translation, or an empty unicode string if no translation was
found and `source` was not passed
"""
- # FIXME: should assert that `source` is unicode and fix all callers to always pass unicode
- # so we can remove the string encoding/decoding.
+ # FIXME: should assert that `source` is unicode and fix all callers to
+ # always pass unicode so we can remove the string encoding/decoding.
if not lang:
return tools.ustr(source or '')
if isinstance(types, basestring):
@@ -659,24 +660,25 @@ class IrTranslation(models.Model):
}
return action
- def _get_import_cursor(self, cr, uid, context=None):
- """ Return a cursor-like object for fast inserting translations
- """
- return IrTranslationImportCursor(cr, uid, self, context=context)
+ @api.model
+ def _get_import_cursor(self):
+ """ Return a cursor-like object for fast inserting translations """
+ return IrTranslationImport(self)
- def load_module_terms(self, cr, modules, langs, context=None):
- context_template = dict(context or {}) # local copy
+ @api.model_cr_context
+ def load_module_terms(self, modules, langs):
+ """ Load PO files of the given modules for the given languages. """
# make sure the given languages are active
- lang_obj = self.pool['res.lang']
+ res_lang = self.env['res.lang'].sudo()
for lang in langs:
- lang_obj.load_lang(cr, odoo.SUPERUSER_ID, lang)
+ res_lang.load_lang(lang)
# load i18n files
for module_name in modules:
- modpath = odoo.modules.get_module_path(module_name)
+ modpath = get_module_path(module_name)
if not modpath:
continue
for lang in langs:
- context = dict(context_template)
+ context = dict(self._context)
lang_code = tools.get_iso_codes(lang)
base_lang_code = None
if '_' in lang_code:
@@ -684,31 +686,31 @@ class IrTranslation(models.Model):
# Step 1: for sub-languages, load base language first (e.g. es_CL.po is loaded over es.po)
if base_lang_code:
- base_trans_file = odoo.modules.get_module_resource(module_name, 'i18n', base_lang_code + '.po')
+ base_trans_file = get_module_resource(module_name, 'i18n', base_lang_code + '.po')
if base_trans_file:
_logger.info('module %s: loading base translation file %s for language %s', module_name, base_lang_code, lang)
- tools.trans_load(cr, base_trans_file, lang, verbose=False, module_name=module_name, context=context)
+ tools.trans_load(self._cr, base_trans_file, lang, verbose=False, module_name=module_name, context=context)
context['overwrite'] = True # make sure the requested translation will override the base terms later
# i18n_extra folder is for additional translations handle manually (eg: for l10n_be)
- base_trans_extra_file = odoo.modules.get_module_resource(module_name, 'i18n_extra', base_lang_code + '.po')
+ base_trans_extra_file = get_module_resource(module_name, 'i18n_extra', base_lang_code + '.po')
if base_trans_extra_file:
_logger.info('module %s: loading extra base translation file %s for language %s', module_name, base_lang_code, lang)
- tools.trans_load(cr, base_trans_extra_file, lang, verbose=False, module_name=module_name, context=context)
+ tools.trans_load(self._cr, base_trans_extra_file, lang, verbose=False, module_name=module_name, context=context)
context['overwrite'] = True # make sure the requested translation will override the base terms later
# Step 2: then load the main translation file, possibly overriding the terms coming from the base language
- trans_file = odoo.modules.get_module_resource(module_name, 'i18n', lang_code + '.po')
+ trans_file = get_module_resource(module_name, 'i18n', lang_code + '.po')
if trans_file:
_logger.info('module %s: loading translation file (%s) for language %s', module_name, lang_code, lang)
- tools.trans_load(cr, trans_file, lang, verbose=False, module_name=module_name, context=context)
+ tools.trans_load(self._cr, trans_file, lang, verbose=False, module_name=module_name, context=context)
elif lang_code != 'en_US':
_logger.info('module %s: no translation for language %s', module_name, lang_code)
- trans_extra_file = odoo.modules.get_module_resource(module_name, 'i18n_extra', lang_code + '.po')
+ trans_extra_file = get_module_resource(module_name, 'i18n_extra', lang_code + '.po')
if trans_extra_file:
_logger.info('module %s: loading extra translation file (%s) for language %s', module_name, lang_code, lang)
- tools.trans_load(cr, trans_extra_file, lang, verbose=False, module_name=module_name, context=context)
+ tools.trans_load(self._cr, trans_extra_file, lang, verbose=False, module_name=module_name, context=context)
return True
@api.model
@@ -721,18 +723,19 @@ class IrTranslation(models.Model):
:return: action definition to open the list of available translations
"""
fields = self.env['ir.model.fields'].search([('model', '=', model_name)])
- view = self.env.ref("base.view_translation_tree", False)
+ view = self.env.ref("base.view_translation_tree", False) or self.env['ir.ui.view']
return {
- 'name': _("Technical Translation"),
+ 'name': _("Technical Translations"),
'view_mode': 'tree',
- 'views': [(view and view.id or False, "list")],
+ 'views': [(view.id, "list")],
'res_model': 'ir.translation',
'type': 'ir.actions.act_window',
- 'domain': ['|',
- '&',('type', '=', 'model'),
- '&',('res_id', 'in', fields.ids),
- ('name', 'like', 'ir.model.fields,'),
- '&',('type', '=', 'selection'),
- ('name', 'like', model_name+','),
- ],
+ 'domain': [
+ '|',
+ '&', ('type', '=', 'model'),
+ '&', ('res_id', 'in', fields.ids),
+ ('name', 'like', 'ir.model.fields,'),
+ '&', ('type', '=', 'selection'),
+ ('name', 'like', model_name+','),
+ ],
}
diff --git a/openerp/addons/base/ir/ir_ui_menu.py b/openerp/addons/base/ir/ir_ui_menu.py
index 61509c8145e..fe205eb9307 100644
--- a/openerp/addons/base/ir/ir_ui_menu.py
+++ b/openerp/addons/base/ir/ir_ui_menu.py
@@ -5,13 +5,14 @@ import base64
import operator
import re
-import odoo
from odoo import api, fields, models, tools, _
from odoo.exceptions import ValidationError
from odoo.http import request
+from odoo.modules import get_module_resource
from odoo.tools.safe_eval import safe_eval as eval
MENU_ITEM_SEPARATOR = "/"
+NUMBER_PARENS = re.compile(r"\(([0-9]+)\)")
class IrUiMenu(models.Model):
@@ -33,7 +34,7 @@ class IrUiMenu(models.Model):
'menu_id', 'gid', string='Groups',
help="If you have groups, the visibility of this menu will be based on these groups. "\
"If this field is empty, Odoo will compute visibility based on the related object's read access.")
- complete_name = fields.Char(compute='_get_full_name', string='Full Path')
+ complete_name = fields.Char(compute='_compute_complete_name', string='Full Path')
web_icon = fields.Char(string='Web Icon File')
action = fields.Reference(selection=[('ir.actions.report.xml', 'ir.actions.report.xml'),
('ir.actions.act_window', 'ir.actions.act_window'),
@@ -43,6 +44,20 @@ class IrUiMenu(models.Model):
web_icon_data = fields.Binary(string='Web Icon Image', compute="_compute_web_icon", store=True, attachment=True)
+ @api.depends('name', 'parent_id.complete_name')
+ def _compute_complete_name(self):
+ for menu in self:
+ menu.complete_name = menu._get_full_name()
+
+ def _get_full_name(self, level=6):
+ """ Return the full name of ``self`` (up to a certain level). """
+ if level <= 0:
+ return '...'
+ if self.parent_id:
+ return self.parent_id._get_full_name(level - 1) + MENU_ITEM_SEPARATOR + self.name
+ else:
+ return self.name
+
@api.depends('web_icon')
def _compute_web_icon(self):
for menu in self:
@@ -52,29 +67,17 @@ class IrUiMenu(models.Model):
if not path:
return False
path_info = path.split(',')
- icon_path = odoo.modules.get_module_resource(path_info[0], path_info[1])
+ icon_path = get_module_resource(path_info[0], path_info[1])
icon_image = False
if icon_path:
- try:
- icon_file = tools.file_open(icon_path, 'rb')
+ with tools.file_open(icon_path, 'rb') as icon_file:
icon_image = base64.encodestring(icon_file.read())
- finally:
- icon_file.close()
return icon_image
- @api.depends('complete_name')
- def _get_full_name(self):
- for element in self:
- element.complete_name = self._get_one_full_name(element)
-
- def _get_one_full_name(self, elmt, level=6):
- if level <= 0:
- return '...'
- if elmt.parent_id:
- parent_path = self._get_one_full_name(elmt.parent_id, level-1) + MENU_ITEM_SEPARATOR
- else:
- parent_path = ''
- return parent_path + elmt.name
+ @api.constrains('parent_id')
+ def _check_parent_id(self):
+ if not self._check_recursion():
+ raise ValidationError(_('Error! You cannot create recursive menus.'))
@api.model
@tools.ormcache('frozenset(self.env.user.groups_id.ids)', 'debug')
@@ -84,7 +87,9 @@ class IrUiMenu(models.Model):
context = {'ir.ui.menu.full_list': True}
menus = self.with_context(context).search([])
- groups = self.env.user.groups_id if debug else self.env.user.groups_id - self.env.ref('base.group_no_one')
+ groups = self.env.user.groups_id
+ if not debug:
+ groups = groups - self.env.ref('base.group_no_one')
# first discard all menus with groups the user does not have
menus = menus.filtered(
lambda menu: not menu.groups_id or menu.groups_id & groups)
@@ -124,39 +129,22 @@ class IrUiMenu(models.Model):
visible_ids = self._visible_menu_ids(request.debug if request else False)
return self.filtered(lambda menu: menu.id in visible_ids)
- def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False):
- if context is None:
- context = {}
-
- ids = super(IrUiMenu, self).search(cr, uid, args, offset=0,
- limit=None, order=order, context=context, count=False)
-
- if not ids:
- if count:
- return 0
- return []
-
- # menu filtering is done only on main menu tree, not other menu lists
- if context.get('ir.ui.menu.full_list'):
- result = ids
- else:
- result = self._filter_visible_menus(cr, uid, ids, context=context)
-
- if offset:
- result = result[long(offset):]
- if limit:
- result = result[:long(limit)]
-
- if count:
- return len(result)
- return result
+ @api.model
+ def search(self, args, offset=0, limit=None, order=None, count=False):
+ menus = super(IrUiMenu, self).search(args, offset=0, limit=None, order=order, count=False)
+ if menus:
+ # menu filtering is done only on main menu tree, not other menu lists
+ if not self._context.get('ir.ui.menu.full_list'):
+ menus = menus._filter_visible_menus()
+ if offset:
+ menus = menus[long(offset):]
+ if limit:
+ menus = menus[:long(limit)]
+ return len(menus) if count else menus
@api.multi
def name_get(self):
- res = []
- for element in self:
- res.append((element.id, self._get_one_full_name(element)))
- return res
+ return [(menu.id, menu._get_full_name()) for menu in self]
@api.model
def create(self, values):
@@ -174,81 +162,66 @@ class IrUiMenu(models.Model):
# cascade-delete submenus blindly. We also can't use ondelete=set null because
# that is not supported when _parent_store is used (would silently corrupt it).
# TODO: ideally we should move them under a generic "Orphans" menu somewhere?
- local_context = dict(self.env.context or {})
- local_context['ir.ui.menu.full_list'] = True
- direct_children = self.with_context(local_context).search([('parent_id', 'in', self.ids)])
- if direct_children:
- direct_children.write({'parent_id': False})
+ extra = {'ir.ui.menu.full_list': True}
+ direct_children = self.with_context(**extra).search([('parent_id', 'in', self.ids)])
+ direct_children.write({'parent_id': False})
- result = super(IrUiMenu, self).unlink()
self.clear_caches()
- return result
+ return super(IrUiMenu, self).unlink()
@api.multi
def copy(self, default=None):
- res = super(IrUiMenu, self).copy(default=default)
- rex = re.compile('\([0-9]+\)')
- concat = rex.findall(res.name)
- if concat:
- next_num = int(concat[0])+1
- res.name = rex.sub(('(%d)' % next_num), res.name)
+ record = super(IrUiMenu, self).copy(default=default)
+ match = NUMBER_PARENS.search(record.name)
+ if match:
+ next_num = int(match.group(1)) + 1
+ record.name = NUMBER_PARENS.sub('(%d)' % next_num, record.name)
else:
- res.name += '(1)'
- res.write({'name': res.name})
- return res
+ record.name = record.name + '(1)'
+ return record
@api.multi
def get_needaction_data(self):
- """ Return for each menu entry of ids :
- - if it uses the needaction mechanism (needaction_enabled)
+ """ Return for each menu entry in ``self``:
+ - whether it uses the needaction mechanism (needaction_enabled)
- the needaction counter of the related action, taking into account
the action domain
"""
- res = {}
menu_ids = set()
for menu in self:
menu_ids.add(menu.id)
- ctx = None
+ ctx = {}
if menu.action and menu.action.type in ('ir.actions.act_window', 'ir.actions.client') and menu.action.context:
- try:
+ with tools.ignore(Exception):
# use magical UnquoteEvalContext to ignore undefined client-side variables such as `active_id`
- ctxt = dict(self.env.context or {})
- eval_ctx = tools.UnquoteEvalContext(**ctxt)
- ctx = eval(menu.action.context, locals_dict=eval_ctx, nocopy=True) or None
- except Exception:
- # if the eval still fails for some reason, we'll simply skip this menu
- pass
- menu_ref = ctx and ctx.get('needaction_menu_ref')
- if menu_ref:
- if not isinstance(menu_ref, list):
- menu_ref = [menu_ref]
- model_data_obj = self.env['ir.model.data']
- for menu_data in menu_ref:
- try:
- model, model_id = model_data_obj.get_object_reference(menu_data.split('.')[0], menu_data.split('.')[1])
- if (model == 'ir.ui.menu'):
- menu_ids.add(model_id)
- except Exception:
- pass
-
- menu_ids = list(menu_ids)
+ eval_ctx = tools.UnquoteEvalContext(self._context)
+ ctx = eval(menu.action.context, locals_dict=eval_ctx, nocopy=True) or {}
+ menu_refs = ctx.get('needaction_menu_ref')
+ if menu_refs:
+ if not isinstance(menu_refs, list):
+ menu_refs = [menu_refs]
+ for menu_ref in menu_refs:
+ record = self.env.ref(menu_ref, False)
+ if record and record._name == 'ir.ui.menu':
+ menu_ids.add(record.id)
- for menu in self:
+ res = {}
+ for menu in self.browse(menu_ids):
res[menu.id] = {
'needaction_enabled': False,
'needaction_counter': False,
}
if menu.action and menu.action.type in ('ir.actions.act_window', 'ir.actions.client') and menu.action.res_model:
- if menu.action.res_model in self.pool:
- obj = self.env[menu.action.res_model]
- if obj._needaction:
+ if menu.action.res_model in self.env:
+ model = self.env[menu.action.res_model]
+ if model._needaction:
if menu.action.type == 'ir.actions.act_window':
eval_context = self.env['ir.actions.act_window']._get_eval_context()
- dom = menu.action.domain and eval(menu.action.domain, eval_context) or []
+ dom = eval(menu.action.domain or '[]', eval_context)
else:
- dom = eval(menu.action.params_store or '{}', {'uid': self.env.uid}).get('domain')
- res[menu.id]['needaction_enabled'] = obj._needaction
- res[menu.id]['needaction_counter'] = obj._needaction_count(dom)
+ dom = eval(menu.action.params_store or '{}', {'uid': self._uid}).get('domain')
+ res[menu.id]['needaction_enabled'] = model._needaction
+ res[menu.id]['needaction_counter'] = model._needaction_count(dom)
return res
@api.model
@@ -261,66 +234,62 @@ class IrUiMenu(models.Model):
"""
return self.search([('parent_id', '=', False)])
- @api.cr_uid_context
- @tools.ormcache_context('uid', keys=('lang',))
- def load_menus_root(self, cr, uid, context=None):
+ @api.model
+ @tools.ormcache_context('self._uid', keys=('lang',))
+ def load_menus_root(self):
fields = ['name', 'sequence', 'parent_id', 'action', 'web_icon_data']
- menu_root_ids = self.get_user_roots(cr, uid, context=context)
- menu_roots = self.read(cr, uid, menu_root_ids, fields, context=context) if menu_root_ids else []
+ menu_roots = self.get_user_roots()
+ menu_roots_data = menu_roots.read(fields) if menu_roots else []
return {
'id': False,
'name': 'root',
'parent_id': [-1, ''],
- 'children': menu_roots,
- 'all_menu_ids': menu_root_ids,
+ 'children': menu_roots_data,
+ 'all_menu_ids': menu_roots.ids,
}
- @api.cr_uid_context
- @tools.ormcache_context('uid', 'debug', keys=('lang',))
- def load_menus(self, cr, uid, debug, context=None):
+ @api.model
+ @tools.ormcache_context('self._uid', 'debug', keys=('lang',))
+ def load_menus(self, debug):
""" Loads all menu items (all applications and their sub-menus).
:return: the menu root
:rtype: dict('children': menu_nodes)
"""
fields = ['name', 'sequence', 'parent_id', 'action', 'web_icon_data']
- menu_root_ids = self.get_user_roots(cr, uid, context=context)
- menu_roots = self.read(cr, uid, menu_root_ids, fields, context=context) if menu_root_ids else []
+ menu_roots = self.get_user_roots()
+ menu_roots_data = menu_roots.read(fields) if menu_roots else []
menu_root = {
'id': False,
'name': 'root',
'parent_id': [-1, ''],
- 'children': menu_roots,
- 'all_menu_ids': menu_root_ids,
+ 'children': menu_roots_data,
+ 'all_menu_ids': menu_roots.ids,
}
- if not menu_roots:
+ if not menu_roots_data:
return menu_root
# menus are loaded fully unlike a regular tree view, cause there are a
# limited number of items (752 when all 6.1 addons are installed)
- menu_ids = self.search(cr, uid, [('id', 'child_of', menu_root_ids)], 0, False, False, context=context)
- menu_items = self.read(cr, uid, menu_ids, fields, context=context)
- # adds roots at the end of the sequence, so that they will overwrite
+ menus = self.search([('id', 'child_of', menu_roots.ids)])
+ menu_items = menus.read(fields)
+
+ # add roots at the end of the sequence, so that they will overwrite
# equivalent menu items from full menu read when put into id:item
# mapping, resulting in children being correctly set on the roots.
- menu_items.extend(menu_roots)
- menu_root['all_menu_ids'] = menu_ids # includes menu_root_ids!
+ menu_items.extend(menu_roots_data)
+ menu_root['all_menu_ids'] = menus.ids # includes menu_roots!
# make a tree using parent_id
- menu_items_map = dict(
- (menu_item["id"], menu_item) for menu_item in menu_items)
+ menu_items_map = {menu_item["id"]: menu_item for menu_item in menu_items}
for menu_item in menu_items:
- if menu_item['parent_id']:
- parent = menu_item['parent_id'][0]
- else:
- parent = False
+ parent = menu_item['parent_id'] and menu_item['parent_id'][0]
if parent in menu_items_map:
menu_items_map[parent].setdefault(
'children', []).append(menu_item)
# sort by sequence a tree using parent_id
for menu_item in menu_items:
- menu_item.setdefault('children', []).sort(
- key=operator.itemgetter('sequence'))
+ menu_item.setdefault('children', []).sort(key=operator.itemgetter('sequence'))
return menu_root
diff --git a/openerp/addons/base/ir/ir_ui_view.py b/openerp/addons/base/ir/ir_ui_view.py
index 59841882cd5..bc9d54a0e7f 100644
--- a/openerp/addons/base/ir/ir_ui_view.py
+++ b/openerp/addons/base/ir/ir_ui_view.py
@@ -14,20 +14,19 @@ from operator import itemgetter
import json
import werkzeug
from lxml import etree
+from lxml.builder import E
-import odoo
-from odoo import api, fields, models, tools, _
+from odoo import api, fields, models, tools, SUPERUSER_ID, _
from odoo.exceptions import ValidationError
from odoo.http import request
from odoo.modules.module import get_resource_from_path, get_resource_path
-from odoo.osv import orm, osv
-from odoo.tools import config, graph, SKIPPED_ELEMENT_TYPES
-from odoo.tools import misc
+from odoo.osv import orm
+from odoo.tools import config, graph, ConstantMapping, SKIPPED_ELEMENT_TYPES
from odoo.tools.convert import _fix_multiple_roots
from odoo.tools.parse_version import parse_version
from odoo.tools.safe_eval import safe_eval as eval
from odoo.tools.view_validation import valid_view
-from odoo.tools.translate import encode, TRANSLATED_ATTRS, xml_translate
+from odoo.tools.translate import encode, xml_translate, TRANSLATED_ATTRS
_logger = logging.getLogger(__name__)
@@ -65,34 +64,33 @@ class ViewCustom(models.Model):
user_id = fields.Many2one('res.users', string='User', index=True, required=True, ondelete='cascade')
arch = fields.Text(string='View Architecture', required=True)
- @api.depends('user_id')
+ @api.multi
def name_get(self):
return [(rec.id, rec.user_id.name) for rec in self]
@api.model
def name_search(self, name, args=None, operator='ilike', limit=100):
if name:
- self = self.search([('user_id', operator, name)] + args, limit=limit)
- return self.name_get()
+ recs = self.search([('user_id', operator, name)] + (args or []), limit=limit)
+ return recs.name_get()
return super(ViewCustom, self).name_search(name, args=args, operator=operator, limit=limit)
+ @api.model_cr_context
def _auto_init(self):
super(ViewCustom, self)._auto_init()
- self.env.cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = \'ir_ui_view_custom_user_id_ref_id\'')
- if not self.env.cr.fetchone():
- self.env.cr.execute('CREATE INDEX ir_ui_view_custom_user_id_ref_id ON ir_ui_view_custom (user_id, ref_id)')
+ self._cr.execute("SELECT indexname FROM pg_indexes WHERE indexname = 'ir_ui_view_custom_user_id_ref_id'")
+ if not self._cr.fetchone():
+ self._cr.execute("CREATE INDEX ir_ui_view_custom_user_id_ref_id ON ir_ui_view_custom (user_id, ref_id)")
def _hasclass(context, *cls):
""" Checks if the context node has all the classes passed as arguments
"""
node_classes = set(context.context_node.attrib.get('class', '').split())
-
return node_classes.issuperset(cls)
def get_view_arch_from_file(filename, xmlid):
-
doc = etree.parse(filename)
node = None
for n in doc.xpath('//*[@id="%s"]' % (xmlid)):
@@ -105,7 +103,6 @@ def get_view_arch_from_file(filename, xmlid):
if n.tag in ('template', 'record'):
node = n
break
-
if node is not None:
if node.tag == 'record':
field = node.find('field[@name="arch"]')
@@ -121,7 +118,7 @@ def get_view_arch_from_file(filename, xmlid):
node.tag = 'data'
node.attrib.pop('id', None)
return etree.tostring(node)
- _logger.warning("Could not find view arch definition in file '%s' for xmlid '%s'" % (filename, xmlid))
+ _logger.warning("Could not find view arch definition in file '%s' for xmlid '%s'", filename, xmlid)
return None
xpath_utils = etree.FunctionNamespace(None)
@@ -137,63 +134,8 @@ class View(models.Model):
# Holds the RNG schema
_relaxng_validator = None
- @api.depends('model_data_id')
- def _get_model_data(self):
- result = dict.fromkeys(self.ids, False)
- IMD = self.env['ir.model.data']
- data_ids = IMD.search_read([('res_id', 'in', self.ids), ('model', '=', 'ir.ui.view')], ['res_id'])
- result.update(map(itemgetter('res_id', 'id'), data_ids))
- return result
-
- def _resolve_external_ids(self, cr, uid, view, arch_fs):
- def replacer(m):
- xmlid = m.group('xmlid')
- if '.' not in xmlid:
- mod = view.get_external_id(cr, uid).get(view.id).split('.')[0]
- xmlid = '%s.%s' % (mod, xmlid)
- return m.group('prefix') + str(self.pool['ir.model.data'].xmlid_to_res_id(cr, uid, xmlid))
- return re.sub('(?P<prefix>[^%])%\((?P<xmlid>.*?)\)[ds]', replacer, arch_fs)
-
- @api.depends('arch')
- def _arch_get(self):
- for view in self:
- arch_fs = None
- if config['dev_mode'] and view.arch_fs and view.xml_id:
- # It is safe to split on / herebelow because arch_fs is explicitely stored with '/'
- fullpath = get_resource_path(*view.arch_fs.split('/'))
- arch_fs = get_view_arch_from_file(fullpath, view.xml_id)
- # replace %(xml_id)s, %(xml_id)d, %%(xml_id)s, %%(xml_id)d by the res_id
- arch_fs = arch_fs and self._resolve_external_ids(view, arch_fs)
- view.arch = arch_fs or view.arch_db
-
- def _arch_set(self):
- if self.arch:
- for view in self:
- data = dict(arch_db=self.arch)
- key = 'install_mode_data'
- if self.env.context and key in self.env.context:
- imd = self.env.context[key]
- if self._model._name == imd['model'] and (not view.xml_id or view.xml_id == imd['xml_id']):
- # we store the relative path to the resource instead of the absolute path, if found
- # (it will be missing e.g. when importing data-only modules using base_import_module)
- path_info = get_resource_from_path(imd['xml_file'])
- if path_info:
- data['arch_fs'] = '/'.join(path_info[0:2])
- self.write(data)
-
- return True
-
- @api.depends('arch_base')
- def _arch_base_get(self):
- """ Return the field 'arch' without translation. """
- return self.with_context(lang=None)._arch_get()
-
- def _arch_base_set(self):
- """ Assign the field 'arch' without translation. """
- return self.with_context(lang=None)._arch_set()
-
name = fields.Char(string='View Name', required=True)
- model = fields.Char(string='Object', index=True)
+ model = fields.Char(index=True)
key = fields.Char()
priority = fields.Integer(string='Sequence', default=16, required=True)
type = fields.Selection([('tree', 'Tree'),
@@ -207,18 +149,16 @@ class View(models.Model):
('sales_team_dashboard', 'Sales Team Dashboard'),
('search', 'Search'),
('qweb', 'QWeb')], string='View Type')
- arch = fields.Text(compute='_arch_get', inverse='_arch_set', string='View Architecture', nodrop=True)
- arch_base = fields.Text(compute='_arch_base_get', inverse='_arch_base_set', string='View Architecture')
+ arch = fields.Text(compute='_compute_arch', inverse='_inverse_arch', string='View Architecture', nodrop=True)
+ arch_base = fields.Text(compute='_compute_arch_base', inverse='_inverse_arch_base', string='View Architecture')
arch_db = fields.Text(string='Arch Blob', translate=xml_translate, oldname='arch')
arch_fs = fields.Char(string='Arch Filename')
inherit_id = fields.Many2one('ir.ui.view', string='Inherited View', ondelete='restrict', index=True)
inherit_children_ids = fields.One2many('ir.ui.view', 'inherit_id', string='Views which inherit from this one')
field_parent = fields.Char(string='Child Field')
- model_data_id = fields.Many2one('ir.model.data', compute='_get_model_data', string="Model Data", store=True)
- # xml_id = fields.Char(compute=model.Model.get_xml_id, string="External ID",
- # help="ID of the view defined in xml file")
- _columns = {'xml_id': odoo.osv.fields.function(osv.osv.get_xml_id, type='char', size=128, string="External ID",
- help="ID of the view defined in xml file")}
+ model_data_id = fields.Many2one('ir.model.data', compute='_compute_model_data_id', string="Model Data", store=True)
+ xml_id = fields.Char(string="External ID", compute='_compute_xml_id',
+ help="ID of the view defined in xml file")
groups_id = fields.Many2many('res.groups', 'ir_ui_view_group_rel', 'view_id', 'group_id',
string='Groups', help="If this field is empty, the view applies to all users. Otherwise, the view applies to the users of those groups only.")
model_ids = fields.One2many('ir.model.data', 'res_id', domain=[('model', '=', 'ir.ui.view')], auto_join=True)
@@ -243,19 +183,75 @@ actual arch.
* if False, the view currently does not extend its parent but can be enabled
""")
+ @api.depends('arch_db', 'arch_fs')
+ def _compute_arch(self):
+ def resolve_external_ids(arch_fs, view_xml_id):
+ def replacer(m):
+ xmlid = m.group('xmlid')
+ if '.' not in xmlid:
+ xmlid = '%s.%s' % (view_xml_id.split('.')[0], xmlid)
+ return m.group('prefix') + str(self.env['ir.model.data'].xmlid_to_res_id(xmlid))
+ return re.sub(r'(?P<prefix>[^%])%\((?P<xmlid>.*?)\)[ds]', replacer, arch_fs)
+
+ for view in self:
+ arch_fs = None
+ if config['dev_mode'] and view.arch_fs and view.xml_id:
+ # It is safe to split on / herebelow because arch_fs is explicitely stored with '/'
+ fullpath = get_resource_path(*view.arch_fs.split('/'))
+ arch_fs = get_view_arch_from_file(fullpath, view.xml_id)
+ # replace %(xml_id)s, %(xml_id)d, %%(xml_id)s, %%(xml_id)d by the res_id
+ arch_fs = arch_fs and resolve_external_ids(arch_fs, view.xml_id)
+ view.arch = arch_fs or view.arch_db
+
+ def _inverse_arch(self):
+ for view in self:
+ data = dict(arch_db=view.arch)
+ if 'install_mode_data' in self._context:
+ imd = self._context['install_mode_data']
+ if self._name == imd['model'] and (not view.xml_id or view.xml_id == imd['xml_id']):
+ # we store the relative path to the resource instead of the absolute path, if found
+ # (it will be missing e.g. when importing data-only modules using base_import_module)
+ path_info = get_resource_from_path(imd['xml_file'])
+ if path_info:
+ data['arch_fs'] = '/'.join(path_info[0:2])
+ view.write(data)
+
+ @api.depends('arch')
+ def _compute_arch_base(self):
+ # 'arch_base' is the same as 'arch' without translation
+ for view, view_wo_lang in zip(self, self.with_context(lang=None)):
+ view.arch_base = view_wo_lang.arch
+
+ def _inverse_arch_base(self):
+ for view, view_wo_lang in zip(self, self.with_context(lang=None)):
+ view_wo_lang.arch = view.arch_base
+
+ def _compute_model_data_id(self):
+ # get the last ir_model_data record corresponding to self
+ domain = [('model', '=', 'ir.ui.view'), ('res_id', 'in', self.ids)]
+ for data in self.env['ir.model.data'].search_read(domain, ['res_id']):
+ view = self.browse(data['res_id'])
+ view.model_data_id = data['id']
+
+ def _compute_xml_id(self):
+ xml_ids = collections.defaultdict(list)
+ domain = [('model', '=', 'ir.ui.view'), ('res_id', 'in', self.ids)]
+ for data in self.env['ir.model.data'].search_read(domain, ['module', 'name', 'res_id']):
+ xml_ids[data['res_id']].append("%s.%s" % (data['module'], data['name']))
+ for view in self:
+ view.xml_id = xml_ids.get(view.id, [''])[0]
+
def _relaxng(self):
if not self._relaxng_validator:
- frng = tools.file_open(os.path.join('base', 'rng', 'view.rng'))
- try:
- relaxng_doc = etree.parse(frng)
- self._relaxng_validator = etree.RelaxNG(relaxng_doc)
- except Exception:
- _logger.exception('Failed to load RelaxNG XML schema for views validation')
- finally:
- frng.close()
+ with tools.file_open(os.path.join('base', 'rng', 'view.rng')) as frng:
+ try:
+ relaxng_doc = etree.parse(frng)
+ self._relaxng_validator = etree.RelaxNG(relaxng_doc)
+ except Exception:
+ _logger.exception('Failed to load RelaxNG XML schema for views validation')
return self._relaxng_validator
- def _valid_inheritance(self, view, arch):
+ def _valid_inheritance(self, arch):
""" Check whether view inheritance is based on translated attribute. """
for node in arch.xpath('//*[@position]'):
# inheritance may not use a translated attribute as selector
@@ -263,23 +259,22 @@ actual arch.
match = TRANSLATED_ATTRS_RE.search(node.get('expr', ''))
if match:
message = "View inheritance may not use attribute %r as a selector." % match.group(1)
- self.raise_view_error(view._cr, view._uid, message, view.id)
+ self.raise_view_error(message, self.id)
else:
for attr in TRANSLATED_ATTRS:
if node.get(attr):
message = "View inheritance may not use attribute %r as a selector." % attr
- self.raise_view_error(view._cr, view._uid, message, view.id)
+ self.raise_view_error(message, self.id)
return True
@api.constrains('arch', 'arch_base')
def _check_xml(self):
-
# Sanity checks: the view should not break anything upon rendering!
# Any exception raised below will cause a transaction rollback.
for view in self:
view_arch = etree.fromstring(encode(view.arch))
- self._valid_inheritance(view, view_arch)
- view_def = self.read_combined(view.id, ['arch'])
+ view._valid_inheritance(view_arch)
+ view_def = view.read_combined(['arch'])
view_arch_utf8 = view_def['arch']
if view.type != 'qweb':
view_doc = etree.fromstring(view_arch_utf8)
@@ -308,16 +303,16 @@ actual arch.
" extend an other view"),
]
+ @api.model_cr_context
def _auto_init(self):
super(View, self)._auto_init()
- self.env.cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = \'ir_ui_view_model_type_inherit_id\'')
- if not self.env.cr.fetchone():
- self.env.cr.execute('CREATE INDEX ir_ui_view_model_type_inherit_id ON ir_ui_view (model, inherit_id)')
+ self._cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = \'ir_ui_view_model_type_inherit_id\'')
+ if not self._cr.fetchone():
+ self._cr.execute('CREATE INDEX ir_ui_view_model_type_inherit_id ON ir_ui_view (model, inherit_id)')
def _compute_defaults(self, values):
if 'inherit_id' in values:
- values.setdefault(
- 'mode', 'extension' if values['inherit_id'] else 'primary')
+ values.setdefault('mode', 'extension' if values['inherit_id'] else 'primary')
return values
@api.model
@@ -338,7 +333,7 @@ actual arch.
def write(self, vals):
# If view is modified we remove the arch_fs information thus activating the arch_db
# version. An `init` of the view will restore the arch_fs for the --dev mode
- if 'arch' in vals and 'install_mode_data' not in self.env.context:
+ if 'arch' in vals and 'install_mode_data' not in self._context:
vals['arch_fs'] = False
# drop the corresponding view customizations (used for dashboards for example), otherwise
@@ -348,8 +343,7 @@ actual arch.
custom_view.unlink()
self.clear_caches()
- res = super(View, self).write(self._compute_defaults(vals))
- return res
+ return super(View, self).write(self._compute_defaults(vals))
@api.multi
def toggle(self):
@@ -369,10 +363,8 @@ actual arch.
:return: id of the default view of False if none found
:rtype: int
"""
- rec = self.search([('model', '=', model), ('type', '=', view_type), ('mode', '=', 'primary')], limit=1)
- if not rec:
- return False
- return rec.id
+ domain = [('model', '=', model), ('type', '=', view_type), ('mode', '=', 'primary')]
+ return self.search(domain, limit=1).id
#------------------------------------------------------
# Inheritance mecanism
@@ -391,8 +383,7 @@ actual arch.
:rtype: list of tuples
:return: [(view_arch,view_id), ...]
"""
-
- user_groups = frozenset(self.env.user.groups_id or ())
+ user_groups = self.env.user.groups_id
conditions = [
['inherit_id', '=', view_id],
@@ -400,36 +391,37 @@ actual arch.
['mode', '=', 'extension'],
['active', '=', True],
]
- if self.pool._init and not self.env.context.get('load_all_views'):
+ if self.pool._init and not self._context.get('load_all_views'):
# Module init currently in progress, only consider views from
# modules whose code is already loaded
conditions.extend([
'|',
- ['model_ids.module', 'in', tuple(self.pool._init_modules)],
- ['id', 'in', self.env.context.get('check_view_ids') or (0,)],
+ ('model_ids.module', 'in', tuple(self.pool._init_modules)),
+ ('id', 'in', self._context.get('check_view_ids') or (0,)),
])
- view_ids = self.search(conditions)
+ views = self.search(conditions)
return [(view.arch, view.id)
- for view in view_ids
- if not (view.groups_id and user_groups.isdisjoint(view.groups_id))]
+ for view in views.sudo()
+ if not view.groups_id or (view.groups_id & user_groups)]
@api.model
def raise_view_error(self, message, view_id):
view = self.browse(view_id)
not_avail = _('n/a')
- message = ("%(msg)s\n\n" +
- _("Error context:\nView `%(view_name)s`") +
- "\n[view_id: %(viewid)s, xml_id: %(xmlid)s, "
- "model: %(model)s, parent_id: %(parent)s]") % \
- {
- 'view_name': view.name or not_avail,
- 'viewid': view_id or not_avail,
- 'xmlid': view.xml_id or not_avail,
- 'model': view.model or not_avail,
- 'parent': view.inherit_id.id or not_avail,
- 'msg': message,
- }
+ message = (
+ "%(msg)s\n\n" +
+ _("Error context:\nView `%(view_name)s`") +
+ "\n[view_id: %(viewid)s, xml_id: %(xmlid)s, "
+ "model: %(model)s, parent_id: %(parent)s]"
+ ) % {
+ 'view_name': view.name or not_avail,
+ 'viewid': view_id or not_avail,
+ 'xmlid': view.xml_id or not_avail,
+ 'model': view.model or not_avail,
+ 'parent': view.inherit_id.id or not_avail,
+ 'msg': message,
+ }
_logger.info(message)
raise AttributeError(message)
@@ -479,7 +471,6 @@ actual arch.
node.set('data-oe-xpath', xpath)
node.set('data-oe-model', 'ir.ui.view')
node.set('data-oe-field', 'arch')
-
return specs_tree
@api.model
@@ -576,14 +567,14 @@ actual arch.
sql_inherit = self.get_inheriting_views_arch(source_id, model)
for (specs, view_id) in sql_inherit:
specs_tree = etree.fromstring(specs.encode('utf-8'))
- if self.env.context.get('inherit_branding'):
+ if self._context.get('inherit_branding'):
self.inherit_branding(specs_tree, view_id, root_id)
source = self.apply_inheritance_specs(source, specs_tree, view_id)
source = self.apply_view_inheritance(source, view_id, model, root_id=root_id)
return source
- @api.model
- def read_combined(self, view_id, fields=None):
+ @api.multi
+ def read_combined(self, fields=None):
"""
Utility function to get a view combined with its inherited views.
@@ -593,33 +584,35 @@ actual arch.
.. note:: ``arch`` is always added to the fields list even if not
requested (similar to ``id``)
"""
- ctx = self.env.context.copy()
+ # introduce check_view_ids in context
+ if 'check_view_ids' not in self._context:
+ self = self.with_context(check_view_ids=[])
+
+ check_view_ids = self._context['check_view_ids']
# if view_id is not a root view, climb back to the top.
- base = v = self.browse(view_id)
- check_view_ids = ctx.setdefault('check_view_ids', [])
- while v.mode != 'primary':
+ root = self
+ while root.mode != 'primary':
# Add inherited views to the list of loading forced views
# Otherwise, inherited views could not find elements created in their direct parents if that parent is defined in the same module
- check_view_ids.append(v.id)
- v = v.inherit_id
- root = v
+ check_view_ids.append(root.id)
+ root = root.inherit_id
# arch and model fields are always returned
if fields:
fields = list({'arch', 'model'}.union(fields))
# read the view arch
- [view] = root.read(fields=fields)
- view_arch = etree.fromstring(view['arch'].encode('utf-8'))
- if not v.inherit_id:
+ [view_data] = root.read(fields=fields)
+ view_arch = etree.fromstring(view_data['arch'].encode('utf-8'))
+ if not root.inherit_id:
arch_tree = view_arch
else:
- parent_view = self.read_combined(v.inherit_id.id, fields=fields)
+ parent_view = root.inherit_id.read_combined(fields=fields)
arch_tree = etree.fromstring(parent_view['arch'])
arch_tree = self.apply_inheritance_specs(arch_tree, view_arch, parent_view['id'])
- if self.env.context.get('inherit_branding'):
+ if self._context.get('inherit_branding'):
arch_tree.attrib.update({
'data-oe-model': 'ir.ui.view',
'data-oe-id': str(root.id),
@@ -627,9 +620,9 @@ actual arch.
})
# and apply inheritance
- arch = self.apply_view_inheritance(arch_tree, root.id, base.model)
+ arch = self.apply_view_inheritance(arch_tree, root.id, self.model)
- return dict(view, arch=etree.tostring(arch, encoding='utf-8'))
+ return dict(view_data, arch=etree.tostring(arch, encoding='utf-8'))
#------------------------------------------------------
# Postprocessing: translation, groups and modifiers
@@ -654,14 +647,9 @@ actual arch.
children = True
modifiers = {}
- Model = self.env[model]
- if Model is None:
+ if model not in self.env:
self.raise_view_error(_('Model not found: %(model)s') % dict(model=model), view_id)
-
- def encode(s):
- if isinstance(s, unicode):
- return s.encode('utf8')
- return s
+ Model = self.env[model]
def check_group(node):
"""Apply group restrictions, may be set at view level or model level::
@@ -686,26 +674,19 @@ actual arch.
node.set('invisible', '1')
modifiers['invisible'] = True
if 'attrs' in node.attrib:
- del(node.attrib['attrs']) # avoid making field visible later
- del(node.attrib['groups'])
+ del node.attrib['attrs'] # avoid making field visible later
+ del node.attrib['groups']
return True
if node.tag in ('field', 'node', 'arrow'):
if node.get('object'):
attrs = {}
views = {}
- xml = "<form>"
- for f in node:
- if f.tag == 'field':
- xml += etree.tostring(f, encoding="utf-8")
- xml += "</form>"
- new_xml = etree.fromstring(encode(xml))
- ctx = self.env.context.copy()
- ctx['base_model_name'] = model
- xarch, xfields = self.with_context(ctx).postprocess_and_fields(node.get('object'), new_xml, view_id)
+ xml_form = E.form(*(f for f in node if f.tag == 'field'))
+ xarch, xfields = self.with_context(base_model_name=model).postprocess_and_fields(node.get('object'), xml_form, view_id)
views['form'] = {
'arch': xarch,
- 'fields': xfields
+ 'fields': xfields,
}
attrs = {'views': views}
fields = xfields
@@ -718,18 +699,16 @@ actual arch.
for f in node:
if f.tag in ('form', 'tree', 'graph', 'kanban', 'calendar'):
node.remove(f)
- ctx = self.env.context.copy()
- ctx['base_model_name'] = model
- xarch, xfields = self.with_context(ctx).postprocess_and_fields(field.comodel_name, f, view_id)
+ xarch, xfields = self.with_context(base_model_name=model).postprocess_and_fields(field.comodel_name, f, view_id)
views[str(f.tag)] = {
'arch': xarch,
- 'fields': xfields
+ 'fields': xfields,
}
attrs = {'views': views}
- Relation = self.pool.get(field.comodel_name)
- if Relation and field.type in ('many2one', 'many2many'):
- node.set('can_create', 'true' if Relation.check_access_rights(self._cr, self._uid, 'create', raise_exception=False) else 'false')
- node.set('can_write', 'true' if Relation.check_access_rights(self._cr, self._uid, 'write', raise_exception=False) else 'false')
+ if field.comodel_name in self.env and field.type in ('many2one', 'many2many'):
+ Comodel = self.env[field.comodel_name]
+ node.set('can_create', 'true' if Comodel.check_access_rights('create', raise_exception=False) else 'false')
+ node.set('can_write', 'true' if Comodel.check_access_rights('write', raise_exception=False) else 'false')
fields[node.get('name')] = attrs
field = model_fields.get(node.get('name'))
@@ -753,7 +732,7 @@ actual arch.
# The view architeture overrides the python model.
# Get the attrs before they are (possibly) deleted by check_group below
- orm.transfer_node_to_modifiers(node, modifiers, self.env.context, in_tree_view)
+ orm.transfer_node_to_modifiers(node, modifiers, self._context, in_tree_view)
for f in node:
if children or (node.tag == 'field' and f.tag in ('filter', 'separator')):
@@ -792,27 +771,28 @@ actual arch.
return arch
+ @api.model
def _disable_workflow_buttons(self, model, node):
""" Set the buttons in node to readonly if the user can't activate them. """
- if model is None or self.env.user.id == 1:
+ if model is None or self.env.user.id == SUPERUSER_ID:
# admin user can always activate workflow buttons
return node
# TODO handle the case of more than one workflow for a model or multiple
# transitions with different groups and same signal
+ user_group_ids = set(self.env.user.groups_id.ids)
buttons = (n for n in node.getiterator('button') if n.get('type') != 'object')
for button in buttons:
- user_groups = self.env.user.groups_id
- self.env.cr.execute("""SELECT DISTINCT t.group_id
- FROM wkf
- INNER JOIN wkf_activity a ON a.wkf_id = wkf.id
- INNER JOIN wkf_transition t ON (t.act_to = a.id)
- WHERE wkf.osv = %s
- AND t.signal = %s
- AND t.group_id is NOT NULL
- """, (model, button.get('name')))
- group_ids = [x[0] for x in self.env.cr.fetchall() if x[0]]
- can_click = not group_ids or bool(set(user_groups).intersection(group_ids))
+ query = """SELECT DISTINCT t.group_id
+ FROM wkf
+ INNER JOIN wkf_activity a ON a.wkf_id = wkf.id
+ INNER JOIN wkf_transition t ON (t.act_to = a.id)
+ WHERE wkf.osv = %s
+ AND t.signal = %s
+ AND t.group_id is NOT NULL"""
+ self._cr.execute(query, (model, button.get('name')))
+ group_ids = set(row[0] for row in self._cr.fetchall() if row[0])
+ can_click = not group_ids or bool(user_group_ids & group_ids)
button.set('readonly', str(int(not can_click)))
return node
@@ -829,18 +809,18 @@ actual arch.
"""
fields = {}
- Model = self.env[model]
- if Model is None:
+ if model not in self.env:
self.raise_view_error(_('Model not found: %(model)s') % dict(model=model), view_id)
+ Model = self.env[model]
if node.tag == 'diagram':
if node.getchildren()[0].tag == 'node':
node_model = self.env[node.getchildren()[0].get('object')]
node_fields = node_model.fields_get(None)
fields.update(node_fields)
- if not node.get("create") and \
- not node_model.check_access_rights('create', raise_exception=False) or \
- not self.env.context.get("create", True):
+ if (not node.get("create") and
+ not node_model.check_access_rights('create', raise_exception=False) or
+ not self._context.get("create", True)):
node.set("create", 'false')
if node.getchildren()[1].tag == 'arrow':
arrow_fields = self.env[node.getchildren()[1].get('object')].fields_get(None)
@@ -853,20 +833,20 @@ actual arch.
node = self._disable_workflow_buttons(model, node)
if node.tag in ('kanban', 'tree', 'form', 'gantt'):
for action, operation in (('create', 'create'), ('delete', 'unlink'), ('edit', 'write')):
- if not node.get(action) and \
- not Model.check_access_rights(operation, raise_exception=False) or \
- not self.env.context.get(action, True):
+ if (not node.get(action) and
+ not Model.check_access_rights(operation, raise_exception=False) or
+ not self._context.get(action, True)):
node.set(action, 'false')
if node.tag in ('kanban',):
group_by_name = node.get('default_group_by')
if group_by_name in Model._fields:
group_by_field = Model._fields[group_by_name]
if group_by_field.type == 'many2one':
- group_by_model = Model.pool[group_by_field.comodel_name]
+ group_by_model = Model.env[group_by_field.comodel_name]
for action, operation in (('group_create', 'create'), ('group_delete', 'unlink'), ('group_edit', 'write')):
- if not node.get(action) and \
- not group_by_model.check_access_rights(operation, raise_exception=False) or \
- not self.env.context.get(action, True):
+ if (not node.get(action) and
+ not group_by_model.check_access_rights(operation, raise_exception=False) or
+ not self._context.get(action, True)):
node.set(action, 'false')
arch = etree.tostring(node, encoding="utf-8").replace('\t', '')
@@ -877,8 +857,7 @@ actual arch.
if field in fields:
fields[field].update(fields_def[field])
else:
- message = _("Field `%(field_name)s` does not exist") % \
- dict(field_name=field)
+ message = _("Field `%(field_name)s` does not exist") % dict(field_name=field)
self.raise_view_error(message, view_id)
return arch, fields
@@ -886,30 +865,40 @@ actual arch.
# QWeb template views
#------------------------------------------------------
+ def _read_template_keys(self):
+ """ Return the list of context keys to use for caching ``_read_template``. """
+ return ['lang', 'inherit_branding', 'editable', 'translatable', 'edit_translations']
+
# apply ormcache_context decorator unless in dev mode...
- @tools.conditional(not config['dev_mode'],
- tools.ormcache_context('uid', 'view_id',
- keys=('lang', 'inherit_branding', 'editable', 'translatable', 'edit_translations')))
- def _read_template(self, cr, uid, view_id, context=None):
- arch = self.read_combined(cr, uid, view_id, fields=['arch'], context=context)['arch']
+ @api.model
+ @tools.conditional(
+ not config['dev_mode'],
+ tools.ormcache('self._uid', 'view_id',
+ 'tuple(map(self._context.get, self._read_template_keys()))'),
+ )
+ def _read_template(self, view_id):
+ arch = self.browse(view_id).read_combined(['arch'])['arch']
arch_tree = etree.fromstring(arch)
self.distribute_branding(arch_tree)
- root = etree.Element('templates')
- root.append(arch_tree)
+ root = E.templates(arch_tree)
arch = etree.tostring(root, encoding='utf-8', xml_declaration=True)
return arch
- def read_template(self, cr, uid, xml_id, context=None):
- if isinstance(xml_id, (int, long)):
- view_id = xml_id
- else:
- if '.' not in xml_id:
- raise ValueError('Invalid template id: %r' % (xml_id,))
- view_id = self.get_view_id(cr, uid, xml_id, context=context)
- return self._read_template(cr, uid, view_id, context=context)
+ @api.model
+ def read_template(self, xml_id):
+ return self._read_template(self.get_view_id(xml_id))
- def get_view_id(self, cr, uid, xml_id, context=None):
- return self.pool['ir.model.data'].xmlid_to_res_id(cr, uid, xml_id, raise_if_not_found=True)
+ @api.model
+ def get_view_id(self, template):
+ """ Return the view ID corresponding to ``template``, which may be a
+ view ID or an XML ID. Note that this method may be overridden for other
+ kinds of template values.
+ """
+ if isinstance(template, (int, long)):
+ return template
+ if '.' not in template:
+ raise ValueError('Invalid template id: %r' % template)
+ return self.env['ir.model.data'].xmlid_to_res_id(template, raise_if_not_found=True)
def clear_cache(self):
""" Deprecated, use `clear_caches` instead. """
@@ -929,12 +918,13 @@ actual arch.
return distributed_branding
def distribute_branding(self, e, branding=None, parent_xpath='',
- index_map=misc.ConstantMapping(1)):
+ index_map=ConstantMapping(1)):
if e.get('t-ignore') or e.tag == 'head':
# remove any view branding possibly injected by inheritance
attrs = set(MOVABLE_BRANDING)
for descendant in e.iterdescendants(tag=etree.Element):
- if not attrs.intersection(descendant.attrib): continue
+ if not attrs.intersection(descendant.attrib):
+ continue
self._pop_view_branding(descendant)
# TODO: find a better name and check if we have a string to boolean helper
return
@@ -945,7 +935,8 @@ actual arch.
if branding and not (e.get('data-oe-model') or e.get('t-field')):
e.attrib.update(branding)
e.set('data-oe-xpath', node_path)
- if not e.get('data-oe-model'): return
+ if not e.get('data-oe-model'):
+ return
if {'t-esc', 't-raw'}.intersection(e.attrib):
# nodes which fully generate their content and have no reason to
@@ -986,27 +977,28 @@ actual arch.
for attr in node.attrib
)
- def translate_qweb(self, cr, uid, id_, arch, lang, context=None):
+ @api.multi
+ def translate_qweb(self, arch, lang):
# Deprecated: templates are translated once read from database
return arch
- @odoo.tools.ormcache('self.env.uid', 'self.id')
+ @api.multi
+ @tools.ormcache('self._uid', 'self.id')
def get_view_xmlid(self):
- xmlid = self.env['ir.model.data'].search([('model', '=', 'ir.ui.view'), ('res_id', '=', id)])
- return '%s.%s' % (xmlid.module, xmlid.name)
+ domain = [('model', '=', 'ir.ui.view'), ('res_id', '=', self.id)]
+ xmlid = self.env['ir.model.data'].search_read(domain, ['module', 'name'])[0]
+ return '%s.%s' % (xmlid['module'], xmlid['name'])
- @api.cr_uid_ids_context
- def render(self, cr, uid, id_or_xml_id, values=None, engine='ir.qweb', context=None):
- if isinstance(id_or_xml_id, list):
- id_or_xml_id = id_or_xml_id[0]
+ @api.model
+ def render_template(self, template, values=None, engine='ir.qweb'):
+ return self.browse(self.get_view_id(template)).render(values, engine)
- if not context:
- context = {}
+ @api.multi
+ def render(self, values=None, engine='ir.qweb'):
+ assert isinstance(self.id, (int, long))
- if values is None:
- values = dict()
qcontext = dict(
- env=api.Environment(cr, uid, context),
+ env=self.env,
keep_query=keep_query,
request=request, # might be unbound if we're not in an httprequest context
debug=request.debug if request else False,
@@ -1016,22 +1008,19 @@ actual arch.
datetime=datetime,
relativedelta=relativedelta,
)
- qcontext.update(values)
+ qcontext.update(values or {})
# TODO: This helper can be used by any template that wants to embedd the backend.
# It is currently necessary because the ir.ui.view bundle inheritance does not
# match the module dependency graph.
def get_modules_order():
if request:
- from openerp.addons.web.controllers.main import module_boot
+ from odoo.addons.web.controllers.main import module_boot
return json.dumps(module_boot())
return '[]'
qcontext['get_modules_order'] = get_modules_order
- def loader(name):
- return self.read_template(cr, uid, name, context=context)
-
- return self.pool[engine].render(cr, uid, id_or_xml_id, qcontext, loader=loader, context=context)
+ return self.env[engine].render(self.id, qcontext, loader=self.read_template)
#------------------------------------------------------
# Misc
@@ -1043,7 +1032,12 @@ actual arch.
return self.env['ir.translation'].translate_fields('ir.ui.view', self.id, 'arch_db')
@api.model
- def graph_get(self, model, node_obj, conn_obj, src_node, des_node, label, scale):
+ def graph_get(self, id, model, node_obj, conn_obj, src_node, des_node, label, scale):
+ def rec_name(rec):
+ return (rec.name if 'name' in rec else
+ rec.x_name if 'x_name' in rec else
+ None)
+
nodes = []
nodes_name = []
transitions = []
@@ -1053,56 +1047,57 @@ actual arch.
no_ancester = []
blank_nodes = []
- _Model_Obj = self.env[model]
- _Node_Obj = self.env[node_obj]
-
- for model_key, model_value in _Model_Obj._columns.items():
- if model_value._type == 'one2many':
- if model_value._obj == node_obj:
- _Node_Field = model_key
- _Model_Field = model_value._fields_id
- for node_key, node_value in _Node_Obj._columns.items():
- if node_value._type == 'one2many':
- if node_value._obj == conn_obj:
- # _Source_Field = "Incoming Arrows" (connected via des_node)
- if node_value._fields_id == des_node:
- _Source_Field = node_key
- # _Destination_Field = "Outgoing Arrows" (connected via src_node)
- if node_value._fields_id == src_node:
- _Destination_Field = node_key
-
- datas = _Model_Obj.read()
- for a in _Node_Obj.read(datas[_Node_Field]):
- if a[_Source_Field] or a[_Destination_Field]:
- nodes_name.append((a['id'], a['name'] if 'name' in a else a.get('x_name')))
- nodes.append(a['id'])
+ Model = self.env[model]
+ Node = self.env[node_obj]
+
+ for model_key, model_value in Model._fields.iteritems():
+ if model_value.type == 'one2many':
+ if model_value.comodel_name == node_obj:
+ _Node_Field = model_key
+ _Model_Field = model_value.inverse_name
+ for node_key, node_value in Node._fields.iteritems():
+ if node_value.type == 'one2many':
+ if node_value.comodel_name == conn_obj:
+ # _Source_Field = "Incoming Arrows" (connected via des_node)
+ if node_value.inverse_name == des_node:
+ _Source_Field = node_key
+ # _Destination_Field = "Outgoing Arrows" (connected via src_node)
+ if node_value.inverse_name == src_node:
+ _Destination_Field = node_key
+
+ record = Model.browse(id)
+ for line in record[_Node_Field]:
+ if line[_Source_Field] or line[_Destination_Field]:
+ nodes_name.append((line.id, rec_name(line)))
+ nodes.append(line.id)
else:
- blank_nodes.append({'id': a['id'], 'name': a['name'] if 'name' in a else a.get('x_name')})
+ blank_nodes.append({'id': line.id, 'name': rec_name(line)})
- if a.has_key('flow_start') and a['flow_start']:
- start.append(a['id'])
- else:
- if not a[_Source_Field]:
- no_ancester.append(a['id'])
- for t in self.env[conn_obj].read(a[_Destination_Field]):
- transitions.append((a['id'], t[des_node][0]))
- tres[str(t['id'])] = (a['id'], t[des_node][0])
+ if 'flow_start' in line and line.flow_start:
+ start.append(line.id)
+ elif not line[_Source_Field]:
+ no_ancester.append(line.id)
+
+ for t in line[_Destination_Field]:
+ transitions.append((line.id, t[des_node].id))
+ tres[str(t['id'])] = (line.id, t[des_node].id)
label_string = ""
if label:
for lbl in eval(label):
- if t.has_key(tools.ustr(lbl)) and tools.ustr(t[lbl]) == 'False':
+ if tools.ustr(lbl) in t and tools.ustr(t[lbl]) == 'False':
label_string += ' '
else:
label_string = label_string + " " + tools.ustr(t[lbl])
- labels[str(t['id'])] = (a['id'], label_string)
+ labels[str(t['id'])] = (line.id, label_string)
+
g = graph(nodes, transitions, no_ancester)
g.process(start)
g.scale(*scale)
result = g.result_get()
results = {}
- for node in nodes_name:
- results[str(node[0])] = result[node[0]]
- results[str(node[0])]['name'] = node[1]
+ for node_id, node_name in nodes_name:
+ results[str(node_id)] = result[node_id]
+ results[str(node_id)]['name'] = node_name
return {'nodes': results,
'transitions': tres,
'label': labels,
@@ -1114,16 +1109,16 @@ actual arch.
"""Validate architecture of custom views (= without xml id) for a given model.
This method is called at the end of registry update.
"""
- self.env.cr.execute("""SELECT max(v.id)
- FROM ir_ui_view v
- LEFT JOIN ir_model_data md ON (md.model = 'ir.ui.view' AND md.res_id = v.id)
- WHERE md.module IS NULL
- AND v.model = %s
- AND v.active = true
- GROUP BY coalesce(v.inherit_id, v.id)
- """, (model,))
-
- rec = self.browse(map(itemgetter(0), self.env.cr.fetchall()))
+ query = """SELECT max(v.id)
+ FROM ir_ui_view v
+ LEFT JOIN ir_model_data md ON (md.model = 'ir.ui.view' AND md.res_id = v.id)
+ WHERE md.module IS NULL
+ AND v.model = %s
+ AND v.active = true
+ GROUP BY coalesce(v.inherit_id, v.id)"""
+ self._cr.execute(query, [model])
+
+ rec = self.browse(map(itemgetter(0), self._cr.fetchall()))
return rec.with_context({'load_all_views': True})._check_xml()
@api.model
@@ -1135,19 +1130,25 @@ actual arch.
if self.pool._init:
# only validate the views that are still existing...
xmlid_filter = "AND md.name IN %s"
- names = tuple(name for (xmod, name), (model, res_id) in self.pool.model_data_reference_ids.items() if xmod == module and model == self._name)
+ names = tuple(
+ name
+ for (xmod, name), (model, res_id) in self.pool.model_data_reference_ids.items()
+ if xmod == module and model == self._name
+ )
if not names:
# no views for this module, nothing to validate
return
params += (names,)
- self.env.cr.execute("""SELECT max(v.id)
- FROM ir_ui_view v
- LEFT JOIN ir_model_data md ON (md.model = 'ir.ui.view' AND md.res_id = v.id)
- WHERE md.module = %s
- {0}
- GROUP BY coalesce(v.inherit_id, v.id)
- """.format(xmlid_filter), params)
-
- for vid, in self.env.cr.fetchall():
- if not self._check_xml():
+
+ query = """SELECT max(v.id)
+ FROM ir_ui_view v
+ LEFT JOIN ir_model_data md ON (md.model = 'ir.ui.view' AND md.res_id = v.id)
+ WHERE md.module = %s {0}
+ GROUP BY coalesce(v.inherit_id, v.id)""".format(xmlid_filter)
+ self._cr.execute(query, params)
+
+ for vid, in self._cr.fetchall():
+ try:
+ self.browse(vid)._check_xml()
+ except Exception:
self.raise_view_error("Can't validate view", vid)
diff --git a/openerp/addons/base/ir/ir_values.py b/openerp/addons/base/ir/ir_values.py
index 84f3990c182..3af0f92249f 100644
--- a/openerp/addons/base/ir/ir_values.py
+++ b/openerp/addons/base/ir/ir_values.py
@@ -11,12 +11,12 @@ EXCLUDED_FIELDS = set((
#: Possible slots to bind an action to with :meth:`~.set_action`
ACTION_SLOTS = [
- "client_action_multi", # sidebar wizard action
- "client_print_multi", # sidebar report printing button
- "client_action_relate", # sidebar related link
- "tree_but_open", # double-click on item in tree view
- "tree_but_action", # deprecated: same as tree_but_open
- ]
+ "client_action_multi", # sidebar wizard action
+ "client_print_multi", # sidebar report printing button
+ "client_action_relate", # sidebar related link
+ "tree_but_open", # double-click on item in tree view
+ "tree_but_action", # deprecated: same as tree_but_open
+]
class IrValues(models.Model):
@@ -68,24 +68,24 @@ class IrValues(models.Model):
users, and set by their UI clients calling :meth:`~.set_default`.
These default values are then automatically used by the
ORM every time a new record is about to be created, i.e. when
- :meth:`~odoo.osv.osv.osv.default_get`
- or :meth:`~odoo.osv.osv.osv.create` are called.
+ :meth:`~odoo.models.Model.default_get`
+ or :meth:`~odoo.models.Model.create` are called.
.. rubric:: Usage: action bindings
Business applications will usually bind their actions during
installation, and Odoo UI clients will apply them as defined,
based on the list of actions included in the result of
- :meth:`~odoo.osv.osv.osv.fields_view_get`,
+ :meth:`~odoo.models.Model.fields_view_get`,
or directly returned by explicit calls to :meth:`~.get_actions`.
"""
_name = 'ir.values'
name = fields.Char(required=True)
- model = fields.Char(string='Model Name', select=True, required=True,
+ model = fields.Char(string='Model Name', index=True, required=True,
help="Model to which this entry applies")
- # TODO: model_id and action_id should be read-write function fields
+ # TODO: model_id and action_id should be read-write function fields
model_id = fields.Many2one('ir.model', string='Model (change only)',
help="Model to which this entry applies - "
"helper field for setting a model, will "
@@ -96,8 +96,8 @@ class IrValues(models.Model):
"automatically set the correct reference")
value = fields.Text(help="Default value (pickled) or reference to an action")
- value_unpickle = fields.Text(compute='_value_unpickle', inverse='_value_pickle',
- string='Default value or action reference')
+ value_unpickle = fields.Text(string='Default value or action reference',
+ compute='_value_unpickle', inverse='_value_pickle')
key = fields.Selection([('action', 'Action'), ('default', 'Default')],
string='Type', index=True, required=True, default='action',
help="- Action: an action attached to one slot of the given model\n"
@@ -117,45 +117,41 @@ class IrValues(models.Model):
company_id = fields.Many2one('res.company', string='Company', ondelete='cascade', index=True,
help="If set, action binding only applies for this company")
- @api.depends('value_unpickle')
+ @api.depends('key', 'value')
def _value_unpickle(self):
for record in self:
value = record.value
if record.key == 'default' and value:
# default values are pickled on the fly
- try:
+ with tools.ignore(Exception):
value = str(pickle.loads(value))
- except Exception:
- pass
record.value_unpickle = value
def _value_pickle(self):
- ctx = self.env.context.copy()
- if self.CONCURRENCY_CHECK_FIELD in ctx:
- del ctx[self.CONCURRENCY_CHECK_FIELD]
- if self.key == 'default':
- # default values are pickled on the fly
- value = pickle.dumps(self.value)
- self.write({value: value})
+ context = dict(self._context)
+ context.pop(self.CONCURRENCY_CHECK_FIELD, None)
+ for record in self.with_context(context):
+ value = record.value_unpickle
+ if record.key == 'default':
+ value = pickle.dumps(value)
+ record.value = value
@api.onchange('model_id')
def onchange_object_id(self):
- if not self.model_id:
- return {}
- return {'value': {'model': self.env['ir.model'].browse(self.model_id).model}}
+ if self.model_id:
+ self.model = self.model_id.model
@api.onchange('action_id')
def onchange_action_id(self):
- if not self.action_id:
- return {}
- act = self.env['ir.actions.actions'].browse(self.action_id)
- return {'value': {'value_unpickle': act.type + ',' + str(act.id)}}
+ if self.action_id:
+ self.value_unpickle = self.action_id
+ @api.model_cr_context
def _auto_init(self):
super(IrValues, self)._auto_init()
- self.env.cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = \'ir_values_key_model_key2_res_id_user_id_idx\'')
- if not self.env.cr.fetchone():
- self.env.cr.execute('CREATE INDEX ir_values_key_model_key2_res_id_user_id_idx ON ir_values (key, model, key2, res_id, user_id)')
+ self._cr.execute("SELECT indexname FROM pg_indexes WHERE indexname = 'ir_values_key_model_key2_res_id_user_id_idx'")
+ if not self._cr.fetchone():
+ self._cr.execute("CREATE INDEX ir_values_key_model_key2_res_id_user_id_idx ON ir_values (key, model, key2, res_id, user_id)")
@api.model
def create(self, vals):
@@ -173,6 +169,7 @@ class IrValues(models.Model):
return super(IrValues, self).unlink()
@api.model
+ @api.returns('self', lambda value: value.id)
def set_default(self, model, field_name, value, for_all_users=True, company_id=False, condition=False):
"""Defines a default value for the given model and field_name. Any previous
default for the same scope (model, field_name, value, for_all_users, company_id, condition)
@@ -205,7 +202,7 @@ class IrValues(models.Model):
(Currently, the condition is trimmed to 200 characters,
so values that share the same first 200 characters always
match)
- :return: id of the newly created ir.values entry
+ :return: the newly created ir.values entry
"""
if isinstance(value, unicode):
value = value.encode('utf8')
@@ -280,34 +277,33 @@ class IrValues(models.Model):
"""
# use a direct SQL query for performance reasons,
# this is called very often
- query = """SELECT v.id, v.name, v.value FROM ir_values v
- LEFT JOIN res_users u ON (v.user_id = u.id)
- WHERE v.key = %%s AND v.model = %%s
- AND (v.user_id = %%s OR v.user_id IS NULL)
- AND (v.company_id IS NULL OR
- v.company_id =
- (SELECT company_id from res_users where id = %%s)
- )
- %s
- ORDER BY v.user_id, u.company_id"""
+ query = """ SELECT v.id, v.name, v.value FROM ir_values v
+ LEFT JOIN res_users u ON (v.user_id = u.id)
+ WHERE v.key = %%s AND v.model = %%s
+ AND (v.user_id = %%s OR v.user_id IS NULL)
+ AND (v.company_id IS NULL OR
+ v.company_id = (SELECT company_id FROM res_users WHERE id = %%s)
+ )
+ %s
+ ORDER BY v.user_id, u.company_id"""
params = ('default', model, self._uid, self._uid)
if condition:
- query %= 'AND v.key2 = %s'
+ query = query % 'AND v.key2 = %s'
params += (condition[:200],)
else:
- query %= 'AND v.key2 is NULL'
- self.env.cr.execute(query, params)
+ query = query % 'AND v.key2 IS NULL'
+ self._cr.execute(query, params)
# keep only the highest priority default for each field
defaults = {}
- for row in self.env.cr.dictfetchall():
- defaults.setdefault(row['name'],
- (row['id'], row['name'], pickle.loads(row['value'].encode('utf-8'))))
+ for row in self._cr.dictfetchall():
+ value = pickle.loads(row['value'].encode('utf-8'))
+ defaults.setdefault(row['name'], (row['id'], row['name'], value))
return defaults.values()
# use ormcache: this is called a lot by BaseModel.default_get()!
@api.model
- @tools.ormcache('self.env.uid', 'model', 'condition')
+ @tools.ormcache('self._uid', 'model', 'condition')
def get_defaults_dict(self, model, condition=False):
""" Returns a dictionary mapping field names with their corresponding
default value. This method simply improves the returned value of
@@ -316,6 +312,7 @@ class IrValues(models.Model):
return dict((f, v) for i, f, v in self.get_defaults(model, condition))
@api.model
+ @api.returns('self', lambda value: value.id)
def set_action(self, name, action_slot, model, action, res_id=False):
"""Binds an the given action to the given model's action slot - for later
retrieval via :meth:`~.get_actions`. Any existing binding of the same action
@@ -332,7 +329,7 @@ class IrValues(models.Model):
:param string action: action reference, in the form ``'model,id'``
:param int res_id: optional record id - will bind the action only to a
specific record of the model, not all records.
- :return: id of the newly created ir.values entry
+ :return: the newly created ir.values entry
"""
assert isinstance(action, basestring) and ',' in action, \
'Action definition must be an action reference, e.g. "ir.actions.act_window,42"'
@@ -356,10 +353,10 @@ class IrValues(models.Model):
'res_id': res_id,
'name': name,
'value': action,
- }).id
+ })
@api.model
- @tools.ormcache_context('self.env.uid', 'action_slot', 'model', 'res_id', keys=('lang',))
+ @tools.ormcache_context('self._uid', 'action_slot', 'model', 'res_id', keys=('lang',))
def get_actions(self, action_slot, model, res_id=False):
"""Retrieves the list of actions bound to the given model's action slot.
See the class description for more details about the various action
@@ -376,23 +373,20 @@ class IrValues(models.Model):
where ``id`` is the ID of the default entry, ``name`` is the
action label, and ``action_def`` is a dict containing the
action definition as obtained by calling
- :meth:`~openerp.osv.osv.osv.read` on the action record.
+ :meth:`~odoo.models.Model.read` on the action record.
"""
assert action_slot in ACTION_SLOTS, 'Illegal action slot value: %s' % action_slot
# use a direct SQL query for performance reasons,
# this is called very often
- query = """SELECT v.id, v.name, v.value FROM ir_values v
- WHERE v.key = %s AND v.key2 = %s
- AND v.model = %s
- AND (v.res_id = %s
- OR v.res_id IS NULL
- OR v.res_id = 0)
- ORDER BY v.id"""
- self.env.cr.execute(query, ('action', action_slot, model, res_id or None))
+ query = """ SELECT v.id, v.name, v.value FROM ir_values v
+ WHERE v.key = %s AND v.key2 = %s AND v.model = %s
+ AND (v.res_id = %s OR v.res_id IS NULL OR v.res_id = 0)
+ ORDER BY v.id """
+ self._cr.execute(query, ('action', action_slot, model, res_id or None))
# map values to their corresponding action record
actions = []
- for id, name, value in self.env.cr.fetchall():
+ for id, name, value in self._cr.fetchall():
if not value:
continue # skip if undefined
action_model, action_id = value.split(',')
diff --git a/openerp/addons/base/module/wizard/base_module_immediate_install.xml b/openerp/addons/base/module/wizard/base_module_immediate_install.xml
index 8a43727a402..02b75a1e4c3 100644
--- a/openerp/addons/base/module/wizard/base_module_immediate_install.xml
+++ b/openerp/addons/base/module/wizard/base_module_immediate_install.xml
@@ -7,7 +7,7 @@
<field name="type">ir.actions.server</field>
<field name="model_id" ref="model_ir_module_module" />
<field name="state">code</field>
- <field name="code">self._model.button_immediate_install(self._cr, self._uid, self.env.context.get('active_ids', []), context=self.env.context)</field>
+ <field name="code">self.button_immediate_install(cr, uid, context.get('active_ids', []), context=context)</field>
</record>
<record model="ir.values" id="action_module_immediate_install">
diff --git a/openerp/addons/base/res/res_currency.py b/openerp/addons/base/res/res_currency.py
index 04924bdc649..d70aad3f103 100644
--- a/openerp/addons/base/res/res_currency.py
+++ b/openerp/addons/base/res/res_currency.py
@@ -179,6 +179,7 @@ class Currency(models.Model):
@api.v8
def compute(self, from_amount, to_currency, round=True):
""" Convert `from_amount` from currency `self` to `to_currency`. """
+ self, to_currency = self or to_currency, to_currency or self
assert self, "compute from unknown currency"
assert to_currency, "compute to unknown currency"
# apply conversion rate
diff --git a/openerp/addons/base/tests/test_api.py b/openerp/addons/base/tests/test_api.py
index aa384dd901c..ec4be747a73 100644
--- a/openerp/addons/base/tests/test_api.py
+++ b/openerp/addons/base/tests/test_api.py
@@ -462,3 +462,21 @@ class TestAPI(common.TransactionCase):
ps.mapped('parent_id.name'),
[p.name for p in parents]
)
+
+ @mute_logger('openerp.models')
+ def test_80_sorted(self):
+ """ Check sorted on recordsets. """
+ ps = self.env['res.partner'].search([])
+
+ # sort by model order
+ qs = ps[:len(ps) / 2] + ps[len(ps) / 2:]
+ self.assertEqual(qs.sorted().ids, ps.ids)
+
+ # sort by name, with a function or a field name
+ by_name_ids = [p.id for p in sorted(ps, key=lambda p: p.name)]
+ self.assertEqual(ps.sorted(lambda p: p.name).ids, by_name_ids)
+ self.assertEqual(ps.sorted('name').ids, by_name_ids)
+
+ # sort by inverse name, with a field name
+ by_name_ids.reverse()
+ self.assertEqual(ps.sorted('name', reverse=True).ids, by_name_ids)
diff --git a/openerp/addons/base/tests/test_ormcache.py b/openerp/addons/base/tests/test_ormcache.py
index 9d44695a32c..c6f27aa39db 100644
--- a/openerp/addons/base/tests/test_ormcache.py
+++ b/openerp/addons/base/tests/test_ormcache.py
@@ -9,7 +9,7 @@ class TestOrmcache(common.TransactionCase):
XMLID = 'base.group_no_one'
# retrieve the cache, its key and stat counter
- cache, key, counter = get_cache_key_counter(IMD.xmlid_lookup, self.cr, self.uid, XMLID)
+ cache, key, counter = get_cache_key_counter(IMD.xmlid_lookup, XMLID)
hit = counter.hit
miss = counter.miss
diff --git a/openerp/addons/base/tests/test_qweb.py b/openerp/addons/base/tests/test_qweb.py
index 2740cbeefb5..01e6626c10b 100644
--- a/openerp/addons/base/tests/test_qweb.py
+++ b/openerp/addons/base/tests/test_qweb.py
@@ -7,20 +7,19 @@ import re
import collections
from lxml import etree
-from openerp.addons.base.ir.ir_qweb import QWebContext, FileSystemLoader
-import openerp.modules
+import openerp.modules
+from openerp.addons.base.ir.ir_qweb import QWebContext, FileSystemLoader
from openerp.tests import common
-from openerp.addons.base.ir import ir_qweb
class TestQWebTField(common.TransactionCase):
def setUp(self):
super(TestQWebTField, self).setUp()
- self.engine = self.registry('ir.qweb')
+ self.env = self.env(context={'inherit_branding': True})
+ self.engine = self.env['ir.qweb']
def context(self, values):
- return ir_qweb.QWebContext(
- self.cr, self.uid, values, context={'inherit_branding': True})
+ return QWebContext(self.env, values)
def test_trivial(self):
field = etree.Element('span', {'t-field': u'company.name'})
@@ -114,7 +113,7 @@ class TestQWeb(common.TransactionCase):
def run_test_file(self, path):
doc = etree.parse(path).getroot()
loader = FileSystemLoader(path)
- context = QWebContext(self.cr, self.uid, {}, loader=loader)
+ context = QWebContext(self.env, {}, loader=loader)
qweb = self.env['ir.qweb']
for template in loader:
if not template or template.startswith('_'):
@@ -128,7 +127,7 @@ class TestQWeb(common.TransactionCase):
ctx.update(params)
result = doc.find('result[@id="{}"]'.format(template)).text
self.assertEqual(
- qweb._model.render(self.cr, self.uid, template, qwebcontext=ctx).strip(),
+ qweb.render(template, qwebcontext=ctx).strip(),
(result or u'').strip().encode('utf-8'),
template
)
diff --git a/openerp/addons/test_assetsbundle/controllers/main.py b/openerp/addons/test_assetsbundle/controllers/main.py
index 66b507a8a83..74d12ba65c7 100644
--- a/openerp/addons/test_assetsbundle/controllers/main.py
+++ b/openerp/addons/test_assetsbundle/controllers/main.py
@@ -12,4 +12,4 @@ class TestAssetsBundleController(Controller):
bundle_id = request.env.ref('test_assetsbundle.bundle1')
extra_view_ids = registry['ir.ui.view'].search(cr, uid, [('inherit_id', '=', bundle_id.id)])
context = dict(context, check_view_ids=extra_view_ids)
- return request.registry['ir.ui.view'].render(cr, uid, 'test_assetsbundle.template1', context=context)
+ return request.registry['ir.ui.view'].render_template(cr, uid, 'test_assetsbundle.template1', context=context)
diff --git a/openerp/addons/test_assetsbundle/tests/test_assetsbundle.py b/openerp/addons/test_assetsbundle/tests/test_assetsbundle.py
index dd830126f54..0067f006607 100644
--- a/openerp/addons/test_assetsbundle/tests/test_assetsbundle.py
+++ b/openerp/addons/test_assetsbundle/tests/test_assetsbundle.py
@@ -1,12 +1,13 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
+from openerp import api
from openerp.tests import HttpCase
from openerp.tests.common import TransactionCase
from openerp.addons.base.ir.ir_qweb import AssetsBundle
from openerp.modules.module import get_resource_path
-from mock import patch
+from collections import Counter
from os import utime
import time
@@ -29,7 +30,7 @@ class TestJavascriptAssetsBundle(TransactionCase):
""" Checks that a bundle creates an ir.attachment record when its `js` method is called
for the first time.
"""
- self.bundle = AssetsBundle(self.jsbundle_xmlid, cr=self.cr, uid=self.uid, context={}, registry=self.registry)
+ self.bundle = AssetsBundle(self.jsbundle_xmlid, env=self.env)
# there shouldn't be any attachment associated to this bundle
self.assertEquals(len(self._any_ira_for_bundle('js')), 0)
@@ -46,7 +47,7 @@ class TestJavascriptAssetsBundle(TransactionCase):
""" Checks that the bundle's cache is working, i.e. that the bundle creates only one
ir.attachment record when rendered multiple times.
"""
- bundle0 = AssetsBundle(self.jsbundle_xmlid, cr=self.cr, uid=self.uid, context={}, registry=self.registry)
+ bundle0 = AssetsBundle(self.jsbundle_xmlid, env=self.env)
bundle0.js()
self.assertEquals(len(self._any_ira_for_bundle('js')), 1)
@@ -55,7 +56,7 @@ class TestJavascriptAssetsBundle(TransactionCase):
ira0 = self.registry['ir.attachment'].browse(self.cr, self.uid, self._any_ira_for_bundle('js')[0])
date0 = ira0.create_date
- bundle1 = AssetsBundle(self.jsbundle_xmlid, cr=self.cr, uid=self.uid, context={}, registry=self.registry)
+ bundle1 = AssetsBundle(self.jsbundle_xmlid, env=self.env)
bundle1.js()
self.assertEquals(len(self._any_ira_for_bundle('js')), 1)
@@ -70,7 +71,7 @@ class TestJavascriptAssetsBundle(TransactionCase):
def test_03_date_invalidation(self):
""" Checks that a bundle is invalidated when one of its assets' modification date is changed.
"""
- bundle0 = AssetsBundle(self.jsbundle_xmlid, cr=self.cr, uid=self.uid, context={}, registry=self.registry)
+ bundle0 = AssetsBundle(self.jsbundle_xmlid, env=self.env)
bundle0.js()
last_modified0 = bundle0.last_modified
version0 = bundle0.version
@@ -78,7 +79,7 @@ class TestJavascriptAssetsBundle(TransactionCase):
path = get_resource_path('test_assetsbundle', 'static', 'src', 'js', 'test_jsfile1.js')
utime(path, None) # touch
- bundle1 = AssetsBundle(self.jsbundle_xmlid, cr=self.cr, uid=self.uid, context={}, registry=self.registry)
+ bundle1 = AssetsBundle(self.jsbundle_xmlid, env=self.env)
bundle1.js()
last_modified1 = bundle1.last_modified
version1 = bundle1.version
@@ -92,7 +93,7 @@ class TestJavascriptAssetsBundle(TransactionCase):
""" Checks that a bundle is invalidated when its content is modified by adding a file to
source.
"""
- bundle0 = AssetsBundle(self.jsbundle_xmlid, cr=self.cr, uid=self.uid, context={}, registry=self.registry)
+ bundle0 = AssetsBundle(self.jsbundle_xmlid, env=self.env)
bundle0.js()
html0 = bundle0.html
version0 = bundle0.version
@@ -114,7 +115,7 @@ class TestJavascriptAssetsBundle(TransactionCase):
'inherit_id': bundle_id,
})
- bundle1 = AssetsBundle(self.jsbundle_xmlid, cr=self.cr, uid=self.uid, context={'check_view_ids': [newid]}, registry=self.registry)
+ bundle1 = AssetsBundle(self.jsbundle_xmlid, env=self.env(context={'check_view_ids': [newid]}))
bundle1.js()
html1 = bundle1.html
version1 = bundle1.version
@@ -128,7 +129,7 @@ class TestJavascriptAssetsBundle(TransactionCase):
def test_05_debug(self):
""" Checks that a bundle rendered in debug mode outputs non-minified assets.
"""
- debug_bundle = AssetsBundle(self.jsbundle_xmlid, cr=self.cr, uid=self.uid, context={}, registry=self.registry)
+ debug_bundle = AssetsBundle(self.jsbundle_xmlid, env=self.env)
content = debug_bundle.to_html(debug='assets')
# find back one of the original asset file
self.assertIn('/test_assetsbundle/static/src/js/test_jsfile1.js', content)
@@ -142,21 +143,21 @@ class TestJavascriptAssetsBundle(TransactionCase):
"""
# note: changing the max_css_rules of a bundle does not invalidate its attachments
# self.cssbundle_xlmid contains 3 rules
- self.bundle = AssetsBundle(self.cssbundle_xmlid, cr=self.cr, uid=self.uid, context={}, registry=self.registry, max_css_rules=1)
+ self.bundle = AssetsBundle(self.cssbundle_xmlid, env=self.env, max_css_rules=1)
self.bundle.css()
self.assertEquals(len(self._any_ira_for_bundle('css')), 3)
self.assertEquals(len(self.bundle.get_attachments('css')), 3)
def test_07_paginated_css_generation2(self):
# self.cssbundle_xlmid contains 3 rules
- self.bundle = AssetsBundle(self.cssbundle_xmlid, cr=self.cr, uid=self.uid, context={}, registry=self.registry, max_css_rules=2)
+ self.bundle = AssetsBundle(self.cssbundle_xmlid, env=self.env, max_css_rules=2)
self.bundle.css()
self.assertEquals(len(self._any_ira_for_bundle('css')), 2)
self.assertEquals(len(self.bundle.get_attachments('css')), 2)
def test_08_paginated_css_generation3(self):
# self.cssbundle_xlmid contains 3 rules
- self.bundle = AssetsBundle(self.cssbundle_xmlid, cr=self.cr, uid=self.uid, context={}, registry=self.registry, max_css_rules=3)
+ self.bundle = AssetsBundle(self.cssbundle_xmlid, env=self.env, max_css_rules=3)
self.bundle.css()
self.assertEquals(len(self._any_ira_for_bundle('css')), 1)
self.assertEquals(len(self.bundle.get_attachments('css')), 1)
@@ -165,7 +166,7 @@ class TestJavascriptAssetsBundle(TransactionCase):
""" Checks that the bundle's cache is working, i.e. that a bundle creates only enough
ir.attachment records when rendered multiple times.
"""
- bundle0 = AssetsBundle(self.cssbundle_xmlid, cr=self.cr, uid=self.uid, context={}, registry=self.registry, max_css_rules=1)
+ bundle0 = AssetsBundle(self.cssbundle_xmlid, env=self.env, max_css_rules=1)
bundle0.css()
self.assertEquals(len(self._any_ira_for_bundle('css')), 3)
@@ -178,7 +179,7 @@ class TestJavascriptAssetsBundle(TransactionCase):
ira2 = self.registry['ir.attachment'].browse(self.cr, self.uid, self._any_ira_for_bundle('css')[2])
date2 = ira2.create_date
- bundle1 = AssetsBundle(self.cssbundle_xmlid, cr=self.cr, uid=self.uid, context={}, registry=self.registry, max_css_rules=1)
+ bundle1 = AssetsBundle(self.cssbundle_xmlid, env=self.env, max_css_rules=1)
bundle1.css()
self.assertEquals(len(self._any_ira_for_bundle('css')), 3)
@@ -199,7 +200,7 @@ class TestJavascriptAssetsBundle(TransactionCase):
def test_10_paginated_css_date_invalidation(self):
""" Checks that a bundle is invalidated when one of its assets' modification date is changed.
"""
- bundle0 = AssetsBundle(self.cssbundle_xmlid, cr=self.cr, uid=self.uid, context={}, registry=self.registry, max_css_rules=1)
+ bundle0 = AssetsBundle(self.cssbundle_xmlid, env=self.env, max_css_rules=1)
bundle0.css()
last_modified0 = bundle0.last_modified
version0 = bundle0.version
@@ -207,7 +208,7 @@ class TestJavascriptAssetsBundle(TransactionCase):
path = get_resource_path('test_assetsbundle', 'static', 'src', 'css', 'test_cssfile1.css')
utime(path, None) # touch
- bundle1 = AssetsBundle(self.cssbundle_xmlid, cr=self.cr, uid=self.uid, context={}, registry=self.registry, max_css_rules=1)
+ bundle1 = AssetsBundle(self.cssbundle_xmlid, env=self.env, max_css_rules=1)
bundle1.css()
last_modified1 = bundle1.last_modified
version1 = bundle1.version
@@ -222,7 +223,7 @@ class TestJavascriptAssetsBundle(TransactionCase):
""" Checks that a bundle is invalidated when its content is modified by adding a file to
source.
"""
- bundle0 = AssetsBundle(self.cssbundle_xmlid, cr=self.cr, uid=self.uid, context={}, registry=self.registry, max_css_rules=1)
+ bundle0 = AssetsBundle(self.cssbundle_xmlid, env=self.env, max_css_rules=1)
bundle0.css()
html0 = bundle0.html
version0 = bundle0.version
@@ -244,7 +245,7 @@ class TestJavascriptAssetsBundle(TransactionCase):
'inherit_id': bundle_id,
})
- bundle1 = AssetsBundle(self.cssbundle_xmlid, cr=self.cr, uid=self.uid, context={'check_view_ids': [newid]}, registry=self.registry, max_css_rules=1)
+ bundle1 = AssetsBundle(self.cssbundle_xmlid, env=self.env(context={'check_view_ids': [newid]}), max_css_rules=1)
bundle1.css()
html1 = bundle1.html
version1 = bundle1.version
@@ -258,7 +259,7 @@ class TestJavascriptAssetsBundle(TransactionCase):
def test_12_paginated_css_debug(self):
""" Check that a bundle in debug mode outputs non-minified assets.
"""
- debug_bundle = AssetsBundle(self.cssbundle_xmlid, cr=self.cr, uid=self.uid, context={}, registry=self.registry, max_css_rules=1)
+ debug_bundle = AssetsBundle(self.cssbundle_xmlid, env=self.env, max_css_rules=1)
content = debug_bundle.to_html(debug='assets')
# find back one of the original asset file
self.assertIn('/test_assetsbundle/static/src/css/test_cssfile1.css', content)
@@ -268,7 +269,7 @@ class TestJavascriptAssetsBundle(TransactionCase):
def test_13_paginated_css_order(self):
# self.cssbundle_xlmid contains 3 rules
- self.bundle = AssetsBundle(self.cssbundle_xmlid, cr=self.cr, uid=self.uid, context={}, registry=self.registry, max_css_rules=1)
+ self.bundle = AssetsBundle(self.cssbundle_xmlid, env=self.env, max_css_rules=1)
stylesheets = self.bundle.css()
self.assertTrue(stylesheets[0].url.endswith('.0.css'))
@@ -317,17 +318,30 @@ class TestAssetsBundleWithIRAMock(TransactionCase):
def setUp(self):
super(TestAssetsBundleWithIRAMock, self).setUp()
self.lessbundle_xmlid = 'test_assetsbundle.bundle3'
- self.patcher1 = patch('openerp.addons.base.ir.ir_attachment.ir_attachment.create', wraps=self.registry['ir.attachment'].create)
- self.patcher2 = patch('openerp.addons.base.ir.ir_attachment.ir_attachment.unlink', wraps=self.registry['ir.attachment'].unlink)
- self.mock_ira_create = self.patcher1.start()
- self.mock_ira_unlink = self.patcher2.start()
+ self.counter = counter = Counter()
+
+ # patch methods 'create' and 'unlink' of model 'ir.attachment'
+ @api.model
+ def create(self, vals):
+ counter.update(['create'])
+ return create.origin(self, vals)
+
+ @api.multi
+ def unlink(self):
+ counter.update(['unlink'])
+ return unlink.origin(self)
+
+ self.env['ir.attachment']._patch_method('create', create)
+ self.addCleanup(self.env['ir.attachment']._revert_method, 'create')
+
+ self.env['ir.attachment']._patch_method('unlink', unlink)
+ self.addCleanup(self.env['ir.attachment']._revert_method, 'unlink')
def _bundle(self, should_create, should_unlink):
- self.mock_ira_create.reset_mock()
- self.mock_ira_unlink.reset_mock()
- AssetsBundle(self.lessbundle_xmlid, cr=self.cr, uid=self.uid, context={}, registry=self.registry).to_html(debug='assets')
- self.assertEquals(self.mock_ira_create.call_count, int(should_create))
- self.assertEquals(self.mock_ira_unlink.call_count, int(should_unlink))
+ self.counter.clear()
+ AssetsBundle(self.lessbundle_xmlid, env=self.env).to_html(debug='assets')
+ self.assertEquals(self.counter['create'], int(should_create))
+ self.assertEquals(self.counter['unlink'], int(should_unlink))
def test_01_debug_mode_assets(self):
""" Checks that the ir.attachments records created for compiled less assets in debug mode
@@ -354,8 +368,3 @@ class TestAssetsBundleWithIRAMock(TransactionCase):
# Compile a fourth time, without changes
self._bundle(False, False)
-
- def tearDown(self):
- self.patcher2.stop()
- self.patcher1.stop()
- super(TestAssetsBundleWithIRAMock, self).tearDown()
diff --git a/openerp/addons/test_converter/tests/test_html.py b/openerp/addons/test_converter/tests/test_html.py
index 2f50602c115..36b4f2fa2d0 100644
--- a/openerp/addons/test_converter/tests/test_html.py
+++ b/openerp/addons/test_converter/tests/test_html.py
@@ -7,7 +7,7 @@ from lxml import etree
from openerp.tests import common
from openerp.tools import html_escape as e
-from openerp.addons.base.ir import ir_qweb
+from openerp.addons.base.ir.ir_qweb import QWebContext
directory = os.path.dirname(__file__)
@@ -113,7 +113,7 @@ class TestCurrencyExport(TestExport):
self.cr, self.uid, 'value', obj, options,
etree.Element('span'),
{'field': 'obj.value', 'field-options': json.dumps(options)},
- '', ir_qweb.QWebContext(self.cr, self.uid, {'obj': obj, 'c2': dest, }),
+ '', QWebContext(self.env, {'obj': obj, 'c2': dest, }),
context=context,
)
return converted
diff --git a/openerp/http.py b/openerp/http.py
index 18404243e92..741905a60c4 100644
--- a/openerp/http.py
+++ b/openerp/http.py
@@ -1429,9 +1429,8 @@ class Response(werkzeug.wrappers.Response):
view_obj = request.registry["ir.ui.view"]
uid = self.uid or request.uid or openerp.SUPERUSER_ID
self.qcontext['request'] = request
- return view_obj.render(
- request.cr, uid, self.template, self.qcontext,
- context=request.context)
+ return view_obj.render_template(request.cr, uid, self.template,
+ self.qcontext, context=request.context)
def flatten(self):
""" Forces the rendering of the response's template, sets the result
diff --git a/openerp/models.py b/openerp/models.py
index af58afa7627..617e3eee7a7 100644
--- a/openerp/models.py
+++ b/openerp/models.py
@@ -32,7 +32,7 @@ import re
import time
from collections import defaultdict, MutableMapping
from inspect import getmembers, currentframe
-from operator import itemgetter
+from operator import attrgetter, itemgetter
import babel.dates
import dateutil.relativedelta
@@ -5642,16 +5642,17 @@ class BaseModel(object):
""" Return the recordset ``self`` ordered by ``key``.
:param key: either a function of one argument that returns a
- comparison key for each record, or ``None``, in which case
- records are ordered according the default model's order
+ comparison key for each record, or a field name, or ``None``, in
+ which case records are ordered according the default model's order
:param reverse: if ``True``, return the result in reverse order
"""
if key is None:
recs = self.search([('id', 'in', self.ids)])
return self.browse(reversed(recs._ids)) if reverse else recs
- else:
- return self.browse(map(itemgetter('id'), sorted(self, key=key, reverse=reverse)))
+ if isinstance(key, basestring):
+ key = itemgetter(key)
+ return self.browse(map(attrgetter('id'), sorted(self, key=key, reverse=reverse)))
@api.multi
def update(self, values):