diff options
23 files changed, 275 insertions, 258 deletions
diff --git a/README.md b/README.md index 106ffeed09d..d0528a87c11 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,7 @@ -[](http://runbot.odoo.com/runbot) +[](http://runbot.odoo.com/runbot) +[](http://www.odoo.com/documentation/master) +[](https://www.odoo.com/forum/help-1) +[](http://nightly.odoo.com/) Odoo ---- @@ -13,7 +16,6 @@ a full-featured <a href="https://www.odoo.com">Open Source ERP</a> when you inst Getting started with Odoo ------------------------- - For a standard installation please follow the <a href="https://www.odoo.com/documentation/8.0/setup/install.html">Setup instructions</a> from the documentation. diff --git a/addons/account_asset/wizard/account_asset_change_duration.py b/addons/account_asset/wizard/account_asset_change_duration.py index d854e8e57ff..35160c026ff 100644 --- a/addons/account_asset/wizard/account_asset_change_duration.py +++ b/addons/account_asset/wizard/account_asset_change_duration.py @@ -27,43 +27,22 @@ class asset_modify(osv.osv_memory): _name = 'asset.modify' _description = 'Modify Asset' + def _get_asset_method_time(self, cr, uid, ids, field_name, arg, context=None): + if ids and len(ids) == 1 and context.get('active_id'): + asset = self.pool['account.asset.asset'].browse(cr, uid, context.get('active_id'), context=context) + return {ids[0]: asset.method_time} + else: + return dict.fromkeys(ids, False) + _columns = { 'name': fields.char('Reason', required=True), 'method_number': fields.integer('Number of Depreciations', required=True), 'method_period': fields.integer('Period Length'), 'method_end': fields.date('Ending date'), 'note': fields.text('Notes'), + 'asset_method_time': fields.function(_get_asset_method_time, type='char', string='Asset Method Time', readonly=True), } - def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False): - """ Returns views and fields for current model. - @param cr: A database cursor - @param user: ID of the user currently logged in - @param view_id: list of fields, which required to read signatures - @param view_type: defines a view type. it can be one of (form, tree, graph, calender, gantt, search, mdx) - @param context: context arguments, like lang, time zone - @param toolbar: contains a list of reports, wizards, and links related to current model - - @return: Returns a dictionary that contains definition for fields, views, and toolbars - """ - if not context: - context = {} - asset_obj = self.pool.get('account.asset.asset') - result = super(asset_modify, self).fields_view_get(cr, uid, view_id, view_type, context=context, toolbar=toolbar, submenu=submenu) - asset_id = context.get('active_id', False) - active_model = context.get('active_model', '') - if active_model == 'account.asset.asset' and asset_id: - asset = asset_obj.browse(cr, uid, asset_id, context=context) - doc = etree.XML(result['arch']) - if asset.method_time == 'number': - node = doc.xpath("//field[@name='method_end']")[0] - node.set('invisible', '1') - elif asset.method_time == 'end': - node = doc.xpath("//field[@name='method_number']")[0] - node.set('invisible', '1') - result['arch'] = etree.tostring(doc) - return result - def default_get(self, cr, uid, fields, context=None): """ To get default values for the object. @param self: The object pointer. @@ -87,6 +66,8 @@ class asset_modify(osv.osv_memory): res.update({'method_period': asset.method_period}) if 'method_end' in fields and asset.method_time == 'end': res.update({'method_end': asset.method_end}) + if context.get('active_id'): + res['asset_method_time'] = self._get_asset_method_time(cr, uid, [0], 'asset_method_time', [], context=context)[0] return res def modify(self, cr, uid, ids, context=None): diff --git a/addons/account_asset/wizard/account_asset_change_duration_view.xml b/addons/account_asset/wizard/account_asset_change_duration_view.xml index b4c9d1f77fa..daa736fb804 100644 --- a/addons/account_asset/wizard/account_asset_change_duration_view.xml +++ b/addons/account_asset/wizard/account_asset_change_duration_view.xml @@ -7,13 +7,14 @@ <field name="model">asset.modify</field> <field name="arch" type="xml"> <form string="Modify Asset"> + <field name="asset_method_time" invisible="1"/> <group string="Asset Durations to Modify" col="4"> <group colspan="2" col="2"> <field name="name"/> - <field name="method_number"/> + <field name="method_number" attrs="{'invisible': [('asset_method_time', '=', 'end')]}"/> </group> <group colspan="2" col="2"> - <field name="method_end"/> + <field name="method_end" attrs="{'invisible': [('asset_method_time', '=', 'number')]}"/> <label for="method_period"/> <div> <field name="method_period" class="oe_inline"/> months diff --git a/addons/crm/wizard/crm_lead_to_opportunity.py b/addons/crm/wizard/crm_lead_to_opportunity.py index ac06e41b1c4..2d3d09111b3 100644 --- a/addons/crm/wizard/crm_lead_to_opportunity.py +++ b/addons/crm/wizard/crm_lead_to_opportunity.py @@ -67,7 +67,7 @@ class crm_lead2opportunity_partner(osv.osv_memory): tomerge.extend(self._get_duplicated_leads(cr, uid, partner_id, email, include_lost=True, context=context)) tomerge = list(set(tomerge)) - if 'action' in fields: + if 'action' in fields and not res.get('action'): res.update({'action' : partner_id and 'exist' or 'create'}) if 'partner_id' in fields: res.update({'partner_id' : partner_id}) diff --git a/addons/crm/wizard/crm_partner_binding.py b/addons/crm/wizard/crm_partner_binding.py index 03c1c901b6b..a7e4977a866 100644 --- a/addons/crm/wizard/crm_partner_binding.py +++ b/addons/crm/wizard/crm_partner_binding.py @@ -89,7 +89,7 @@ class crm_partner_binding(osv.osv_memory): res = super(crm_partner_binding, self).default_get(cr, uid, fields, context=context) partner_id = self._find_matching_partner(cr, uid, context=context) - if 'action' in fields: + if 'action' in fields and not res.get('action'): res['action'] = partner_id and 'exist' or 'create' if 'partner_id' in fields: res['partner_id'] = partner_id diff --git a/addons/gamification/models/challenge.py b/addons/gamification/models/challenge.py index 22f054eb7a9..699f2e28d83 100644 --- a/addons/gamification/models/challenge.py +++ b/addons/gamification/models/challenge.py @@ -301,18 +301,23 @@ class gamification_challenge(osv.Model): goal_obj = self.pool.get('gamification.goal') - # we use yesterday to update the goals that just ended + # include yesterday goals to update the goals that just ended + # exclude goals for users that did not connect since the last update yesterday = date.today() - timedelta(days=1) - goal_ids = goal_obj.search(cr, uid, [ - ('challenge_id', 'in', ids), - '|', - ('state', '=', 'inprogress'), - '&', - ('state', 'in', ('reached', 'failed')), - '|', - ('end_date', '>=', yesterday.strftime(DF)), - ('end_date', '=', False) - ], context=context) + cr.execute("""SELECT gg.id + FROM gamification_goal as gg, + gamification_challenge as gc, + res_users as ru + WHERE gg.challenge_id = gc.id + AND gg.user_id = ru.id + AND gg.write_date < ru.login_date + AND gg.closed IS false + AND gc.id IN %s + AND (gg.state = 'inprogress' + OR (gg.state = 'reached' + AND (gg.end_date >= %s OR gg.end_date IS NULL))) + """, (tuple(ids), yesterday.strftime(DF))) + goal_ids = [res[0] for res in cr.fetchall()] # update every running goal already generated linked to selected challenges goal_obj.update(cr, uid, goal_ids, context=context) diff --git a/addons/gamification/models/goal.py b/addons/gamification/models/goal.py index 48adf095ac5..ec6f1f9dcb8 100644 --- a/addons/gamification/models/goal.py +++ b/addons/gamification/models/goal.py @@ -269,6 +269,25 @@ class gamification_goal(osv.Model): return {'to_update': True} return {} + def _get_write_values(self, cr, uid, goal, new_value, context=None): + """Generate values to write after recomputation of a goal score""" + if new_value == goal.current: + # avoid useless write if the new value is the same as the old one + return {} + + result = {goal.id: {'current': new_value}} + if (goal.definition_id.condition == 'higher' and new_value >= goal.target_goal) \ + or (goal.definition_id.condition == 'lower' and new_value <= goal.target_goal): + # success, do no set closed as can still change + result[goal.id]['state'] = 'reached' + + elif goal.end_date and fields.date.today() > goal.end_date: + # check goal failure + result[goal.id]['state'] = 'failed' + result[goal.id]['closed'] = True + + return result + def update(self, cr, uid, ids, context=None): """Update the goals to recomputes values and change of states @@ -282,14 +301,8 @@ class gamification_goal(osv.Model): commit = context.get('commit_gamification', False) goals_by_definition = {} - all_goals = {} for goal in self.browse(cr, uid, ids, context=context): - if goal.state in ('draft', 'canceled'): - # draft or canceled goals should not be recomputed - continue - goals_by_definition.setdefault(goal.definition_id, []).append(goal) - all_goals[goal.id] = goal for definition, goals in goals_by_definition.items(): goals_to_write = dict((goal.id, {}) for goal in goals) @@ -314,10 +327,12 @@ class gamification_goal(osv.Model): # the result of the evaluated codeis put in the 'result' local variable, propagated to the context result = cxt.get('result') if result is not None and type(result) in (float, int, long): - if result != goal.current: - goals_to_write[goal.id]['current'] = result + goals_to_write.update( + self._get_write_values(cr, uid, goal, result, context=context) + ) + else: - _logger.exception(_('Invalid return content from the evaluation of code for definition %s' % definition.name)) + _logger.exception(_('Invalid return content from the evaluation of code for definition %s') % definition.name) else: # count or sum @@ -357,8 +372,9 @@ class gamification_goal(osv.Model): queried_value = queried_value[0] if queried_value == query_goals[goal.id]: new_value = user_value.get(field_name+'_count', goal.current) - if new_value != goal.current: - goals_to_write[goal.id]['current'] = new_value + goals_to_write.update( + self._get_write_values(cr, uid, goal, new_value, context=context) + ) else: for goal in goals: @@ -380,26 +396,14 @@ class gamification_goal(osv.Model): else: # computation mode = count new_value = obj.search(cr, uid, domain, context=context, count=True) - # avoid useless write if the new value is the same as the old one - if new_value != goal.current: - goals_to_write[goal.id]['current'] = new_value + goals_to_write.update( + self._get_write_values(cr, uid, goal, new_value, context=context) + ) for goal_id, value in goals_to_write.items(): if not value: continue - goal = all_goals[goal_id] - - # check goal target reached - if (goal.definition_id.condition == 'higher' and value.get('current', goal.current) >= goal.target_goal) \ - or (goal.definition_id.condition == 'lower' and value.get('current', goal.current) <= goal.target_goal): - value['state'] = 'reached' - - # check goal failure - elif goal.end_date and fields.date.today() > goal.end_date: - value['state'] = 'failed' - value['closed'] = True - if value: - self.write(cr, uid, [goal.id], value, context=context) + self.write(cr, uid, [goal_id], value, context=context) if commit: cr.commit() return True diff --git a/addons/google_calendar/res_config.py b/addons/google_calendar/res_config.py index dbb8a633bf8..fb34d58cbe9 100644 --- a/addons/google_calendar/res_config.py +++ b/addons/google_calendar/res_config.py @@ -13,8 +13,8 @@ class calendar_config_settings(osv.TransientModel): def set_calset(self,cr,uid,ids,context=None) : params = self.pool['ir.config_parameter'] myself = self.browse(cr,uid,ids[0],context=context) - params.set_param(cr, uid, 'google_calendar_client_id', myself.cal_client_id.strip() or '', groups=['base.group_system'], context=None) - params.set_param(cr, uid, 'google_calendar_client_secret', myself.cal_client_secret.strip() or '', groups=['base.group_system'], context=None) + params.set_param(cr, uid, 'google_calendar_client_id', (myself.cal_client_id or '').strip(), groups=['base.group_system'], context=None) + params.set_param(cr, uid, 'google_calendar_client_secret', (myself.cal_client_secret or '').strip(), groups=['base.group_system'], context=None) def get_default_all(self,cr,uid,ids,context=None): diff --git a/addons/stock/stock.py b/addons/stock/stock.py index fe2e0e78708..d68dbac1942 100644 --- a/addons/stock/stock.py +++ b/addons/stock/stock.py @@ -1088,6 +1088,7 @@ class stock_picking(osv.osv): # Create the necessary operations for the grouped quants and remaining qtys uom_obj = self.pool.get('product.uom') + prevals = {} for key, qty in qtys_grouped.items(): product = self.pool.get("product.product").browse(cr, uid, key[0], context=context) uom_id = product.uom_id.id @@ -1095,7 +1096,7 @@ class stock_picking(osv.osv): if product_uom.get(key[0]): uom_id = product_uom[key[0]].id qty_uom = uom_obj._compute_qty(cr, uid, product.uom_id.id, qty, uom_id) - vals.append({ + val_dict = { 'picking_id': picking.id, 'product_qty': qty_uom, 'product_id': key[0], @@ -1105,7 +1106,17 @@ class stock_picking(osv.osv): 'location_id': key[4], 'location_dest_id': key[5], 'product_uom_id': uom_id, - }) + } + if key[0] in prevals: + prevals[key[0]].append(val_dict) + else: + prevals[key[0]] = [val_dict] + # prevals var holds the operations in order to create them in the same order than the picking stock moves if possible + processed_products = set() + for move in picking.move_lines: + if move.product_id.id not in processed_products: + vals += prevals.get(move.product_id.id, []) + processed_products.add(move.product_id.id) return vals @api.cr_uid_ids_context diff --git a/addons/web/static/src/js/chrome.js b/addons/web/static/src/js/chrome.js index 0cafc63d829..7e42441b877 100644 --- a/addons/web/static/src/js/chrome.js +++ b/addons/web/static/src/js/chrome.js @@ -600,12 +600,9 @@ instance.web.DatabaseManager = instance.web.Widget.extend({ self.do_notify(_t("Backed"), _t("Database backed up successfully")); }, error: function(error){ - if(error){ - self.display_error({ - title: _t("Backup Database"), - error: 'AccessDenied' - }); - } + if (error && error[1]) { + self.display_error(error[1][0]); + } }, complete: function() { self.unblockUI(); diff --git a/addons/web/static/src/js/view_form.js b/addons/web/static/src/js/view_form.js index 2783b85780c..c87fa98b15c 100644 --- a/addons/web/static/src/js/view_form.js +++ b/addons/web/static/src/js/view_form.js @@ -2615,6 +2615,9 @@ instance.web.form.FieldFloat = instance.web.form.FieldChar.extend({ // As in GTK client, floats default to 0 value_ = 0; } + if (this.digits !== undefined && this.digits.length === 2) { + value_ = instance.web.round_precision(value_, this.digits[1]); + } this._super.apply(this, [value_]); }, focus: function () { diff --git a/addons/website_sale/security/ir.model.access.csv b/addons/website_sale/security/ir.model.access.csv index c1774fffe2c..2101397bf60 100644 --- a/addons/website_sale/security/ir.model.access.csv +++ b/addons/website_sale/security/ir.model.access.csv @@ -10,7 +10,7 @@ access_product_pricelist_item_public,product.pricelist.item.public,product.model access_product_product_price_type_public,product.price.type.public,product.model_product_price_type,,1,0,0,0 access_product_style,product.style.public,website_sale.model_product_style,,1,0,0,0 access_product_supplierinfo,product.supplierinfo.public,product.model_product_supplierinfo,,1,0,0,0 -access_product_attribute_public,product.attribute public,product.model_product_attribute,base.group_public,1,0,0,0 -access_product_attribute_value_public,product.attribute value public,product.model_product_attribute_value,base.group_public,1,0,0,0 -access_product_attribute_price_public,product.attribute price public,product.model_product_attribute_price,base.group_public,1,0,0,0 -access_product_attribute_line_public,product.attribute line public,product.model_product_attribute_line,base.group_public,1,0,0,0 +access_product_attribute_public,product.attribute public,product.model_product_attribute,,1,0,0,0 +access_product_attribute_value_public,product.attribute value public,product.model_product_attribute_value,,1,0,0,0 +access_product_attribute_price_public,product.attribute price public,product.model_product_attribute_price,,1,0,0,0 +access_product_attribute_line_public,product.attribute line public,product.model_product_attribute_line,,1,0,0,0 diff --git a/openerp/addons/base/ir/ir_model.py b/openerp/addons/base/ir/ir_model.py index 77e07c8b672..96f351b9ee3 100644 --- a/openerp/addons/base/ir/ir_model.py +++ b/openerp/addons/base/ir/ir_model.py @@ -387,9 +387,7 @@ class ir_model_fields(osv.osv): if vals['model'].startswith('x_') and vals['name'] == 'x_name': model._rec_name = 'x_name' - if self.pool.fields_by_model is not None: - cr.execute('SELECT * FROM ir_model_fields WHERE id=%s', (res,)) - self.pool.fields_by_model.setdefault(vals['model'], []).append(cr.dictfetchone()) + self.pool.clear_manual_fields() # re-initialize model in registry model.__init__(self.pool, cr) diff --git a/openerp/addons/test_impex/tests/test_load.py b/openerp/addons/test_impex/tests/test_load.py index d9cbd5b100a..2fcf6dc2ad6 100644 --- a/openerp/addons/test_impex/tests/test_load.py +++ b/openerp/addons/test_impex/tests/test_load.py @@ -642,12 +642,11 @@ class test_m2o(ImporterCase): @mute_logger('openerp.sql_db') def test_fail_id_mistype(self): result = self.import_(['value/.id'], [["foo"]]) - - self.assertEqual(result['messages'], [ - message(u"Invalid database id 'foo' for the field 'unknown'", - moreinfo=moreaction(res_model='ir.model.data', - domain=[('model','=','export.integer')])) - ]) + try: + int("foo") + except ValueError, exc: + expected_message = unicode(exc) + self.assertEqual(result['messages'], [message(expected_message)]) self.assertIs(result['ids'], False) def test_sub_field(self): diff --git a/openerp/fields.py b/openerp/fields.py index 030872c0092..b371e257624 100644 --- a/openerp/fields.py +++ b/openerp/fields.py @@ -336,8 +336,6 @@ class Field(object): # determine self.default and cls._defaults in a consistent way self._determine_default(cls, name) - self.reset() - def _determine_default(self, cls, name): """ Retrieve the default value for `self` in the hierarchy of `cls`, and determine `self.default` and `cls._defaults` accordingly. @@ -403,33 +401,40 @@ class Field(object): self.inverse_fields = [] def setup(self, env): - """ Complete the setup of `self` (dependencies, recomputation triggers, - and other properties). This method is idempotent: it has no effect - if `self` has already been set up. - """ + """ Make sure that `self` is set up, except for recomputation triggers. """ if not self.setup_done: - self._setup(env) + if self.related: + self._setup_related(env) + else: + self._setup_regular(env) self.setup_done = True - def _setup(self, env): - """ Do the actual setup of `self`. """ - if self.related: - self._setup_related(env) - else: - self._setup_regular(env) + # + # Setup of non-related fields + # - # put invalidation/recomputation triggers on field dependencies - model = env[self.model_name] - for path in self.depends: - self._setup_dependency([], model, path.split('.')) + def _setup_regular(self, env): + """ Setup the attributes of a non-related field. """ + recs = env[self.model_name] + + def make_depends(deps): + return tuple(deps(recs) if callable(deps) else deps) - # put invalidation triggers on model dependencies - for dep_model_name, field_names in model._depends.iteritems(): - dep_model = env[dep_model_name] - dep_model._setup_fields() - for field_name in field_names: - field = dep_model._fields[field_name] - field._triggers.add((self, None)) + # convert compute into a callable and determine depends + if isinstance(self.compute, basestring): + # if the compute method has been overridden, concatenate all their _depends + self.depends = () + for method in resolve_all_mro(type(recs), self.compute, reverse=True): + self.depends += make_depends(getattr(method, '_depends', ())) + self.compute = getattr(type(recs), self.compute) + else: + self.depends = make_depends(getattr(self.compute, '_depends', ())) + + # convert inverse and search into callables + if isinstance(self.inverse, basestring): + self.inverse = getattr(type(recs), self.inverse) + if isinstance(self.search, basestring): + self.search = getattr(type(recs), self.search) # # Setup of related fields @@ -445,7 +450,6 @@ class Field(object): recs = env[self.model_name] fields = [] for name in self.related: - recs._setup_fields() field = recs._fields[name] field.setup(env) recs = recs[name] @@ -524,31 +528,14 @@ class Field(object): return self.related_field if self.inherited else self # - # Setup of non-related fields + # Setup of field triggers # - def _setup_regular(self, env): - """ Setup the attributes of a non-related field. """ - recs = env[self.model_name] - - def make_depends(deps): - return tuple(deps(recs) if callable(deps) else deps) - - # convert compute into a callable and determine depends - if isinstance(self.compute, basestring): - # if the compute method has been overridden, concatenate all their _depends - self.depends = () - for method in resolve_all_mro(type(recs), self.compute, reverse=True): - self.depends += make_depends(getattr(method, '_depends', ())) - self.compute = getattr(type(recs), self.compute) - else: - self.depends = make_depends(getattr(self.compute, '_depends', ())) - - # convert inverse and search into callables - if isinstance(self.inverse, basestring): - self.inverse = getattr(type(recs), self.inverse) - if isinstance(self.search, basestring): - self.search = getattr(type(recs), self.search) + def setup_triggers(self, env): + """ Add the necessary triggers to invalidate/recompute `self`. """ + model = env[self.model_name] + for path in self.depends: + self._setup_dependency([], model, path.split('.')) def _setup_dependency(self, path0, model, path1): """ Make `self` depend on `model`; `path0 + path1` is a dependency of @@ -558,7 +545,6 @@ class Field(object): env = model.env head, tail = path1[0], path1[1:] - model._setup_fields() if head == '*': # special case: add triggers on all fields of model (except self) fields = set(model._fields.itervalues()) - set([self]) @@ -571,8 +557,6 @@ class Field(object): self.recursive = True continue - field.setup(env) - #_logger.debug("Add trigger on %s to recompute %s", field, self) field._triggers.add((self, '.'.join(path0 or ['id']))) @@ -1050,8 +1034,8 @@ class Char(_String): type = 'char' size = None - def _setup(self, env): - super(Char, self)._setup(env) + def _setup_regular(self, env): + super(Char, self)._setup_regular(env) assert isinstance(self.size, (NoneType, int)), \ "Char field %s with non-integer size %r" % (self, self.size) @@ -1253,8 +1237,8 @@ class Selection(Field): selection = api.expected(api.model, selection) super(Selection, self).__init__(selection=selection, string=string, **kwargs) - def _setup(self, env): - super(Selection, self)._setup(env) + def _setup_regular(self, env): + super(Selection, self)._setup_regular(env) assert self.selection is not None, "Field %s without selection" % self def _setup_related(self, env): @@ -1341,8 +1325,8 @@ class Reference(Selection): def __init__(self, selection=None, string=None, **kwargs): super(Reference, self).__init__(selection=selection, string=string, **kwargs) - def _setup(self, env): - super(Reference, self)._setup(env) + def _setup_regular(self, env): + super(Reference, self)._setup_regular(env) assert isinstance(self.size, (NoneType, int)), \ "Reference field %s with non-integer size %r" % (self, self.size) @@ -1378,8 +1362,8 @@ class _Relational(Field): domain = None # domain for searching values context = None # context for searching values - def _setup(self, env): - super(_Relational, self)._setup(env) + def _setup_regular(self, env): + super(_Relational, self)._setup_regular(env) if self.comodel_name not in env.registry: _logger.warning("Field %s with unknown comodel_name %r" % (self, self.comodel_name)) @@ -1664,7 +1648,6 @@ class One2many(_RelationalMulti): if self.inverse_name: # link self to its inverse field and vice-versa comodel = env[self.comodel_name] - comodel._setup_fields() invf = comodel._fields[self.inverse_name] # In some rare cases, a `One2many` field can link to `Int` field # (res_model/res_id pattern). Only inverse the field if this is diff --git a/openerp/models.py b/openerp/models.py index eec8f99e17b..6e084dde3da 100644 --- a/openerp/models.py +++ b/openerp/models.py @@ -476,11 +476,7 @@ class BaseModel(object): # basic setup of field field.set_class_name(cls, name) - if field.store or field.column: - cls._columns[name] = field.to_column() - else: - # remove potential column that may be overridden by field - cls._columns.pop(name, None) + # cls._columns will be updated once fields are set up @classmethod def _pop_field(cls, name): @@ -600,13 +596,6 @@ class BaseModel(object): # inferred metadata; use its ancestor instead parent_class = type(parent_model).__base__ - # don't inherit custom fields - columns = dict((key, val) - for key, val in parent_class._columns.iteritems() - if not val.manual - ) - columns.update(cls._columns) - inherits = dict(parent_class._inherits) inherits.update(cls._inherits) @@ -630,7 +619,6 @@ class BaseModel(object): attrs = { '_name': name, '_register': False, - '_columns': columns, '_inherits': inherits, '_depends': depends, '_constraints': constraints, @@ -643,7 +631,7 @@ class BaseModel(object): attrs = { '_name': name, '_register': False, - '_columns': dict(cls._columns), + '_columns': {}, # filled by _setup_fields() '_defaults': {}, # filled by Field._determine_default() '_inherits': dict(cls._inherits), '_depends': dict(cls._depends), @@ -699,16 +687,11 @@ class BaseModel(object): pool._store_function[model].sort(key=lambda x: x[4]) @classmethod - def _init_manual_fields(cls, cr, partial=False): - # Check whether the query is already done - if cls.pool.fields_by_model is not None: - manual_fields = cls.pool.fields_by_model.get(cls._name, []) - else: - cr.execute('SELECT * FROM ir_model_fields WHERE model=%s AND state=%s', (cls._name, 'manual')) - manual_fields = cr.dictfetchall() + def _init_manual_fields(cls, cr, partial): + manual_fields = cls.pool.get_manual_fields(cr, cls._name) - for field in manual_fields: - if field['name'] in cls._fields: + for name, field in manual_fields.iteritems(): + if name in cls._fields: continue attrs = { 'manual': True, @@ -729,7 +712,11 @@ class BaseModel(object): attrs['ondelete'] = field['on_delete'] attrs['domain'] = eval(field['domain']) if field['domain'] else None elif field['ttype'] == 'one2many': - if partial and field['relation'] not in cls.pool: + if partial and not ( + field['relation'] in cls.pool and ( + field['relation_field'] in cls.pool[field['relation']]._fields or + field['relation_field'] in cls.pool.get_manual_fields(cr, field['relation']) + )): continue attrs['comodel_name'] = field['relation'] attrs['inverse_name'] = field['relation_field'] @@ -740,11 +727,11 @@ class BaseModel(object): attrs['comodel_name'] = field['relation'] _rel1 = field['relation'].replace('.', '_') _rel2 = field['model'].replace('.', '_') - attrs['relation'] = 'x_%s_%s_%s_rel' % (_rel1, _rel2, field['name']) + attrs['relation'] = 'x_%s_%s_%s_rel' % (_rel1, _rel2, name) attrs['column1'] = 'id1' attrs['column2'] = 'id2' attrs['domain'] = eval(field['domain']) if field['domain'] else None - cls._add_field(field['name'], Field.by_type[field['ttype']](**attrs)) + cls._add_field(name, Field.by_type[field['ttype']](**attrs)) @classmethod def _init_constraints_onchanges(cls): @@ -817,9 +804,6 @@ class BaseModel(object): # introduce magic fields cls._add_magic_fields() - # register stuff about low-level function fields and custom fields - cls._init_function_fields(pool, cr) - # register constraints and onchange methods cls._init_constraints_onchanges() @@ -1700,7 +1684,7 @@ class BaseModel(object): return False @api.model - def name_search(self, name='', args=None, operator='ilike', limit=100): + def name_search(self, name='', args=None, operator=None, limit=100): """ name_search(name='', args=None, operator='ilike', limit=100) -> records Search for records that have a display name matching the given @@ -1726,7 +1710,7 @@ class BaseModel(object): """ return self._name_search(name, args, operator, limit=limit) - def _name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=100, name_get_uid=None): + def _name_search(self, cr, user, name='', args=None, operator=None, context=None, limit=100, name_get_uid=None): # private implementation of name_search, allows passing a dedicated user # for the name_get part to solve some access rights issues args = list(args or []) @@ -1734,6 +1718,9 @@ class BaseModel(object): if not self._rec_name: _logger.warning("Cannot execute name_search, no _rec_name defined on %s", self._name) elif not (name == '' and operator == 'ilike'): + if operator is None: + field = self._fields[self._rec_name] + operator = ('=' if field.type == 'integer' else 'ilike') args += [(self._rec_name, operator, name)] access_rights_uid = name_get_uid or user ids = self._search(cr, user, args, limit=limit, context=context, access_rights_uid=access_rights_uid) @@ -2876,18 +2863,12 @@ class BaseModel(object): # @classmethod - def _inherits_reload(cls): - """ Recompute the _inherit_fields mapping, and inherited fields. """ - struct = {} + def _init_inherited_fields(cls): + """ Determine inherited fields. """ + # determine candidate inherited fields fields = {} for parent_model, parent_field in cls._inherits.iteritems(): parent = cls.pool[parent_model] - # old-api struct for _inherit_fields - for name, column in parent._columns.iteritems(): - struct[name] = (parent_model, parent_field, column, parent_model) - for name, source in parent._inherit_fields.iteritems(): - struct[name] = (parent_model, parent_field, source[2], source[3]) - # new-api fields for _fields for name, field in parent._fields.iteritems(): fields[name] = field.new( inherited=True, @@ -2895,16 +2876,26 @@ class BaseModel(object): related_sudo=False, ) - # old-api stuff - cls._inherit_fields = struct - cls._all_columns = cls._get_column_infos() - # add inherited fields that are not redefined locally for name, field in fields.iteritems(): if name not in cls._fields: cls._add_field(name, field) @classmethod + def _inherits_reload(cls): + """ Recompute the _inherit_fields and _all_columns mappings. """ + cls._inherit_fields = struct = {} + for parent_model, parent_field in cls._inherits.iteritems(): + parent = cls.pool[parent_model] + for name, column in parent._columns.iteritems(): + struct[name] = (parent_model, parent_field, column, parent_model) + for name, source in parent._inherit_fields.iteritems(): + struct[name] = (parent_model, parent_field, source[2], source[3]) + + # old-api stuff + cls._all_columns = cls._get_column_infos() + + @classmethod def _get_column_infos(cls): """Returns a dict mapping all fields names (direct fields and inherited field via _inherits) to a ``column_info`` struct @@ -2920,14 +2911,16 @@ class BaseModel(object): @classmethod def _inherits_check(cls): for table, field_name in cls._inherits.items(): - if field_name not in cls._columns: + field = cls._fields.get(field_name) + if not field: _logger.info('Missing many2one field definition for _inherits reference "%s" in "%s", using default one.', field_name, cls._name) - cls._columns[field_name] = fields.many2one(table, string="Automatically created field to link to parent %s" % table, - required=True, ondelete="cascade") - elif not cls._columns[field_name].required or cls._columns[field_name].ondelete.lower() not in ("cascade", "restrict"): + from .fields import Many2one + field = Many2one(table, string="Automatically created field to link to parent %s" % table, required=True, ondelete="cascade") + cls._add_field(field_name, field) + elif not field.required or field.ondelete.lower() not in ("cascade", "restrict"): _logger.warning('Field definition for _inherits reference "%s" in "%s" must be marked as "required" with ondelete="cascade" or "restrict", forcing it to required + cascade.', field_name, cls._name) - cls._columns[field_name].required = True - cls._columns[field_name].ondelete = "cascade" + field.required = True + field.ondelete = "cascade" # reflect fields with delegate=True in dictionary cls._inherits for field in cls._fields.itervalues(): @@ -2940,44 +2933,48 @@ class BaseModel(object): cls._inherits[field.comodel_name] = field.name @api.model - def _prepare_setup_fields(self): - """ Prepare the setup of fields once the models have been loaded. """ + def _prepare_setup(self): + """ Prepare the setup of the model. """ type(self)._setup_done = False - for name, field in self._fields.items(): - if field.inherited: - del self._fields[name] - else: - field.reset() @api.model - def _setup_fields(self, partial=False): - """ Setup the fields (dependency triggers, etc). - - :param partial: ``True`` if all models have not been loaded yet. - """ + def _setup_base(self, partial): + """ Determine the inherited and custom fields of the model. """ cls = type(self) if cls._setup_done: return - cls._setup_done = True - # first make sure that parent models are all set up - for parent in self._inherits: - self.env[parent]._setup_fields() + # first make sure that parent models determine all their fields + 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=partial) + cls._init_manual_fields(self._cr, partial) # retrieve inherited fields - cls._inherits_check() - cls._inherits_reload() + cls._init_inherited_fields() - # set up fields + # prepare the setup of fields for field in cls._fields.itervalues(): - field.setup(self.env) + field.reset() - # update columns (fields may have changed) + cls._setup_done = True + + @api.model + def _setup_fields(self): + """ Setup the fields, except for recomputation triggers. """ + cls = type(self) + + # set up fields, and update their corresponding columns for name, field in cls._fields.iteritems(): - if field.column: + field.setup(self.env) + if field.store or field.column: cls._columns[name] = field.to_column() # group fields by compute to determine field.computed_fields @@ -2989,6 +2986,30 @@ class BaseModel(object): else: field.computed_fields = [] + @api.model + def _setup_complete(self): + """ Setup recomputation triggers, and complete the model setup. """ + cls = type(self) + + # set up field triggers + for field in cls._fields.itervalues(): + field.setup_triggers(self.env) + + # add invalidation triggers on model dependencies + if cls._depends: + triggers = [(field, None) for field in cls._fields.itervalues()] + for model_name, field_names in cls._depends.iteritems(): + model = self.env[model_name] + for field_name in field_names: + field = model._fields[field_name] + field._triggers.update(triggers) + + # determine old-api cls._inherit_fields and cls._all_columns + cls._inherits_reload() + + # 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): diff --git a/openerp/modules/loading.py b/openerp/modules/loading.py index 33475493115..29a7e3e68e6 100644 --- a/openerp/modules/loading.py +++ b/openerp/modules/loading.py @@ -125,13 +125,7 @@ def load_module_graph(cr, graph, status=None, perform_checks=True, skip_modules= migrations = openerp.modules.migration.MigrationManager(cr, graph) _logger.info('loading %d modules...', len(graph)) - # Query manual fields for all models at once and save them on the registry - # so the initialization code for each model does not have to do it - # one model at a time. - registry.fields_by_model = {} - cr.execute('SELECT * FROM ir_model_fields WHERE state=%s', ('manual',)) - for field in cr.dictfetchall(): - registry.fields_by_model.setdefault(field['model'], []).append(field) + registry.clear_manual_fields() # register, instantiate and initialize models for each modules t0 = time.time() @@ -161,13 +155,6 @@ def load_module_graph(cr, graph, status=None, perform_checks=True, skip_modules= registry.setup_models(cr, partial=True) init_module_models(cr, package.name, models) - # Can't put this line out of the loop: ir.module.module will be - # registered by init_module_models() above. - modobj = registry['ir.module.module'] - - if perform_checks: - modobj.check(cr, SUPERUSER_ID, [module_id]) - idref = {} mode = 'update' @@ -175,6 +162,13 @@ def load_module_graph(cr, graph, status=None, perform_checks=True, skip_modules= mode = 'init' if hasattr(package, 'init') or hasattr(package, 'update') or package.state in ('to install', 'to upgrade'): + # Can't put this line out of the loop: ir.module.module will be + # registered by init_module_models() above. + modobj = registry['ir.module.module'] + + if perform_checks: + modobj.check(cr, SUPERUSER_ID, [module_id]) + if package.state=='to upgrade': # upgrading the module information modobj.write(cr, SUPERUSER_ID, [module_id], modobj.get_values_from_terp(package.data)) @@ -226,9 +220,7 @@ def load_module_graph(cr, graph, status=None, perform_checks=True, skip_modules= _logger.log(25, "%s modules loaded in %.2fs, %s queries", len(graph), time.time() - t0, openerp.sql_db.sql_counter - t0_sql) - # The query won't be valid for models created later (i.e. custom model - # created after the registry has been loaded), so empty its result. - registry.fields_by_model = None + registry.clear_manual_fields() cr.commit() diff --git a/openerp/modules/registry.py b/openerp/modules/registry.py index 47980eea091..2a3b28f6c8f 100644 --- a/openerp/modules/registry.py +++ b/openerp/modules/registry.py @@ -22,7 +22,7 @@ """ Models registries. """ -from collections import Mapping +from collections import Mapping, defaultdict import logging import os import threading @@ -51,7 +51,7 @@ class Registry(Mapping): self._init = True self._init_parent = {} self._assertion_report = assertion_report.assertion_report() - self.fields_by_model = None + self._fields_by_model = None # modules fully loaded (maintained during init phase by `loading` module) self._init_modules = set() @@ -114,6 +114,20 @@ class Registry(Mapping): fields.append(model_fields[fname]) return fields + def clear_manual_fields(self): + """ Invalidate the cache for manual fields. """ + self._fields_by_model = None + + def get_manual_fields(self, cr, model_name): + """ Return the manual fields (as a dict) for the given model. """ + if self._fields_by_model is None: + # Query manual fields for all models at once + self._fields_by_model = dic = defaultdict(dict) + cr.execute('SELECT * FROM ir_model_fields WHERE state=%s', ('manual',)) + for field in cr.dictfetchall(): + dic[field['model']][field['name']] = field + return self._fields_by_model[model_name] + def do_parent_store(self, cr): for o in self._init_parent: self.get(o)._parent_store_compute(cr) @@ -166,12 +180,18 @@ class Registry(Mapping): # prepare the setup on all models for model in self.models.itervalues(): - model._prepare_setup_fields(cr, SUPERUSER_ID) + model._prepare_setup(cr, SUPERUSER_ID) # do the actual setup from a clean state self._m2m = {} for model in self.models.itervalues(): - model._setup_fields(cr, SUPERUSER_ID, partial=partial) + model._setup_base(cr, SUPERUSER_ID, partial) + + for model in self.models.itervalues(): + model._setup_fields(cr, SUPERUSER_ID) + + for model in self.models.itervalues(): + model._setup_complete(cr, SUPERUSER_ID) def clear_caches(self): """ Clear the caches diff --git a/openerp/osv/expression.py b/openerp/osv/expression.py index 0dbe417b814..81138c76c8b 100644 --- a/openerp/osv/expression.py +++ b/openerp/osv/expression.py @@ -1195,10 +1195,6 @@ class expression(object): query, params = self.__leaf_to_sql( create_substitution_leaf(eleaf, (left, '=', right), model)) - elif left == 'id': - query = '%s.id %s %%s' % (table_alias, operator) - params = right - else: need_wildcard = operator in ('like', 'ilike', 'not like', 'not ilike') sql_operator = {'=like': 'like', '=ilike': 'ilike'}.get(operator, operator) diff --git a/openerp/tools/misc.py b/openerp/tools/misc.py index 11d0ebb7088..2f02eb8db2a 100644 --- a/openerp/tools/misc.py +++ b/openerp/tools/misc.py @@ -119,7 +119,7 @@ def exec_pg_command(name, *args): prog = find_pg_tool(name) env = exec_pg_environ() with open(os.devnull) as dn: - rc = subprocess.call((prog,) + args, stdout=dn, stderr=subprocess.STDOUT) + rc = subprocess.call((prog,) + args, env=env, stdout=dn, stderr=subprocess.STDOUT) if rc: raise Exception('Postgres subprocess %s error %s' % (args2, rc)) diff --git a/setup/package.dfcentos b/setup/package.dfcentos index cd494534c51..1ad8141c887 100644 --- a/setup/package.dfcentos +++ b/setup/package.dfcentos @@ -42,8 +42,10 @@ RUN yum install -d 0 -e 0 epel-release -y && \ RUN easy_install pyPdf vatnumber pydot psycogreen -# Manual start of postgres +# Postgres configuration RUN mkdir -p /var/lib/postgres/data RUN chown -R postgres:postgres /var/lib/postgres/data RUN su postgres -c "initdb -D /var/lib/postgres/data -E UTF-8" -RUN cp /usr/share/pgsql/postgresql.conf.sample /var/lib/postgres/data/postgresql.conf
\ No newline at end of file +RUN cp /usr/share/pgsql/postgresql.conf.sample /var/lib/postgres/data/postgresql.conf + +RUN echo "PS1=\"[\u@nightly-tests] # \"" > ~/.bashrc diff --git a/setup/package.dfdebian b/setup/package.dfdebian index 8afcad6431b..f59233e4db4 100644 --- a/setup/package.dfdebian +++ b/setup/package.dfdebian @@ -68,3 +68,5 @@ RUN apt-get update -qq && \ COPY requirements.txt /opt/release/requirements.txt RUN pip install -r /opt/release/requirements.txt + +RUN echo "PS1=\"[\u@nightly-tests] # \"" > ~/.bashrc diff --git a/setup/package.py b/setup/package.py index 7de6b48fba8..ce0db21bcf9 100755 --- a/setup/package.py +++ b/setup/package.py @@ -120,7 +120,7 @@ class OdooDocker(object): def __init__(self): self.log_file = NamedTemporaryFile(mode='w+b', prefix="bash", suffix=".txt", delete=False) self.port = 8069 # TODO sle: reliable way to get a free port? - self.prompt_re = '(\r\nroot@|bash-).*# ' + self.prompt_re = '\[root@nightly-tests\] #' self.timeout = 600 def system(self, command): |
