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')) + 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')) - 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) 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//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 @@ Choose Your Theme Todo Action -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 = {} - 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') - data = fp.read() - except: - data = False - finally: - if fp: - fp.close() - res[report.id] = data - return res - - @api.depends('report_sxw_content') - def _compute_report_sxw_content(self): - self.report_sxw_content = self._report_content(self.name) - - @api.depends('report_rml_content') - def _compute_report_rml_content(self): - self.report_rml_content = self._report_content(self.name) - - def _inverse_report_content(self, name, value): - self.write({name+'_data': value}) - - @api.depends('report_sxw') + @api.depends('report_rml') def _compute_report_sxw(self): for report in self: if report.report_rml: self.report_sxw = report.report_rml.replace('.rml', '.sxw') - @api.multi + 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 Exception: + data = False + return data + + @api.depends('report_sxw', 'report_sxw_content_data') + def _compute_report_sxw_content(self): + for report in self: + report.report_sxw_content = report._report_content('report_sxw') + + @api.depends('report_rml', 'report_rml_content_data') + def _compute_report_rml_content(self): + for report in self: + report.report_rml_content = report._report_content('report_rml') + + def _inverse_report_sxw_content(self): + for report in self: + report.report_sxw_content_data = report.report_sxw_content + + def _inverse_report_rml_content(self): + for report in self: + report.report_rml_content_data = report.report_rml_content + + @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] - 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 odoo.report.interface.report_int._reports['report.' + name] - return new_report + 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: + 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. + @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. - - :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. + 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, - } + 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 - 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): + @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('write_expression', 'model_id') - def on_change_write_expression(self): + @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') + 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, ) """ # 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_ + 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 + attach.datas = attach.db_datas - 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({ + 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) + '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 - # 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 + # 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 = 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') - _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'), - } + # 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) - _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), - } + @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() - 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): + @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']) + 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']) - 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): + # 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]) - 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.v8 + def read(self, fields=None, load='_classic_read'): + self.check('read') + return super(IrAttachment, self).read(fields, load=load) + + @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 + return self._get_param(key) or 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'] + @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 @@ - + ir.config_parameter @@ -35,4 +35,4 @@ - + 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') + raise ValidationError(_('Invalid arguments')) + + @api.multi + def method_direct_trigger(self): + for cron in self: + self._callback(cron.model, cron.function, cron.args, cron.id) return True - 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) - 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,16 +95,13 @@ 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'([^ ,<@]+@[^> ,]+)') - def extract_rfc2822_addresses(text): """Returns a list of valid RFC2822 addresses that can be found in ``source``, ignoring @@ -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 @@ - +