diff --git a/addons/analytic_user_function/analytic_user_function_view.xml b/addons/analytic_user_function/analytic_user_function_view.xml index 67d8672f44b..132f44ebef5 100644 --- a/addons/analytic_user_function/analytic_user_function_view.xml +++ b/addons/analytic_user_function/analytic_user_function_view.xml @@ -78,8 +78,10 @@ - - + + [('type','=','normal'),('state', '<>', 'close')] + on_change_account_id(account_id, user_id, unit_amount) + {'default_invoice_on_timesheets': 1} @@ -90,8 +92,9 @@ hr.analytic.timesheet - - + + [('type','=','normal'),('state', '<>', 'close')] + on_change_account_id(account_id, user_id, unit_amount) @@ -112,8 +115,9 @@ hr.analytic.timesheet - - + + [('type','=','normal'),('state', '<>', 'close')] + on_change_account_id(account_id, user_id, unit_amount) diff --git a/addons/google_calendar/google_calendar.py b/addons/google_calendar/google_calendar.py index 79be277de09..70c6cceaed4 100644 --- a/addons/google_calendar/google_calendar.py +++ b/addons/google_calendar/google_calendar.py @@ -536,7 +536,7 @@ class google_calendar(osv.AbstractModel): for user_to_sync in ids: _logger.info("Calendar Synchro - Starting synchronization for a new user [%s] " % user_to_sync) try: - resp = self.synchronize_events(cr, uid, [user_to_sync], lastSync=True, context=None) + resp = self.synchronize_events(cr, user_to_sync, False, lastSync=True, context=None) if resp.get("status") == "need_reset": _logger.info("[%s] Calendar Synchro - Failed - NEED RESET !" % user_to_sync) else: @@ -564,7 +564,7 @@ class google_calendar(osv.AbstractModel): user_to_sync = ids and ids[0] or uid current_user = self.pool['res.users'].browse(cr, SUPERUSER_ID, user_to_sync, context=context) - st, current_google, ask_time = self.get_calendar_primary_id(cr, uid, context=context) + st, current_google, ask_time = self.get_calendar_primary_id(cr, user_to_sync, context=context) if current_user.google_calendar_cal_id: if current_google != current_user.google_calendar_cal_id: @@ -577,8 +577,8 @@ class google_calendar(osv.AbstractModel): "url": '' } - if lastSync and self.get_last_sync_date(cr, uid, context=context) and not self.get_disable_since_synchro(cr, uid, context=context): - lastSync = self.get_last_sync_date(cr, uid, context) + if lastSync and self.get_last_sync_date(cr, user_to_sync, context=context) and not self.get_disable_since_synchro(cr, user_to_sync, context=context): + lastSync = self.get_last_sync_date(cr, user_to_sync, context) _logger.info("[%s] Calendar Synchro - MODE SINCE_MODIFIED : %s !" % (user_to_sync, lastSync.strftime(DEFAULT_SERVER_DATETIME_FORMAT))) else: lastSync = False @@ -589,14 +589,14 @@ class google_calendar(osv.AbstractModel): _logger.info("[%s] Calendar Synchro - MODE FULL SYNCHRO - NEW CAL ID" % user_to_sync) new_ids = [] - new_ids += self.create_new_events(cr, uid, context=context) - new_ids += self.bind_recurring_events_to_google(cr, uid, context) + new_ids += self.create_new_events(cr, user_to_sync, context=context) + new_ids += self.bind_recurring_events_to_google(cr, user_to_sync, context) - res = self.update_events(cr, uid, lastSync, context) + res = self.update_events(cr, user_to_sync, lastSync, context) current_user.write({'google_calendar_last_sync_date': ask_time}) return { - "status": res and "need_refresh" or "no_new_event_form_google", + "status": res and "need_refresh" or "no_new_event_from_google", "url": '' } diff --git a/addons/google_calendar/google_calendar.xml b/addons/google_calendar/google_calendar.xml index 40bca105d1e..d2516f10233 100644 --- a/addons/google_calendar/google_calendar.xml +++ b/addons/google_calendar/google_calendar.xml @@ -3,7 +3,6 @@ - Calendar synchronization diff --git a/addons/hr_timesheet/hr_timesheet_view.xml b/addons/hr_timesheet/hr_timesheet_view.xml index 0a3f5e79c79..1f66de06c7f 100644 --- a/addons/hr_timesheet/hr_timesheet_view.xml +++ b/addons/hr_timesheet/hr_timesheet_view.xml @@ -11,7 +11,7 @@ - + @@ -49,7 +49,7 @@ - + diff --git a/addons/hr_timesheet_invoice/hr_timesheet_invoice_view.xml b/addons/hr_timesheet_invoice/hr_timesheet_invoice_view.xml index b7f03683945..9250677ab28 100644 --- a/addons/hr_timesheet_invoice/hr_timesheet_invoice_view.xml +++ b/addons/hr_timesheet_invoice/hr_timesheet_invoice_view.xml @@ -45,8 +45,10 @@ hr.analytic.timesheet - - + + [('type','in',['normal','contract']),('state', '!=', 'close'),('invoice_on_timesheets','=',1)] + on_change_account_id(account_id, user_id) + {'default_invoice_on_timesheets': 1} @@ -67,8 +69,10 @@ hr.analytic.timesheet - - + + [('type','in',['normal','contract']),('state', '!=', 'close'),('invoice_on_timesheets','=',1)] + on_change_account_id(account_id, user_id) + {'default_invoice_on_timesheets': 1} diff --git a/addons/pad/__openerp__.py b/addons/pad/__openerp__.py index b4bbc722f8a..24a05414206 100644 --- a/addons/pad/__openerp__.py +++ b/addons/pad/__openerp__.py @@ -8,7 +8,7 @@ Adds enhanced support for (Ether)Pad attachments in the web client. =================================================================== Lets the company customize which Pad installation should be used to link to new -pads (by default, http://ietherpad.com/). +pads (by default, http://etherpad.com/). """, 'author': 'OpenERP SA', 'website': 'https://www.odoo.com/page/notes', diff --git a/addons/project_issue/security/project_issue_security.xml b/addons/project_issue/security/project_issue_security.xml index c8ed12086e4..ea7674ea794 100644 --- a/addons/project_issue/security/project_issue_security.xml +++ b/addons/project_issue/security/project_issue_security.xml @@ -16,7 +16,7 @@ ('project_id.privacy_visibility', 'in', ['public', 'employees']), '&', ('project_id.privacy_visibility', '=', 'followers'), - ('message_follower_ids', 'in', [user.id]), + ('message_follower_ids', 'in', [user.partner_id.id]), ] diff --git a/addons/report/models/report.py b/addons/report/models/report.py index 574aa3c7947..1be7d6efa0b 100644 --- a/addons/report/models/report.py +++ b/addons/report/models/report.py @@ -87,6 +87,31 @@ class Report(osv.Model): #-------------------------------------------------------------------------- # Extension of ir_ui_view.render with arguments frequently used in reports #-------------------------------------------------------------------------- + + def translate_doc(self, cr, uid, doc_id, model, lang_field, template, values, context=None): + """Helper used when a report should be translated into a specific lang. + + + + + + :param doc_id: id of the record to translate + :param model: model of the record to translate + :param lang_field': field of the record containing the lang + :param template: name of the template to translate into the lang_field + """ + ctx = context.copy() + doc = self.pool[model].browse(cr, uid, doc_id, context=ctx) + qcontext = values.copy() + # Do not force-translate if we chose to display the report in a specific lang + if ctx.get('translatable') is True: + qcontext['o'] = doc + else: + # Reach the lang we want to translate the doc into + ctx['lang'] = eval('doc.%s' % lang_field, {'doc': doc}) + qcontext['o'] = self.pool[model].browse(cr, uid, doc_id, context=ctx) + return self.pool['ir.ui.view'].render(cr, uid, template, qcontext, context=ctx) + def render(self, cr, uid, ids, template, values=None, context=None): """Allow to render a QWeb template python-side. This function returns the 'ir.ui.view' render but embellish it with some variables/methods used in reports. @@ -105,28 +130,7 @@ class Report(osv.Model): view_obj = self.pool['ir.ui.view'] def translate_doc(doc_id, model, lang_field, template): - """Helper used when a report should be translated into a specific lang. - - - - - - :param doc_id: id of the record to translate - :param model: model of the record to translate - :param lang_field': field of the record containing the lang - :param template: name of the template to translate into the lang_field - """ - ctx = context.copy() - doc = self.pool[model].browse(cr, uid, doc_id, context=ctx) - qcontext = values.copy() - # Do not force-translate if we chose to display the report in a specific lang - if ctx.get('translatable') is True: - qcontext['o'] = doc - else: - # Reach the lang we want to translate the doc into - ctx['lang'] = eval('doc.%s' % lang_field, {'doc': doc}) - qcontext['o'] = self.pool[model].browse(cr, uid, doc_id, context=ctx) - return view_obj.render(cr, uid, template, qcontext, context=ctx) + return self.translate_doc(cr, uid, doc_id, model, lang_field, template, values, context=context) user = self.pool['res.users'].browse(cr, uid, uid) website = None diff --git a/addons/web/static/src/js/view_form.js b/addons/web/static/src/js/view_form.js index b01c8090491..95472605143 100644 --- a/addons/web/static/src/js/view_form.js +++ b/addons/web/static/src/js/view_form.js @@ -3478,7 +3478,7 @@ instance.web.form.M2ODialog = instance.web.Dialog.extend({ var text = _.str.sprintf(_t("You are creating a new %s, are you sure it does not exist yet?"), self.name); this.$("p").text( text ); this.$buttons.html(QWeb.render("M2ODialog.buttons")); - this.$("input").val(this.getParent().last_query); + this.$("input").val(this.getParent().$input.val()); this.$buttons.find(".oe_form_m2o_qc_button").click(function(e){ if (self.$("input").val() != ''){ self.getParent()._quick_create(self.$("input").val()); @@ -3660,13 +3660,13 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instanc self.display_value_backup = {}; self.display_value["" + self.last_search[0][0]] = self.last_search[0][1]; self.reinit_value(self.last_search[0][0]); + self.last_search = [] } else { used = true; self.render_value(); } } else { used = true; - self.reinit_value(false); } self.floating = false; } @@ -3740,7 +3740,7 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instanc // disabled to solve a bug, but may cause others //close: anyoneLoosesFocus, minLength: 0, - delay: 250, + delay: 200, }); var appendTo = this.$input.parents('.oe-view-manager-content, .modal-dialog').last(); if (appendTo.length === 0) { diff --git a/addons/website_quote/models/order.py b/addons/website_quote/models/order.py index 81f19b3c65f..6b15301c7cd 100644 --- a/addons/website_quote/models/order.py +++ b/addons/website_quote/models/order.py @@ -71,7 +71,7 @@ class sale_quote_line(osv.osv): if product_obj.description_sale: name += '\n' + product_obj.description_sale vals.update({ - 'price_unit': product_obj.list_price, + 'price_unit': product_obj.lst_price, 'product_uom_id': product_obj.uom_id.id, 'website_description': product_obj and (product_obj.quote_description or product_obj.website_description) or '', 'name': name, diff --git a/addons/website_report/__init__.py b/addons/website_report/__init__.py index e69de29bb2d..a0526c7c636 100644 --- a/addons/website_report/__init__.py +++ b/addons/website_report/__init__.py @@ -0,0 +1,2 @@ +import controllers +import report diff --git a/addons/website_report/controllers/__init__.py b/addons/website_report/controllers/__init__.py new file mode 100644 index 00000000000..8ee9bae18d9 --- /dev/null +++ b/addons/website_report/controllers/__init__.py @@ -0,0 +1 @@ +import main diff --git a/addons/website_report/controllers/main.py b/addons/website_report/controllers/main.py new file mode 100644 index 00000000000..11b4257b6c6 --- /dev/null +++ b/addons/website_report/controllers/main.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2014-Today OpenERP SA (). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +from openerp.addons.website.controllers.main import Website +from openerp.http import request, route + + +class Website(Website): + + @route() + def customize_template_get(self, xml_id, full=False): + res = super(Website, self).customize_template_get(xml_id, full=full) + if full and request.session.get('report_view_ids'): + res += request.session['report_view_ids'] + return res diff --git a/addons/website_report/report.py b/addons/website_report/report.py new file mode 100644 index 00000000000..4da33a4d7c2 --- /dev/null +++ b/addons/website_report/report.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2014-Today OpenERP SA (). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +from openerp.addons.web.http import request +from openerp.osv import osv + + +class Report(osv.Model): + _inherit = 'report' + + def translate_doc(self, cr, uid, doc_id, model, lang_field, template, values, context=None): + if request and hasattr(request, 'website'): + if request.website is not None: + v = request.website.get_template(template) + request.session['report_view_ids'].append({ + 'name': v.name, + 'id': v.id, + 'xml_id': v.xml_id, + 'inherit_id': v.inherit_id.id, + 'header': False, + 'active': v.active, + }) + return super(Report, self).translate_doc(cr, uid, doc_id, model, lang_field, template, values, context=context) + + def render(self, cr, uid, ids, template, values=None, context=None): + if request and hasattr(request, 'website'): + if request.website is not None: + request.session['report_view_ids'] = [] + return super(Report, self).render(cr, uid, ids, template, values=values, context=context) diff --git a/doc/_themes/odoodoc/static/odoodoc.css_t b/doc/_themes/odoodoc/static/odoodoc.css_t index a70db9fb825..64a14dc61e4 100644 --- a/doc/_themes/odoodoc/static/odoodoc.css_t +++ b/doc/_themes/odoodoc/static/odoodoc.css_t @@ -1 +1 @@ -@import "style.css"; +@import url("style.css"); diff --git a/openerp/addons/base/ir/ir_model.py b/openerp/addons/base/ir/ir_model.py index 5b2a013aa05..aeead6e664e 100644 --- a/openerp/addons/base/ir/ir_model.py +++ b/openerp/addons/base/ir/ir_model.py @@ -1228,20 +1228,25 @@ class ir_model_data(osv.osv): and a module in ir_model_data and noupdate set to false, but not present in self.loads. """ - if not modules: + if not modules or config.get('import_partial'): return True - to_unlink = [] + + bad_imd_ids = [] + context = {MODULE_UNINSTALL_FLAG: True} cr.execute("""SELECT id,name,model,res_id,module FROM ir_model_data - WHERE module IN %s AND res_id IS NOT NULL AND noupdate=%s ORDER BY id DESC""", - (tuple(modules), False)) + WHERE module IN %s AND res_id IS NOT NULL AND noupdate=%s ORDER BY id DESC + """, (tuple(modules), False)) for (id, name, model, res_id, module) in cr.fetchall(): - if (module,name) not in self.loads: - to_unlink.append((model,res_id)) - if not config.get('import_partial'): - for (model, res_id) in to_unlink: + if (module, name) not in self.loads: if model in self.pool: - _logger.info('Deleting %s@%s', res_id, model) - self.pool[model].unlink(cr, uid, [res_id]) + _logger.info('Deleting %s@%s (%s.%s)', res_id, model, module, name) + if self.pool[model].exists(cr, uid, [res_id], context=context): + self.pool[model].unlink(cr, uid, [res_id], context=context) + else: + bad_imd_ids.append(id) + if bad_imd_ids: + self.unlink(cr, uid, bad_imd_ids, context=context) + self.loads.clear() class wizard_model_menu(osv.osv_memory): _name = 'wizard.ir.model.menu.create' diff --git a/openerp/addons/test_inherit/models.py b/openerp/addons/test_inherit/models.py index f5728bbf0f6..7313e12c2a3 100644 --- a/openerp/addons/test_inherit/models.py +++ b/openerp/addons/test_inherit/models.py @@ -25,10 +25,9 @@ class mother(models.Model): # in the child object class daughter(models.Model): _name = 'test.inherit.daughter' - _inherits = {'test.inherit.mother': 'template_id'} template_id = fields.Many2one('test.inherit.mother', 'Template', - required=True, ondelete='cascade') + delegate=True, required=True, ondelete='cascade') field_in_daughter = fields.Char('Field1') diff --git a/openerp/addons/test_inherit/tests/test_inherit.py b/openerp/addons/test_inherit/tests/test_inherit.py index 653f37f1f04..c277692b7d3 100644 --- a/openerp/addons/test_inherit/tests/test_inherit.py +++ b/openerp/addons/test_inherit/tests/test_inherit.py @@ -3,7 +3,13 @@ from openerp.tests import common class test_inherits(common.TransactionCase): - def test_access_from_child_to_parent_model(self): + def test_00_inherits(self): + """ Check that a many2one field with delegate=True adds an entry in _inherits """ + daughter = self.env['test.inherit.daughter'] + + self.assertEqual(daughter._inherits, {'test.inherit.mother': 'template_id'}) + + def test_10_access_from_child_to_parent_model(self): """ check whether added field in model is accessible from children models (_inherits) """ # This test checks if the new added column of a parent model # is accessible from the child model. This test has been written @@ -15,7 +21,7 @@ class test_inherits(common.TransactionCase): self.assertIn('field_in_mother', mother._fields) self.assertIn('field_in_mother', daughter._fields) - def test_field_extension(self): + def test_20_field_extension(self): """ check the extension of a field in an inherited model """ mother = self.env['test.inherit.mother'] daughter = self.env['test.inherit.daughter'] @@ -41,7 +47,7 @@ class test_inherits(common.TransactionCase): self.assertEqual(field.string, "Template") self.assertTrue(field.required) - def test_depends_extension(self): + def test_30_depends_extension(self): """ check that @depends on overridden compute methods extends dependencies """ mother = self.env['test.inherit.mother'] field = mother._fields['surname'] @@ -49,7 +55,7 @@ class test_inherits(common.TransactionCase): # the field dependencies are added self.assertItemsEqual(field.depends, ['name', 'field_in_mother']) - def test_selection_extension(self): + def test_40_selection_extension(self): """ check that attribute selection_add=... extends selection on fields. """ mother = self.env['test.inherit.mother'] diff --git a/openerp/addons/test_new_api/ir.model.access.csv b/openerp/addons/test_new_api/ir.model.access.csv index d43088d808b..5b62045ae08 100644 --- a/openerp/addons/test_new_api/ir.model.access.csv +++ b/openerp/addons/test_new_api/ir.model.access.csv @@ -2,5 +2,4 @@ access_category,test_new_api_category,test_new_api.model_test_new_api_category,,1,1,1,1 access_discussion,test_new_api_discussion,test_new_api.model_test_new_api_discussion,,1,1,1,1 access_message,test_new_api_message,test_new_api.model_test_new_api_message,,1,1,1,1 -access_talk,test_new_api_talk,test_new_api.model_test_new_api_talk,,1,1,1,1 access_mixed,test_new_api_mixed,test_new_api.model_test_new_api_mixed,,1,1,1,1 diff --git a/openerp/addons/test_new_api/models.py b/openerp/addons/test_new_api/models.py index 9866c8b6fd4..55611f6029d 100644 --- a/openerp/addons/test_new_api/models.py +++ b/openerp/addons/test_new_api/models.py @@ -153,12 +153,6 @@ class Message(models.Model): self.double_size = self.double_size + size -class Talk(models.Model): - _name = 'test_new_api.talk' - - parent = fields.Many2one('test_new_api.discussion', delegate=True, required=True) - - class MixedModel(models.Model): _name = 'test_new_api.mixed' diff --git a/openerp/addons/test_new_api/tests/test_new_fields.py b/openerp/addons/test_new_api/tests/test_new_fields.py index 5441698e7ed..af42443fbc0 100644 --- a/openerp/addons/test_new_api/tests/test_new_fields.py +++ b/openerp/addons/test_new_api/tests/test_new_fields.py @@ -373,20 +373,3 @@ class TestMagicFields(common.TransactionCase): record = self.env['test_new_api.discussion'].create({'name': 'Booba'}) self.assertEqual(record.create_uid, self.env.user) self.assertEqual(record.write_uid, self.env.user) - - -class TestInherits(common.TransactionCase): - - def test_inherits(self): - """ Check that a many2one field with delegate=True adds an entry in _inherits """ - Talk = self.env['test_new_api.talk'] - self.assertEqual(Talk._inherits, {'test_new_api.discussion': 'parent'}) - self.assertIn('name', Talk._fields) - self.assertEqual(Talk._fields['name'].related, ('parent', 'name')) - - talk = Talk.create({'name': 'Foo'}) - discussion = talk.parent - self.assertTrue(discussion) - self.assertEqual(talk._name, 'test_new_api.talk') - self.assertEqual(discussion._name, 'test_new_api.discussion') - self.assertEqual(talk.name, discussion.name) diff --git a/openerp/fields.py b/openerp/fields.py index adc9fcacc93..0c1f058ca91 100644 --- a/openerp/fields.py +++ b/openerp/fields.py @@ -297,6 +297,13 @@ class Field(object): self._attrs = {key: val for key, val in kwargs.iteritems() if val is not None} self._free_attrs = [] + # self._triggers is a set of pairs (field, path) that represents the + # computed fields that depend on `self`. When `self` is modified, it + # invalidates the cache of each `field`, and registers the records to + # recompute based on `path`. See method `modified` below for details. + self._triggers = set() + self.inverse_fields = [] + def new(self, **kwargs): """ Return a field of the same type as `self`, with its own parameters. """ return type(self)(**kwargs) @@ -396,16 +403,6 @@ class Field(object): # Field setup # - def reset(self): - """ Prepare `self` for a new setup. """ - self.setup_done = False - # self._triggers is a set of pairs (field, path) that represents the - # computed fields that depend on `self`. When `self` is modified, it - # invalidates the cache of each `field`, and registers the records to - # recompute based on `path`. See method `modified` below for details. - self._triggers = set() - self.inverse_fields = [] - def setup(self, env): """ Make sure that `self` is set up, except for recomputation triggers. """ if not self.setup_done: @@ -640,8 +637,11 @@ class Field(object): # def to_column(self): - """ return a low-level field object corresponding to `self` """ - assert self.store or self.column + """ Return a column object corresponding to `self`, or ``None``. """ + if not self.store and self.compute: + # non-stored computed fields do not have a corresponding column + self.column = None + return None # determine column parameters #_logger.debug("Create fields._column for Field %s", self) @@ -677,6 +677,7 @@ class Field(object): _column_groups = property(attrgetter('groups')) _column_change_default = property(attrgetter('change_default')) _column_deprecated = property(attrgetter('deprecated')) + _column_compute = property(lambda self: bool(self.compute)) ############################################################################ # diff --git a/openerp/models.py b/openerp/models.py index f82a4059373..83103617b57 100644 --- a/openerp/models.py +++ b/openerp/models.py @@ -631,8 +631,8 @@ class BaseModel(object): attrs = { '_name': name, '_register': False, - '_columns': {}, # filled by _setup_fields() - '_defaults': {}, # filled by Field._determine_default() + '_columns': None, # recomputed in _setup_fields() + '_defaults': None, # recomputed in _setup_base() '_inherits': dict(cls._inherits), '_depends': dict(cls._depends), '_constraints': list(cls._constraints), @@ -739,15 +739,40 @@ class BaseModel(object): for (key, _, msg) in cls._sql_constraints: cls.pool._sql_error[cls._table + '_' + key] = msg - # collect constraint and onchange methods - cls._constraint_methods = [] - cls._onchange_methods = defaultdict(list) - for attr, func in getmembers(cls, callable): - if hasattr(func, '_constrains'): - cls._constraint_methods.append(func) - if hasattr(func, '_onchange'): - for name in func._onchange: - cls._onchange_methods[name].append(func) + @property + def _constraint_methods(self): + """ Return a list of methods implementing Python constraints. """ + def is_constraint(func): + return callable(func) and hasattr(func, '_constrains') + + cls = type(self) + methods = [] + for attr, func in getmembers(cls, is_constraint): + if not all(name in cls._fields for name in func._constrains): + _logger.warning("@constrains%r parameters must be field names", func._constrains) + methods.append(func) + + # optimization: memoize result on cls, it will not be recomputed + cls._constraint_methods = methods + return methods + + @property + def _onchange_methods(self): + """ Return a dictionary mapping field names to onchange methods. """ + def is_onchange(func): + return callable(func) and hasattr(func, '_onchange') + + cls = type(self) + methods = defaultdict(list) + for attr, func in getmembers(cls, is_onchange): + for name in func._onchange: + if name not in cls._fields: + _logger.warning("@onchange%r parameters must be field names", func._onchange) + methods[name].append(func) + + # optimization: memoize result on cls, it will not be recomputed + cls._onchange_methods = methods + return methods def __new__(cls): # In the past, this method was registering the model class in the server. @@ -794,19 +819,6 @@ class BaseModel(object): "TransientModels must have log_access turned on, " \ "in order to implement their access rights policy" - # retrieve new-style fields (from above registry class) and duplicate - # them (to avoid clashes with inheritance between different models) - cls._fields = {} - above = cls.__bases__[0] - for attr, field in getmembers(above, Field.__instancecheck__): - cls._add_field(attr, field.new()) - - # introduce magic fields - cls._add_magic_fields() - - # register constraints and onchange methods - cls._init_constraints_onchanges() - # prepare ormcache, which must be shared by all instances of the model cls._ormcache = {} @@ -2951,26 +2963,28 @@ class BaseModel(object): if cls._setup_done: return - # first make sure that parent models determine all their fields + # 1. determine the proper fields of the model; duplicate them on cls to + # avoid clashes with inheritance between different models + for name in getattr(cls, '_fields', {}): + delattr(cls, name) + + # retrieve fields from parent classes + cls._fields = {} + cls._defaults = {} + for attr, field in getmembers(cls, Field.__instancecheck__): + cls._add_field(attr, field.new()) + + # add magic and custom fields + cls._add_magic_fields() + cls._init_manual_fields(self._cr, partial) + + # 2. make sure that parent models determine their own fields, then add + # inherited fields to cls cls._inherits_check() for parent in cls._inherits: self.env[parent]._setup_base(partial) - - # remove inherited fields from cls._fields - for name, field in cls._fields.items(): - if field.inherited: - del cls._fields[name] - - # retrieve custom fields - cls._init_manual_fields(self._cr, partial) - - # retrieve inherited fields cls._init_inherited_fields() - # prepare the setup of fields - for field in cls._fields.itervalues(): - field.reset() - cls._setup_done = True @api.model @@ -2978,11 +2992,13 @@ class BaseModel(object): """ Setup the fields, except for recomputation triggers. """ cls = type(self) - # set up fields, and update their corresponding columns + # set up fields, and determine their corresponding column + cls._columns = {} for name, field in cls._fields.iteritems(): field.setup(self.env) - if field.store or field.column: - cls._columns[name] = field.to_column() + column = field.to_column() + if column: + cls._columns[name] = column # group fields by compute to determine field.computed_fields fields_by_compute = defaultdict(list) @@ -3017,14 +3033,8 @@ class BaseModel(object): # register stuff about low-level function fields cls._init_function_fields(cls.pool, self._cr) - # check constraints - for func in cls._constraint_methods: - if not all(name in cls._fields for name in func._constrains): - _logger.warning("@constrains%r parameters must be field names", func._constrains) - for name in cls._onchange_methods: - if name not in cls._fields: - func = cls._onchange_methods[name] - _logger.warning("@onchange%r parameters must be field names", func._onchange) + # register constraints and onchange methods + cls._init_constraints_onchanges() # check defaults for name in cls._defaults: diff --git a/openerp/osv/fields.py b/openerp/osv/fields.py index 3bf0c268961..c8a961c68f5 100644 --- a/openerp/osv/fields.py +++ b/openerp/osv/fields.py @@ -1293,9 +1293,14 @@ class function(_column): self._symbol_set = type_class._symbol_set def new(self, **args): - # HACK: function fields are tricky to recreate, simply return a copy - import copy - return copy.copy(self) + if args.get('compute'): + # field is computed, we need an instance of the given type + type_class = globals()[self._type] + return type_class(**args) + else: + # HACK: function fields are tricky to recreate, simply return a copy + import copy + return copy.copy(self) def to_field_args(self): args = super(function, self).to_field_args()