diff --git a/doc/03_module_dev_03.rst b/doc/03_module_dev_03.rst index 40921eebcb5..da0e6da1090 100644 --- a/doc/03_module_dev_03.rst +++ b/doc/03_module_dev_03.rst @@ -37,7 +37,6 @@ There are two types of views: .. note:: Since OpenERP 4.1, form views can also contain graphs. - Form views ---------- @@ -388,6 +387,33 @@ The easiest method to compute real statistics on objects is: You can get en example in all modules of the form: report\_.... Example: report_crm. +Controlling view actions +------------------------ + +When defining a view, the following attributes can be added on the +opening element of the view (i.e. ``
``, ````...) + +``create`` + set to ``false`` to hide the link / button which allows to create a new + record. + +``delete`` + set to ``false`` to hide the link / button which allows to remove a + record. + +``edit`` + set to ``false`` to hide the link / button which allows to + edit a record. + + +These attributes are available on form, tree, kanban and gantt +views. They are normally automatically set from the access rights of +the users, but can be forced globally in the view definition. A +possible use case for these attributes is to define an inner tree view +for a one2many relation inside a form view, in which the user cannot +add or remove related records, but only edit the existing ones (which +are presumably created through another way, such as a wizard). + Calendar Views -------------- @@ -680,6 +706,7 @@ toolbar its descendants will be displayed in the main tree. The value is ignored for flat lists. + Grouping Elements +++++++++++++++++ @@ -1351,12 +1378,22 @@ When you add a one2many field in a form view, you do something like this : If you want to specify the views to use, you can add a *context* attribute, and specify a view id for each type of view supported, exactly like the action's -*view_id* attribute: +*view_id* attribute, except that the provided view id must always be +fully-qualified with the module name, even if it belongs to the same module: .. code-block:: xml + context="{'form_view_ref': 'module.view_id', + 'tree_view_ref': 'module.view_id'}"/> + +.. note:: + + You *have to* put the module name in the view_id, because this + is evaluated when the view is displayed, and not when the XML file + is parsed, so the module name information is not available. Failing + to do so will result in the default view being selected (see + below). If you don't specify the views, OpenERP will choose one in this order : diff --git a/openerp/addons/base/res/res_config.py b/openerp/addons/base/res/res_config.py index 00fb87d93fe..abfe8df7268 100644 --- a/openerp/addons/base/res/res_config.py +++ b/openerp/addons/base/res/res_config.py @@ -49,10 +49,10 @@ class res_config_module_installation_mixin(object): to_install_missing_names.append(name) elif module.state == 'uninstalled': to_install_ids.append(module.id) - + result = None if to_install_ids: - ir_module.button_immediate_install(cr, uid, to_install_ids, context=context) - + result = ir_module.button_immediate_install(cr, uid, to_install_ids, context=context) + #FIXME: if result is not none, the corresponding todo will be skipped because it was just marked done if to_install_missing_names: return { 'type': 'ir.actions.client', @@ -60,7 +60,7 @@ class res_config_module_installation_mixin(object): 'params': {'modules': to_install_missing_names}, } - return None + return result class res_config_configurable(osv.osv_memory): ''' Base classes for new-style configuration items diff --git a/openerp/addons/base/res/res_partner.py b/openerp/addons/base/res/res_partner.py index 97b0ac454ae..02ceebf5777 100644 --- a/openerp/addons/base/res/res_partner.py +++ b/openerp/addons/base/res/res_partner.py @@ -481,7 +481,11 @@ class res_partner(osv.osv, format_address): if partner.child_ids: # 2a. Commercial Fields: sync if commercial entity if partner.commercial_partner_id == partner: - self._commercial_sync_to_children(cr, uid, partner, context=context) + commercial_fields = self._commercial_fields(cr, uid, + context=context) + if any(field in update_values for field in commercial_fields): + self._commercial_sync_to_children(cr, uid, partner, + context=context) # 2b. Address fields: sync if address changed address_fields = self._address_fields(cr, uid, context=context) if any(field in update_values for field in address_fields): @@ -504,6 +508,16 @@ class res_partner(osv.osv, format_address): def write(self, cr, uid, ids, vals, context=None): if isinstance(ids, (int, long)): ids = [ids] + #res.partner must only allow to set the company_id of a partner if it + #is the same as the company of all users that inherit from this partner + #(this is to allow the code from res_users to write to the partner!) or + #if setting the company_id to False (this is compatible with any user company) + if vals.get('company_id'): + for partner in self.browse(cr, uid, ids, context=context): + if partner.user_ids: + user_companies = set([user.company_id.id for user in partner.user_ids]) + if len(user_companies) > 1 or vals['company_id'] not in user_companies: + raise osv.except_osv(_("Warning"),_("You can not change the company as the partner/user has multiple user linked with different companies.")) result = super(res_partner,self).write(cr, uid, ids, vals, context=context) for partner in self.browse(cr, uid, ids, context=context): self._fields_sync(cr, uid, partner, vals, context) @@ -604,14 +618,15 @@ class res_partner(osv.osv, format_address): if operator in ('=ilike', '=like'): operator = operator[1:] query_args = {'name': search_name} - limit_str = '' + query = ('''SELECT id FROM res_partner + WHERE email ''' + operator + ''' %(name)s + OR display_name ''' + operator + ''' %(name)s + ORDER BY display_name + ''') if limit: - limit_str = ' limit %(limit)s' + query += ' limit %(limit)s' query_args['limit'] = limit - cr.execute('''SELECT partner.id FROM res_partner partner - LEFT JOIN res_partner company ON partner.parent_id = company.id - WHERE partner.email ''' + operator +''' %(name)s OR - partner.display_name ''' + operator + ' %(name)s ' + limit_str, query_args) + cr.execute(query, query_args) ids = map(lambda x: x[0], cr.fetchall()) ids = self.search(cr, uid, [('id', 'in', ids)] + args, limit=limit, context=context) if ids: diff --git a/openerp/addons/base/res/res_users.py b/openerp/addons/base/res/res_users.py index 93d6d307484..ba709a48175 100644 --- a/openerp/addons/base/res/res_users.py +++ b/openerp/addons/base/res/res_users.py @@ -283,6 +283,13 @@ class res_users(osv.osv): return result + def create(self, cr, uid, vals, context=None): + user_id = super(res_users, self).create(cr, uid, vals, context=context) + user = self.browse(cr, uid, user_id, context=context) + if user.partner_id.company_id: + user.partner_id.write({'company_id': user.company_id.id}) + return user_id + def write(self, cr, uid, ids, values, context=None): if not hasattr(ids, '__iter__'): ids = [ids] @@ -297,7 +304,11 @@ class res_users(osv.osv): uid = 1 # safe fields only, so we write as super-user to bypass access rights res = super(res_users, self).write(cr, uid, ids, values, context=context) - + if 'company_id' in values: + for user in self.browse(cr, uid, ids, context=context): + # if partner is global we keep it that way + if user.partner_id.company_id and user.partner_id.company_id.id != values['company_id']: + user.partner_id.write({'company_id': user.company_id.id}) # clear caches linked to the users self.pool['ir.model.access'].call_cache_clearing_methods(cr) clear = partial(self.pool['ir.rule'].clear_cache, cr) diff --git a/openerp/addons/base/security/ir.model.access.csv b/openerp/addons/base/security/ir.model.access.csv index e3198abb8fa..e17ffb314e1 100644 --- a/openerp/addons/base/security/ir.model.access.csv +++ b/openerp/addons/base/security/ir.model.access.csv @@ -110,9 +110,7 @@ "access_res_bank_user","res_bank user","model_res_bank","group_user",1,0,0,0 "access_multi_company_default user","multi_company_default all","model_multi_company_default",,1,0,0,0 "access_multi_company_default manager","multi_company_default Manager","model_multi_company_default","group_erp_manager",1,1,1,1 -"access_ir_filter all","ir_filters all","model_ir_filters",,1,0,0,0 -"access_ir_filter employee","ir_filters employee","model_ir_filters","group_user",1,1,1,1 -"access_ir_filters","ir_filters_all","model_ir_filters",,1,1,1,1 +"access_ir_filter all","ir_filters all","model_ir_filters",,1,1,1,1 "access_ir_config_parameter","ir_config_parameter","model_ir_config_parameter",,1,0,0,0 "access_ir_mail_server","ir_mail_server","model_ir_mail_server","group_system",1,1,1,1 "access_ir_actions_client","ir_actions_client all","model_ir_actions_client",,1,0,0,0 diff --git a/openerp/modules/loading.py b/openerp/modules/loading.py index c288671a7e1..814c1dac5d9 100644 --- a/openerp/modules/loading.py +++ b/openerp/modules/loading.py @@ -315,13 +315,21 @@ def load_modules(db, force_demo=False, status=None, update_module=False): # they are part of the "currently installed" modules. They will # be dropped in STEP 6 later, before restarting the loading # process. - states_to_load = ['installed', 'to upgrade', 'to remove'] - processed = load_marked_modules(cr, graph, states_to_load, force, status, report, loaded_modules, update_module) - processed_modules.extend(processed) - if update_module: - states_to_load = ['to install'] - processed = load_marked_modules(cr, graph, states_to_load, force, status, report, loaded_modules, update_module) - processed_modules.extend(processed) + # IMPORTANT 2: We have to loop here until all relevant modules have been + # processed, because in some rare cases the dependencies have + # changed, and modules that depend on an uninstalled module + # will not be processed on the first pass. + # It's especially useful for migrations. + previously_processed = -1 + while previously_processed < len(processed_modules): + previously_processed = len(processed_modules) + processed_modules += load_marked_modules(cr, graph, + ['installed', 'to upgrade', 'to remove'], + force, status, report, loaded_modules, update_module) + if update_module: + processed_modules += load_marked_modules(cr, graph, + ['to install'], force, status, report, + loaded_modules, update_module) # load custom models cr.execute('select model from ir_model where state=%s', ('manual',)) diff --git a/openerp/tools/mail.py b/openerp/tools/mail.py index 7895f2c7192..6c4776bf0e4 100644 --- a/openerp/tools/mail.py +++ b/openerp/tools/mail.py @@ -43,6 +43,9 @@ _logger = logging.getLogger(__name__) tags_to_kill = ["script", "head", "meta", "title", "link", "style", "frame", "iframe", "base", "object", "embed"] tags_to_remove = ['html', 'body', 'font'] +# allow new semantic HTML5 tags +allowed_tags = clean.defs.tags | frozenset('article section header footer hgroup nav aside figure'.split()) +safe_attrs = clean.defs.safe_attrs | frozenset(['style']) def html_sanitize(src, silent=True): if not src: @@ -59,6 +62,8 @@ def html_sanitize(src, silent=True): 'page_structure': True, 'style': False, # do not remove style attributes 'forms': True, # remove form tags + 'remove_unknown_tags': False, + 'allow_tags': allowed_tags, } if etree.LXML_VERSION >= (2, 3, 1): # kill_tags attribute has been added in version 2.3.1 @@ -72,7 +77,7 @@ def html_sanitize(src, silent=True): if etree.LXML_VERSION >= (3, 1, 0): kwargs.update({ 'safe_attrs_only': True, - 'safe_attrs': clean.defs.safe_attrs | set(['style']), + 'safe_attrs': safe_attrs, }) else: # lxml < 3.1.0 does not allow to specify safe_attrs. We keep all attributes in order to keep "style"