diff options
| author | Christophe Simonis <chs@openerp.com> | 2013-04-23 20:37:23 +0200 |
|---|---|---|
| committer | Christophe Simonis <chs@openerp.com> | 2013-04-23 20:37:23 +0200 |
| commit | 261dea6fec607d8e1d216acddda1d845e205b698 (patch) | |
| tree | e4fe87158a63abcc795ce5f957610c1870f2ad40 | |
| parent | 8590e68b2b399394c8b0541e885949fc40bca898 (diff) | |
| parent | 60f0bd08eec2d1b513549f5714f6c1c3e2a06045 (diff) | |
[MERGE] forward port of server v7 up to revision 4954
bzr revid: qdp-launchpad@openerp.com-20130422093655-txqmbalod6qmw1s6
bzr revid: chs@openerp.com-20130423183723-9kiexdyzdew8iuzm
22 files changed, 661 insertions, 193 deletions
diff --git a/openerp/addons/base/ir/ir_cron.py b/openerp/addons/base/ir/ir_cron.py index bd2ab610bf4..60a69cd6b38 100644 --- a/openerp/addons/base/ir/ir_cron.py +++ b/openerp/addons/base/ir/ir_cron.py @@ -31,9 +31,12 @@ from openerp.osv import fields, osv from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT from openerp.tools.safe_eval import safe_eval as eval from openerp.tools.translate import _ +from openerp.modules import load_information_from_description_file _logger = logging.getLogger(__name__) +BASE_VERSION = load_information_from_description_file('base')['version'] + def str2tuple(s): return eval('tuple(%s)' % (s or '')) @@ -195,12 +198,17 @@ class ir_cron(osv.osv): cr = db.cursor() jobs = [] try: - # Careful to compare timestamps with 'UTC' - everything is UTC as of v6.1. - cr.execute("""SELECT * FROM ir_cron - WHERE numbercall != 0 - AND active AND nextcall <= (now() at time zone 'UTC') - ORDER BY priority""") - jobs = cr.dictfetchall() + # Make sure the database we poll has the same version as the code of base + cr.execute("SELECT 1 FROM ir_module_module WHERE name=%s AND latest_version=%s", ('base', BASE_VERSION)) + if cr.fetchone(): + # Careful to compare timestamps with 'UTC' - everything is UTC as of v6.1. + cr.execute("""SELECT * FROM ir_cron + WHERE numbercall != 0 + AND active AND nextcall <= (now() at time zone 'UTC') + ORDER BY priority""") + jobs = cr.dictfetchall() + else: + _logger.warning('Skipping database %s as its base version is not %s.', db_name, BASE_VERSION) except psycopg2.ProgrammingError, e: if e.pgcode == '42P01': # Class 42 — Syntax Error or Access Rule Violation; 42P01: undefined_table diff --git a/openerp/addons/base/ir/ir_model.py b/openerp/addons/base/ir/ir_model.py index b39162ff80e..57c4607e09c 100644 --- a/openerp/addons/base/ir/ir_model.py +++ b/openerp/addons/base/ir/ir_model.py @@ -863,11 +863,22 @@ class ir_model_data(osv.osv): def get_object_reference(self, cr, uid, module, xml_id): """Returns (model, res_id) corresponding to a given module and xml_id (cached) or raise ValueError if not found""" data_id = self._get_id(cr, uid, module, xml_id) + #assuming data_id is not False, as it was checked upstream res = self.read(cr, uid, data_id, ['model', 'res_id']) if not res['res_id']: raise ValueError('No such external ID currently defined in the system: %s.%s' % (module, xml_id)) return res['model'], res['res_id'] + def check_object_reference(self, cr, uid, module, xml_id): + """Returns (model, res_id) corresponding to a given module and xml_id (cached), if and only if the user has the necessary access rights + to see that object, otherwise raise ValueError""" + model, res_id = self.get_object_reference(cr, uid, module, xml_id) + #search on id found in result to check if current user has read access right + check_right = self.pool.get(model).search(cr, uid, [('id', '=', res_id)]) + if check_right: + return model, res_id + raise ValueError('Not enough access rights on the external ID: %s.%s' % (module, xml_id)) + def get_object(self, cr, uid, module, xml_id, context=None): """Returns a browsable record for the given module name and xml_id or raise ValueError if not found""" res_model, res_id = self.get_object_reference(cr, uid, module, xml_id) diff --git a/openerp/addons/base/ir/ir_sequence.py b/openerp/addons/base/ir/ir_sequence.py index 8ba94d37006..44c9e57af32 100644 --- a/openerp/addons/base/ir/ir_sequence.py +++ b/openerp/addons/base/ir/ir_sequence.py @@ -217,7 +217,7 @@ class ir_sequence(openerp.osv.osv.osv): def next_by_id(self, cr, uid, sequence_id, context=None): """ Draw an interpolated string using the specified sequence.""" self.check_access_rights(cr, uid, 'read') - company_ids = self.pool.get('res.company').search(cr, uid, [], order='company_id', context=context) + [False] + company_ids = self.pool.get('res.company').search(cr, uid, [], context=context) + [False] ids = self.search(cr, uid, ['&',('id','=', sequence_id),('company_id','in',company_ids)]) return self._next(cr, uid, ids, context) @@ -234,8 +234,8 @@ class ir_sequence(openerp.osv.osv.osv): specific company will get higher priority. """ self.check_access_rights(cr, uid, 'read') - company_ids = self.pool.get('res.company').search(cr, uid, [], order='company_id', context=context) + [False] - ids = self.search(cr, uid, ['&',('code','=', sequence_code),('company_id','in',company_ids)]) + company_ids = self.pool.get('res.company').search(cr, uid, [], context=context) + [False] + ids = self.search(cr, uid, ['&', ('code', '=', sequence_code), ('company_id', 'in', company_ids)]) return self._next(cr, uid, ids, context) def get_id(self, cr, uid, sequence_code_or_id, code_or_id='id', context=None): diff --git a/openerp/addons/base/ir/ir_ui_view.py b/openerp/addons/base/ir/ir_ui_view.py index 1d666f2f5c8..5a168010991 100644 --- a/openerp/addons/base/ir/ir_ui_view.py +++ b/openerp/addons/base/ir/ir_ui_view.py @@ -100,7 +100,7 @@ class view(osv.osv): else: inferred_type = etree.fromstring(values['arch'].encode('utf8')).tag values['name'] = "%s %s" % (values['model'], inferred_type) - return super(osv.osv, self).create(cr, uid, values, context) + return super(view, self).create(cr, uid, values, context) def _relaxng(self): if not self._relaxng_validator: diff --git a/openerp/addons/base/module/module.py b/openerp/addons/base/module/module.py index e0c38c969ec..6e79d0e4650 100644 --- a/openerp/addons/base/module/module.py +++ b/openerp/addons/base/module/module.py @@ -472,6 +472,7 @@ class module(osv.osv): function(cr, uid, ids, context=context) cr.commit() + openerp.modules.registry.RegistryManager.signal_registry_change(cr.dbname) registry = openerp.modules.registry.RegistryManager.new(cr.dbname, update_module=True) config = registry['res.config'].next(cr, uid, [], context=context) or {} diff --git a/openerp/addons/base/module/report/ir_module_reference.rml b/openerp/addons/base/module/report/ir_module_reference.rml index 0fd3c492110..240d75fbccf 100644 --- a/openerp/addons/base/module/report/ir_module_reference.rml +++ b/openerp/addons/base/module/report/ir_module_reference.rml @@ -246,7 +246,7 @@ </para> </section> <section> - <para style="P1">[[ repeatIn(findflds(object.model), 'field') ]]</para> + <para style="P1">[[ repeatIn(findflds(object.model, module.name), 'field') ]]</para> <blockTable colWidths="113.0,397.0" repeatRows="1" style="Table2"> <tr> <td> diff --git a/openerp/addons/base/module/report/ir_module_reference_print.py b/openerp/addons/base/module/report/ir_module_reference_print.py index 7f2b8a2a5b4..37aae5db223 100644 --- a/openerp/addons/base/module/report/ir_module_reference_print.py +++ b/openerp/addons/base/module/report/ir_module_reference_print.py @@ -64,17 +64,24 @@ class ir_module_reference_print(report_sxw.rml_parse): return res def _object_find(self, module): - ids2 = self.pool.get('ir.model.data').search(self.cr, self.uid, [('module','=',module), ('model','=','ir.model')]) + ids2 = self.pool['ir.model.data'].search(self.cr, self.uid, [('module','=',module), ('model','=','ir.model')]) ids = [] - for mod in self.pool.get('ir.model.data').browse(self.cr, self.uid, ids2): + for mod in self.pool['ir.model.data'].browse(self.cr, self.uid, ids2): ids.append(mod.res_id) - modobj = self.pool.get('ir.model') + modobj = self.pool['ir.model'] return modobj.browse(self.cr, self.uid, ids) - def _fields_find(self, obj): + def _fields_find(self, obj, module): + res = [] + data_obj = self.pool['ir.model.data'] modobj = self.pool[obj] - res = modobj.fields_get(self.cr, self.uid).items() - res.sort() + fname_wildcard = 'field_' + modobj._name.replace('.', '_') + '_%' + module_fields_ids = data_obj.search(self.cr, self.uid, [('model', '=', 'ir.model.fields'), ('module', '=', module), ('name', 'like', fname_wildcard)]) + if module_fields_ids: + module_fields_res_ids = [x['res_id'] for x in data_obj.read(self.cr, self.uid, module_fields_ids, ['res_id'])] + module_fields_names = [x['name'] for x in self.pool['ir.model.fields'].read(self.cr, self.uid, module_fields_res_ids, ['name'])] + res = modobj.fields_get(self.cr, self.uid, allfields=module_fields_names).items() + res.sort() return res report_sxw.report_sxw('report.ir.module.reference', 'ir.module.module', diff --git a/openerp/addons/base/res/res_bank.py b/openerp/addons/base/res/res_bank.py index fa8516bece2..00d18d08078 100644 --- a/openerp/addons/base/res/res_bank.py +++ b/openerp/addons/base/res/res_bank.py @@ -158,8 +158,8 @@ class res_partner_bank(osv.osv): 'name': '/' } - def fields_get(self, cr, uid, fields=None, context=None): - res = super(res_partner_bank, self).fields_get(cr, uid, fields, context) + def fields_get(self, cr, uid, allfields=None, context=None): + res = super(res_partner_bank, self).fields_get(cr, uid, allfields=allfields, context=context) bank_type_obj = self.pool.get('res.partner.bank.type') type_ids = bank_type_obj.search(cr, uid, []) types = bank_type_obj.browse(cr, uid, type_ids) diff --git a/openerp/addons/base/res/res_company.py b/openerp/addons/base/res/res_company.py index 022c9c06da0..c8ea8d7856e 100644 --- a/openerp/addons/base/res/res_company.py +++ b/openerp/addons/base/res/res_company.py @@ -97,7 +97,7 @@ class res_company(osv.osv): address_data = part_obj.address_get(cr, uid, [company.partner_id.id], adr_pref=['default']) address = address_data['default'] if address: - part_obj.write(cr, uid, [address], {name: value or False}) + part_obj.write(cr, uid, [address], {name: value or False}, context=context) else: part_obj.create(cr, uid, {name: value or False, 'parent_id': company.partner_id.id}, context=context) return True diff --git a/openerp/addons/base/res/res_config.py b/openerp/addons/base/res/res_config.py index 1a5806ad93b..1732acfe9a2 100644 --- a/openerp/addons/base/res/res_config.py +++ b/openerp/addons/base/res/res_config.py @@ -31,6 +31,37 @@ from openerp import exceptions _logger = logging.getLogger(__name__) + +class res_config_module_installation_mixin(object): + def _install_modules(self, cr, uid, modules, context): + """Install the requested modules. + return the next action to execute + + modules is a list of tuples + (mod_name, browse_record | None) + """ + ir_module = self.pool.get('ir.module.module') + to_install_ids = [] + to_install_missing_names = [] + + for name, module in modules: + if not module: + to_install_missing_names.append(name) + elif module.state == 'uninstalled': + to_install_ids.append(module.id) + + if to_install_ids: + ir_module.button_immediate_install(cr, uid, to_install_ids, context=context) + + if to_install_missing_names: + return { + 'type': 'ir.actions.client', + 'tag': 'apps', + 'params': {'modules': to_install_missing_names}, + } + + return None + class res_config_configurable(osv.osv_memory): ''' Base classes for new-style configuration items @@ -155,7 +186,7 @@ class res_config_configurable(osv.osv_memory): res_config_configurable() -class res_config_installer(osv.osv_memory): +class res_config_installer(osv.osv_memory, res_config_module_installation_mixin): """ New-style configuration base specialized for addons selection and installation. @@ -353,17 +384,18 @@ class res_config_installer(osv.osv_memory): return fields def execute(self, cr, uid, ids, context=None): - modules = self.pool['ir.module.module'] to_install = list(self.modules_to_install( cr, uid, ids, context=context)) _logger.info('Selecting addons %s to install', to_install) - modules.state_update( - cr, uid, - modules.search(cr, uid, [('name','in',to_install)]), - 'to install', ['uninstalled'], context=context) - cr.commit() - openerp.modules.registry.RegistryManager.signal_registry_change(cr.dbname) - openerp.modules.registry.RegistryManager.new(cr.dbname, update_module=True) + + ir_module = self.pool.get('ir.module.module') + modules = [] + for name in to_install: + mod_ids = ir_module.search(cr, uid, [('name', '=', name)]) + record = ir_module.browse(cr, uid, mod_ids[0], context) if mod_ids else None + modules.append((name, record)) + + return self._install_modules(cr, uid, modules, context=context) res_config_installer() @@ -404,8 +436,7 @@ class ir_actions_configuration_wizard(osv.osv_memory): ir_actions_configuration_wizard() - -class res_config_settings(osv.osv_memory): +class res_config_settings(osv.osv_memory, res_config_module_installation_mixin): """ Base configuration wizard for application settings. It provides support for setting default values, assigning groups to employee users, and installing modules. To make such a 'settings' wizard, define a model like:: @@ -529,33 +560,22 @@ class res_config_settings(osv.osv_memory): getattr(self, method)(cr, uid, ids, context) # module fields: install/uninstall the selected modules - to_install_missing_names = [] + to_install = [] to_uninstall_ids = [] - to_install_ids = [] lm = len('module_') for name, module in classified['module']: if config[name]: - if not module: - # missing module, will be provided by apps.openerp.com - to_install_missing_names.append(name[lm:]) - elif module.state == 'uninstalled': - # local module, to be installed - to_install_ids.append(module.id) + to_install.append((name[lm:], module)) else: if module and module.state in ('installed', 'to upgrade'): to_uninstall_ids.append(module.id) if to_uninstall_ids: ir_module.button_immediate_uninstall(cr, uid, to_uninstall_ids, context=context) - if to_install_ids: - ir_module.button_immediate_install(cr, uid, to_install_ids, context=context) - if to_install_missing_names: - return { - 'type': 'ir.actions.client', - 'tag': 'apps', - 'params': {'modules': to_install_missing_names}, - } + action = self._install_modules(cr, uid, to_install, context=context) + if action: + return action # After the uninstall/install calls, the self.pool is no longer valid. # So we reach into the RegistryManager directly. diff --git a/openerp/addons/base/res/res_partner.py b/openerp/addons/base/res/res_partner.py index 31e2d44b306..c46fd71ce5f 100644 --- a/openerp/addons/base/res/res_partner.py +++ b/openerp/addons/base/res/res_partner.py @@ -30,6 +30,7 @@ from openerp import SUPERUSER_ID from openerp import tools from openerp.osv import osv, fields from openerp.tools.translate import _ +from openerp.tools.yaml_import import is_comment class format_address(object): def fields_view_get_address(self, cr, uid, arch, context={}): @@ -159,8 +160,8 @@ def _lang_get(self, cr, uid, context=None): res = lang_pool.read(cr, uid, ids, ['code', 'name'], context) return [(r['code'], r['name']) for r in res] -POSTAL_ADDRESS_FIELDS = ('street', 'street2', 'zip', 'city', 'state_id', 'country_id') -ADDRESS_FIELDS = POSTAL_ADDRESS_FIELDS + ('email', 'phone', 'fax', 'mobile', 'website', 'ref', 'lang') +# fields copy if 'use_parent_address' is checked +ADDRESS_FIELDS = ('street', 'street2', 'zip', 'city', 'state_id', 'country_id') class res_partner(osv.osv, format_address): _description = 'Partner' @@ -193,13 +194,38 @@ class res_partner(osv.osv, format_address): result[obj.id] = obj.image != False return result - _order = "name" + def _commercial_partner_compute(self, cr, uid, ids, name, args, context=None): + """ Returns the partner that is considered the commercial + entity of this partner. The commercial entity holds the master data + for all commercial fields (see :py:meth:`~_commercial_fields`) """ + result = dict.fromkeys(ids, False) + for partner in self.browse(cr, uid, ids, context=context): + current_partner = partner + while not current_partner.is_company and current_partner.parent_id: + current_partner = current_partner.parent_id + result[partner.id] = current_partner.id + return result + + def _display_name_compute(self, cr, uid, ids, name, args, context=None): + return dict(self.name_get(cr, uid, ids, context=context)) + + # indirections to avoid passing a copy of the overridable method when declaring the function field + _commercial_partner_id = lambda self, *args, **kwargs: self._commercial_partner_compute(*args, **kwargs) + _display_name = lambda self, *args, **kwargs: self._display_name_compute(*args, **kwargs) + + _commercial_partner_store_triggers = { + 'res.partner': (lambda self,cr,uid,ids,context=None: self.search(cr, uid, [('id','child_of',ids)]), + ['parent_id', 'is_company'], 10) + } + + _order = "display_name" _columns = { 'name': fields.char('Name', size=128, required=True, select=True), + 'display_name': fields.function(_display_name, type='char', string='Name', store=_commercial_partner_store_triggers), 'date': fields.date('Date', select=1), 'title': fields.many2one('res.partner.title', 'Title'), 'parent_id': fields.many2one('res.partner', 'Related Company'), - 'child_ids': fields.one2many('res.partner', 'parent_id', 'Contacts'), + 'child_ids': fields.one2many('res.partner', 'parent_id', 'Contacts', domain=[('active','=',True)]), # force "active_test" domain to bypass _search() override 'ref': fields.char('Reference', size=64, select=1), 'lang': fields.selection(_lang_get, 'Language', help="If the selected language is loaded in the system, all documents related to this contact will be printed in this language. If not, it will be English."), @@ -264,6 +290,9 @@ class res_partner(osv.osv, format_address): 'color': fields.integer('Color Index'), 'user_ids': fields.one2many('res.users', 'partner_id', 'Users'), 'contact_address': fields.function(_address_display, type='char', string='Complete Address'), + + # technical field used for managing commercial fields + 'commercial_partner_id': fields.function(_commercial_partner_id, type='many2one', relation='res.partner', string='Commercial Entity', store=_commercial_partner_store_triggers) } def _default_category(self, cr, uid, context=None): @@ -302,11 +331,15 @@ class res_partner(osv.osv, format_address): 'company_id': lambda self, cr, uid, ctx: self.pool['res.company']._company_default_get(cr, uid, 'res.partner', context=ctx), 'color': 0, 'is_company': False, - 'type': 'default', - 'use_parent_address': True, + 'type': 'contact', # type 'default' is wildcard and thus inappropriate + 'use_parent_address': False, 'image': False, } + _constraints = [ + (osv.osv._check_recursion, 'You cannot create recursive Partner hierarchies.', ['parent_id']), + ] + def copy(self, cr, uid, id, default=None, context=None): if default is None: default = {} @@ -318,7 +351,6 @@ class res_partner(osv.osv, format_address): value = {} value['title'] = False if is_company: - value['parent_id'] = False domain = {'title': [('domain', '=', 'partner')]} else: domain = {'title': [('domain', '=', 'contact')]} @@ -328,11 +360,22 @@ class res_partner(osv.osv, format_address): def value_or_id(val): """ return val or val.id if val is a browse record """ return val if isinstance(val, (bool, int, long, float, basestring)) else val.id - - if use_parent_address and parent_id: + result = {} + if parent_id: + if ids: + partner = self.browse(cr, uid, ids[0], context=context) + if partner.parent_id and partner.parent_id.id != parent_id: + result['warning'] = {'title': _('Warning'), + 'message': _('Changing the company of a contact should only be done if it ' + 'was never correctly set. If an existing contact starts working for a new ' + 'company then a new contact should be created under that new ' + 'company. You can use the "Discard" button to abandon this change.')} parent = self.browse(cr, uid, parent_id, context=context) - return {'value': dict((key, value_or_id(parent[key])) for key in ADDRESS_FIELDS)} - return {} + address_fields = self._address_fields(cr, uid, context=context) + result['value'] = dict((key, value_or_id(parent[key])) for key in address_fields) + else: + result['value'] = {'use_parent_address': False} + return result def onchange_state(self, cr, uid, ids, state_id, context=None): if state_id: @@ -358,50 +401,134 @@ class res_partner(osv.osv, format_address): # _constraints = [(_check_ean_key, 'Error: Invalid ean code', ['ean13'])] - def write(self, cr, uid, ids, vals, context=None): - # Update parent and siblings or children records - if isinstance(ids, (int, long)): - ids = [ids] - for partner in self.browse(cr, uid, ids, context=context): - update_ids = [] - if partner.is_company: - domain_children = [('parent_id', 'child_of', partner.id), ('use_parent_address', '=', True)] - update_ids = self.search(cr, uid, domain_children, context=context) - elif partner.parent_id and vals.get('use_parent_address', partner.use_parent_address): - domain_siblings = [('parent_id', '=', partner.parent_id.id), ('use_parent_address', '=', True)] - update_ids = [partner.parent_id.id] + self.search(cr, uid, domain_siblings, context=context) - self.update_address(cr, uid, update_ids, vals, context) - return super(res_partner,self).write(cr, uid, ids, vals, context=context) - - def create(self, cr, uid, vals, context=None): - if context is None: - context = {} - # Update parent and siblings records - if vals.get('parent_id'): - if 'use_parent_address' in vals: - use_parent_address = vals['use_parent_address'] + def _update_fields_values(self, cr, uid, partner, fields, context=None): + """ Returns dict of write() values for synchronizing ``fields`` """ + values = {} + for field in fields: + column = self._all_columns[field].column + if column._type == 'one2many': + raise AssertionError('One2Many fields cannot be synchronized as part of `commercial_fields` or `address fields`') + if column._type == 'many2one': + values[field] = partner[field].id if partner[field] else False + elif column._type == 'many2many': + values[field] = [(6,0,[r.id for r in partner[field] or []])] else: - use_parent_address = self.default_get(cr, uid, ['use_parent_address'], context=context)['use_parent_address'] - - if use_parent_address: - domain_siblings = [('parent_id', '=', vals['parent_id']), ('use_parent_address', '=', True)] - update_ids = [vals['parent_id']] + self.search(cr, uid, domain_siblings, context=context) - self.update_address(cr, uid, update_ids, vals, context) - - # add missing address keys - onchange_values = self.onchange_address(cr, uid, [], use_parent_address, - vals['parent_id'], context=context).get('value') or {} - vals.update(dict((key, value) - for key, value in onchange_values.iteritems() - if key in ADDRESS_FIELDS and key not in vals)) + values[field] = partner[field] + return values - return super(res_partner, self).create(cr, uid, vals, context=context) + def _address_fields(self, cr, uid, context=None): + """ Returns the list of address fields that are synced from the parent + when the `use_parent_address` flag is set. """ + return list(ADDRESS_FIELDS) def update_address(self, cr, uid, ids, vals, context=None): - addr_vals = dict((key, vals[key]) for key in POSTAL_ADDRESS_FIELDS if key in vals) + address_fields = self._address_fields(cr, uid, context=context) + addr_vals = dict((key, vals[key]) for key in address_fields if key in vals) if addr_vals: return super(res_partner, self).write(cr, uid, ids, addr_vals, context) + def _commercial_fields(self, cr, uid, context=None): + """ Returns the list of fields that are managed by the commercial entity + to which a partner belongs. These fields are meant to be hidden on + partners that aren't `commercial entities` themselves, and will be + delegated to the parent `commercial entity`. The list is meant to be + extended by inheriting classes. """ + return ['vat'] + + def _commercial_sync_from_company(self, cr, uid, partner, context=None): + """ Handle sync of commercial fields when a new parent commercial entity is set, + as if they were related fields """ + if partner.commercial_partner_id != partner: + commercial_fields = self._commercial_fields(cr, uid, context=context) + sync_vals = self._update_fields_values(cr, uid, partner.commercial_partner_id, + commercial_fields, context=context) + return self.write(cr, uid, partner.id, sync_vals, context=context) + + def _commercial_sync_to_children(self, cr, uid, partner, context=None): + """ Handle sync of commercial fields to descendants """ + commercial_fields = self._commercial_fields(cr, uid, context=context) + sync_vals = self._update_fields_values(cr, uid, partner.commercial_partner_id, + commercial_fields, context=context) + sync_children = [c for c in partner.child_ids if not c.is_company] + for child in sync_children: + self._commercial_sync_to_children(cr, uid, child, context=context) + return self.write(cr, uid, [c.id for c in sync_children], sync_vals, context=context) + + def _fields_sync(self, cr, uid, partner, update_values, context=None): + """ Sync commercial fields and address fields from company and to children after create/update, + just as if those were all modeled as fields.related to the parent """ + # 1. From UPSTREAM: sync from parent + if update_values.get('parent_id') or update_values.get('use_company_address'): + # 1a. Commercial fields: sync if parent changed + if update_values.get('parent_id'): + self._commercial_sync_from_company(cr, uid, partner, context=context) + # 1b. Address fields: sync if parent or use_parent changed *and* both are now set + if partner.parent_id and partner.use_parent_address: + onchange_vals = self.onchange_address(cr, uid, [partner.id], + use_parent_address=partner.use_parent_address, + parent_id=partner.parent_id.id, + context=context).get('value', {}) + self.update_address(cr, uid, partner.id, onchange_vals, context=context) + + # 2. To DOWNSTREAM: sync children + 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) + # 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): + domain_children = [('parent_id', '=', partner.id), ('use_parent_address', '=', True)] + update_ids = self.search(cr, uid, domain_children, context=context) + self.update_address(cr, uid, update_ids, update_values, context=context) + + def _handle_first_contact_creation(self, cr, uid, partner, context=None): + """ On creation of first contact for a company (or root) that has no address, assume contact address + was meant to be company address """ + parent = partner.parent_id + address_fields = self._address_fields(cr, uid, context=context) + if parent and (parent.is_company or not parent.parent_id) and len(parent.child_ids) == 1 and \ + any(partner[f] for f in address_fields) and not any(parent[f] for f in address_fields): + addr_vals = self._update_fields_values(cr, uid, partner, address_fields, context=context) + parent.update_address(addr_vals) + if not parent.is_company: + parent.write({'is_company': True}) + + def write(self, cr, uid, ids, vals, context=None): + if isinstance(ids, (int, long)): + ids = [ids] + 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) + return result + + def create(self, cr, uid, vals, context=None): + new_id = super(res_partner, self).create(cr, uid, vals, context=context) + partner = self.browse(cr, uid, new_id, context=context) + self._fields_sync(cr, uid, partner, vals, context) + self._handle_first_contact_creation(cr, uid, partner, context) + return new_id + + def open_commercial_entity(self, cr, uid, ids, context=None): + """ Utility method used to add an "Open Company" button in partner views """ + partner = self.browse(cr, uid, ids[0], context=context) + return {'type': 'ir.actions.act_window', + 'res_model': 'res.partner', + 'view_mode': 'form', + 'res_id': partner.commercial_partner_id.id, + 'target': 'new', + 'flags': {'form': {'action_buttons': True}}} + + def open_parent(self, cr, uid, ids, context=None): + """ Utility method used to add an "Open Parent" button in partner views """ + partner = self.browse(cr, uid, ids[0], context=context) + return {'type': 'ir.actions.act_window', + 'res_model': 'res.partner', + 'view_mode': 'form', + 'res_id': partner.parent_id.id, + 'target': 'new', + 'flags': {'form': {'action_buttons': True}}} + def name_get(self, cr, uid, ids, context=None): if context is None: context = {} @@ -410,8 +537,8 @@ class res_partner(osv.osv, format_address): res = [] for record in self.browse(cr, uid, ids, context=context): name = record.name - if record.parent_id: - name = "%s (%s)" % (name, record.parent_id.name) + if record.parent_id and not record.is_company: + name = "%s, %s" % (record.parent_id.name, name) if context.get('show_address'): name = name + "\n" + self._display_address(cr, uid, record, without_company=True, context=context) name = name.replace('\n\n','\n') @@ -450,6 +577,15 @@ class res_partner(osv.osv, format_address): rec_id = self.create(cr, uid, {self._rec_name: name or email, 'email': email or False}, context=context) return self.name_get(cr, uid, [rec_id], context)[0] + def _search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False, access_rights_uid=None): + """ Override search() to always show inactive children when searching via ``child_of`` operator. The ORM will + always call search() with a simple domain of the form [('parent_id', 'in', [ids])]. """ + # a special ``domain`` is set on the ``child_ids`` o2m to bypass this logic, as it uses similar domain expressions + if len(args) == 1 and len(args[0]) == 3 and args[0][:2] == ('parent_id','in'): + context = dict(context or {}, active_test=False) + return super(res_partner, self)._search(cr, user, args, offset=offset, limit=limit, order=order, context=context, + count=count, access_rights_uid=access_rights_uid) + def name_search(self, cr, uid, name, args=None, operator='ilike', context=None, limit=100): if not args: args = [] @@ -510,25 +646,42 @@ class res_partner(osv.osv, format_address): ids = ids[16:] return True - def address_get(self, cr, uid, ids, adr_pref=None): - if adr_pref is None: - adr_pref = ['default'] + def address_get(self, cr, uid, ids, adr_pref=None, context=None): + """ Find contacts/addresses of the right type(s) by doing a depth-first-search + through descendants within company boundaries (stop at entities flagged ``is_company``) + then continuing the search at the ancestors that are within the same company boundaries. + Defaults to partners of type ``'default'`` when the exact type is not found, or to the + provided partner itself if no type ``'default'`` is found either. """ + adr_pref = set(adr_pref or []) + if 'default' not in adr_pref: + adr_pref.add('default') result = {} - # retrieve addresses from the partner itself and its children - res = [] - # need to fix the ids ,It get False value in list like ids[False] - if ids and ids[0]!=False: - for p in self.browse(cr, uid, ids): - res.append((p.type, p.id)) - res.extend((c.type, c.id) for c in p.child_ids) - address_dict = dict(reversed(res)) - # get the id of the (first) default address if there is one, - # otherwise get the id of the first address in the list - default_address = False - if res: - default_address = address_dict.get('default', res[0][1]) - for adr in adr_pref: - result[adr] = address_dict.get(adr, default_address) + visited = set() + for partner in self.browse(cr, uid, filter(None, ids), context=context): + current_partner = partner + while current_partner: + to_scan = [current_partner] + # Scan descendants, DFS + while to_scan: + record = to_scan.pop(0) + visited.add(record) + if record.type in adr_pref and not result.get(record.type): + result[record.type] = record.id + if len(result) == len(adr_pref): + return result + to_scan = [c for c in record.child_ids + if c not in visited + if not c.is_company] + to_scan + + # Continue scanning at ancestor if current_partner is not a commercial entity + if current_partner.is_company or not current_partner.parent_id: + break + current_partner = current_partner.parent_id + + # default to type 'default' or the partner itself + default = result.get('default', partner.id) + for adr_type in adr_pref: + result[adr_type] = result.get(adr_type) or default return result def view_header_get(self, cr, uid, view_id, view_type, context): @@ -570,8 +723,7 @@ class res_partner(osv.osv, format_address): 'country_name': address.country_id and address.country_id.name or '', 'company_name': address.parent_id and address.parent_id.name or '', } - address_field = ['title', 'street', 'street2', 'zip', 'city'] - for field in address_field : + for field in self._address_fields(cr, uid, context=context): args[field] = getattr(address, field) or '' if without_company: args['company_name'] = '' diff --git a/openerp/addons/base/res/res_partner_view.xml b/openerp/addons/base/res/res_partner_view.xml index 88a1266b689..5b9d21045aa 100644 --- a/openerp/addons/base/res/res_partner_view.xml +++ b/openerp/addons/base/res/res_partner_view.xml @@ -77,7 +77,7 @@ <field eval="8" name="priority"/> <field name="arch" type="xml"> <tree string="Contacts"> - <field name="name"/> + <field name="display_name"/> <field name="function" invisible="1"/> <field name="phone"/> <field name="email"/> @@ -138,8 +138,8 @@ </h1> <field name="parent_id" placeholder="Company" - domain="[('is_company', '=', True)]" context="{'default_is_company': True}" - attrs="{'invisible': [('is_company','=', True)]}" + domain="[('is_company', '=', True)]" context="{'default_is_company': True, 'default_supplier': supplier}" + attrs="{'invisible': [('is_company','=', True),('parent_id', '=', False)]}" on_change="onchange_address(use_parent_address, parent_id)"/> <field name="category_id" widget="many2many_tags" placeholder="Tags..."/> </div> @@ -151,21 +151,24 @@ <div attrs="{'invisible': [('parent_id','=', False)]}" name="div_type"> <field class="oe_inline" name="type"/> - <label for="use_parent_address" class="oe_edit_only"/> - <field name="use_parent_address" class="oe_edit_only oe_inline" - on_change="onchange_address(use_parent_address, parent_id)"/> </div> <label for="street" string="Address"/> <div> - <field name="street" placeholder="Street..."/> - <field name="street2"/> + <field name="use_parent_address" class="oe_edit_only oe_inline" + on_change="onchange_address(use_parent_address, parent_id)" + attrs="{'invisible': [('parent_id','=', False)]}"/> + <label for="use_parent_address" class="oe_edit_only" attrs="{'invisible': [('parent_id','=', False)]}"/> + <button name="open_parent" type="object" string="(edit company address)" class="oe_link oe_edit_only" + attrs="{'invisible': ['|',('parent_id','=', False),('use_parent_address','=',False)]}"/> + <field name="street" placeholder="Street..." attrs="{'readonly': [('use_parent_address','=',True)]}"/> + <field name="street2" attrs="{'readonly': [('use_parent_address','=',True)]}"/> <div class="address_format"> - <field name="city" placeholder="City" style="width: 40%%"/> - <field name="state_id" class="oe_no_button" placeholder="State" style="width: 37%%" options='{"no_open": True}' on_change="onchange_state(state_id)"/> - <field name="zip" placeholder="ZIP" style="width: 20%%"/> + <field name="city" placeholder="City" style="width: 40%%" attrs="{'readonly': [('use_parent_address','=',True)]}"/> + <field name="state_id" class="oe_no_button" placeholder="State" style="width: 37%%" options='{"no_open": True}' on_change="onchange_state(state_id)" attrs="{'readonly': [('use_parent_address','=',True)]}"/> + <field name="zip" placeholder="ZIP" style="width: 20%%" attrs="{'readonly': [('use_parent_address','=',True)]}"/> </div> - <field name="country_id" placeholder="Country" class="oe_no_button" options='{"no_open": True}'/> + <field name="country_id" placeholder="Country" class="oe_no_button" options='{"no_open": True}' attrs="{'readonly': [('use_parent_address','=',True)]}"/> </div> <field name="website" widget="url" placeholder="e.g. www.openerp.com"/> </group> @@ -182,8 +185,8 @@ </group> <notebook colspan="4"> - <page string="Contacts" attrs="{'invisible': [('is_company','=',False)]}"> - <field name="child_ids" context="{'default_parent_id': active_id}" mode="kanban"> + <page string="Contacts" attrs="{'invisible': [('is_company','=',False), ('child_ids', '=', [])]}" autofocus="autofocus"> + <field name="child_ids" mode="kanban" context="{'default_parent_id': active_id, 'default_street': street, 'default_street2': street2, 'default_city': city, 'default_state_id': state_id, 'default_zip': zip, 'default_country_id': country_id, 'default_supplier': supplier}"> <kanban> <field name="color"/> <field name="name"/> @@ -249,17 +252,41 @@ </templates> </kanban> <form string="Contact" version="7.0"> - <field name="image" widget='image' class="oe_avatar oe_left" options='{"preview_image": "image_medium"}'/> - <div class="oe_title"> + <sheet> + <field name="image" widget='image' class="oe_avatar oe_left" options='{"preview_image": "image_medium"}'/> + <div class="oe_title"> + <label for="name" class="oe_edit_only"/> + <h1><field name="name" style="width: 70%%"/></h1> + <field name="category_id" widget="many2many_tags" placeholder="Tags..." style="width: 70%%"/> + </div> <group> - <field name="name"/> - <field name="category_id" widget="many2many_tags" placeholder="Tags..."/> <field name="function" placeholder="e.g. Sales Director"/> <field name="email"/> <field name="phone"/> <field name="mobile"/> </group> - </div> + <div> + <field name="use_parent_address"/><label for="use_parent_address"/> + </div> + <group> + <label for="type"/> + <div name="div_type"> + <field class="oe_inline" name="type"/> + </div> + <label for="street" string="Address" attrs="{'invisible': [('use_parent_address','=', True)]}"/> + <div attrs="{'invisible': [('use_parent_address','=', True)]}" name="div_address"> + <field name="street" placeholder="Street..."/> + <field name="street2"/> + <div class="address_format"> + <field name="city" placeholder="City" style="width: 40%%"/> + <field name="state_id" class="oe_no_button" placeholder="State" style="width: 37%%" options='{"no_open": True}' on_change="onchange_state(state_id)"/> + <field name="zip" placeholder="ZIP" style="width: 20%%"/> + </div> + <field name="country_id" placeholder="Country" class="oe_no_button" options='{"no_open": True}'/> + </div> + </group> + <field name="supplier" invisible="True"/> + </sheet> </form> </field> </page> @@ -330,7 +357,7 @@ <field name="arch" type="xml"> <kanban> <field name="color"/> - <field name="name"/> + <field name="display_name"/> <field name="title"/> <field name="email"/> <field name="parent_id"/> @@ -363,7 +390,7 @@ </t> </a> <div class="oe_kanban_details"> - <h4 class="oe_partner_heading"><a type="open"><field name="name"/></a></h4> + <h4 class="oe_partner_heading"><a type="open"><field name="display_name"/></a></h4> <div class="oe_kanban_partner_categories"/> <div class="oe_kanban_partner_links"/> <ul> diff --git a/openerp/addons/base/static/src/js/apps.js b/openerp/addons/base/static/src/js/apps.js index 4ac01006256..eaaca6b13c3 100644 --- a/openerp/addons/base/static/src/js/apps.js +++ b/openerp/addons/base/static/src/js/apps.js @@ -44,7 +44,7 @@ openerp.base = function(instance) { } else { sessionStorage.removeItem('apps.login'); sessionStorage.removeItem('apps.access_token'); - client.bind_crendentials(client.dbname, 'anonymous', 'anonymous'); + client.bind_credentials(client.dbname, 'anonymous', 'anonymous'); client.authenticate().then( function() { /* done */ d.resolve(client); diff --git a/openerp/addons/base/tests/test_base.py b/openerp/addons/base/tests/test_base.py index 4e1de54920e..ec192f2667e 100644 --- a/openerp/addons/base/tests/test_base.py +++ b/openerp/addons/base/tests/test_base.py @@ -1,6 +1,7 @@ import unittest2 import openerp.tests.common as common +from openerp.osv.orm import except_orm class test_base(common.TransactionCase): @@ -39,5 +40,251 @@ class test_base(common.TransactionCase): self.assertTrue(new_id2 > new_id, 'find_or_create failed - should have created new one again') + def test_20_res_partner_address_sync(self): + cr, uid = self.cr, self.uid + ghoststep = self.res_partner.browse(cr, uid, self.res_partner.create(cr, uid, + {'name': 'GhostStep', + 'is_company': True, + 'street': 'Main Street, 10', + 'phone': '123456789', + 'email': 'info@ghoststep.com', + 'vat': 'BE0477472701', + 'type': 'default'})) + p1 = self.res_partner.browse(cr, uid, self.res_partner.name_create(cr, uid, 'Denis Bladesmith <denis.bladesmith@ghoststep.com>')[0]) + self.assertEqual(p1.type, 'contact', 'Default type must be "contact"') + p1phone = '123456789#34' + p1.write({'phone': p1phone, + 'parent_id': ghoststep.id, + 'use_parent_address': True}) + p1.refresh() + self.assertEqual(p1.street, ghoststep.street, 'Address fields must be synced') + self.assertEqual(p1.phone, p1phone, 'Phone should be preserved after address sync') + self.assertEqual(p1.type, 'contact', 'Type should be preserved after address sync') + self.assertEqual(p1.email, 'denis.bladesmith@ghoststep.com', 'Email should be preserved after sync') + ghoststreet = 'South Street, 25' + ghoststep.write({'street': ghoststreet}) + p1.refresh() + self.assertEqual(p1.street, ghoststreet, 'Address fields must be synced automatically') + self.assertEqual(p1.phone, p1phone, 'Phone should not be synced') + self.assertEqual(p1.email, 'denis.bladesmith@ghoststep.com', 'Email should be preserved after sync') + + p1street = 'My Street, 11' + p1.write({'street': p1street}) + ghoststep.refresh() + self.assertEqual(ghoststep.street, ghoststreet, 'Touching contact should never alter parent') + + + def test_30_res_partner_first_contact_sync(self): + """ Test initial creation of company/contact pair where contact address gets copied to + company """ + cr, uid = self.cr, self.uid + ironshield = self.res_partner.browse(cr, uid, self.res_partner.name_create(cr, uid, 'IronShield')[0]) + self.assertFalse(ironshield.is_company, 'Partners are not companies by default') + self.assertFalse(ironshield.use_parent_address, 'use_parent_address defaults to False') + self.assertEqual(ironshield.type, 'contact', 'Default type must be "contact"') + ironshield.write({'type': 'default'}) # force default type to double-check sync + p1 = self.res_partner.browse(cr, uid, self.res_partner.create(cr, uid, + {'name': 'Isen Hardearth', + 'street': 'Strongarm Avenue, 12', + 'parent_id': ironshield.id})) + self.assertEquals(p1.type, 'contact', 'Default type must be "contact", not the copied parent type') + ironshield.refresh() + self.assertEqual(ironshield.street, p1.street, 'Address fields should be copied to company') + self.assertTrue(ironshield.is_company, 'Company flag should be turned on after first contact creation') + + def test_40_res_partner_address_getc(self): + """ Test address_get address resolution mechanism: it should first go down through descendants, + stopping when encountering another is_copmany entity, then go up, stopping again at the first + is_company entity or the root ancestor and if nothing matches, it should use the provided partner + itself """ + cr, uid = self.cr, self.uid + elmtree = self.res_partner.browse(cr, uid, self.res_partner.name_create(cr, uid, 'Elmtree')[0]) + branch1 = self.res_partner.browse(cr, uid, self.res_partner.create(cr, uid, {'name': 'Branch 1', + 'parent_id': elmtree.id, + 'is_company': True})) + leaf10 = self.res_partner.browse(cr, uid, self.res_partner.create(cr, uid, {'name': 'Leaf 10', + 'parent_id': branch1.id, + 'type': 'invoice'})) + branch11 = self.res_partner.browse(cr, uid, self.res_partner.create(cr, uid, {'name': 'Branch 11', + 'parent_id': branch1.id, + 'type': 'other'})) + leaf111 = self.res_partner.browse(cr, uid, self.res_partner.create(cr, uid, {'name': 'Leaf 111', + 'parent_id': branch11.id, + 'type': 'delivery'})) + branch11.write({'is_company': False}) # force is_company after creating 1rst child + branch2 = self.res_partner.browse(cr, uid, self.res_partner.create(cr, uid, {'name': 'Branch 2', + 'parent_id': elmtree.id, + 'is_company': True})) + leaf21 = self.res_partner.browse(cr, uid, self.res_partner.create(cr, uid, {'name': 'Leaf 21', + 'parent_id': branch2.id, + 'type': 'delivery'})) + leaf22 = self.res_partner.browse(cr, uid, self.res_partner.create(cr, uid, {'name': 'Leaf 22', + 'parent_id': branch2.id})) + leaf23 = self.res_partner.browse(cr, uid, self.res_partner.create(cr, uid, {'name': 'Leaf 23', + 'parent_id': branch2.id, + 'type': 'default'})) + # go up, stop at branch1 + self.assertEqual(self.res_partner.address_get(cr, uid, [leaf111.id], ['delivery', 'invoice', 'contact', 'other', 'default']), + {'delivery': leaf111.id, + 'invoice': leaf10.id, + 'contact': branch1.id, + 'other': branch11.id, + 'default': leaf111.id}, 'Invalid address resolution') + self.assertEqual(self.res_partner.address_get(cr, uid, [branch11.id], ['delivery', 'invoice', 'contact', 'other', 'default']), + {'delivery': leaf111.id, + 'invoice': leaf10.id, + 'contact': branch1.id, + 'other': branch11.id, + 'default': branch11.id}, 'Invalid address resolution') + + # go down, stop at at all child companies + self.assertEqual(self.res_partner.address_get(cr, uid, [elmtree.id], ['delivery', 'invoice', 'contact', 'other', 'default']), + {'delivery': elmtree.id, + 'invoice': elmtree.id, + 'contact': elmtree.id, + 'other': elmtree.id, + 'default': elmtree.id}, 'Invalid address resolution') + + # go down through children + self.assertEqual(self.res_partner.address_get(cr, uid, [branch1.id], ['delivery', 'invoice', 'contact', 'other', 'default']), + {'delivery': leaf111.id, + 'invoice': leaf10.id, + 'contact': branch1.id, + 'other': branch11.id, + 'default': branch1.id}, 'Invalid address resolution') + self.assertEqual(self.res_partner.address_get(cr, uid, [branch2.id], ['delivery', 'invoice', 'contact', 'other', 'default']), + {'delivery': leaf21.id, + 'invoice': leaf23.id, + 'contact': branch2.id, + 'other': leaf23.id, + 'default': leaf23.id}, 'Invalid address resolution') + + # go up then down through siblings + self.assertEqual(self.res_partner.address_get(cr, uid, [leaf21.id], ['delivery', 'invoice', 'contact', 'other', 'default']), + {'delivery': leaf21.id, + 'invoice': leaf23.id, + 'contact': branch2.id, + 'other': leaf23.id, + 'default': leaf23.id + }, 'Invalid address resolution, should scan commercial entity ancestor and its descendants') + self.assertEqual(self.res_partner.address_get(cr, uid, [leaf22.id], ['delivery', 'invoice', 'contact', 'other', 'default']), + {'delivery': leaf21.id, + 'invoice': leaf23.id, + 'contact': leaf22.id, + 'other': leaf23.id, + 'default': leaf23.id}, 'Invalid address resolution, should scan commercial entity ancestor and its descendants') + self.assertEqual(self.res_partner.address_get(cr, uid, [leaf23.id], ['delivery', 'invoice', 'contact', 'other', 'default']), + {'delivery': leaf21.id, + 'invoice': leaf23.id, + 'contact': branch2.id, + 'other': leaf23.id, + 'default': leaf23.id}, 'Invalid address resolution, `default` should only override if no partner with specific type exists') + + # empty adr_pref means only 'default' + self.assertEqual(self.res_partner.address_get(cr, uid, [elmtree.id], []), + {'default': elmtree.id}, 'Invalid address resolution, no default means commercial entity ancestor') + self.assertEqual(self.res_partner.address_get(cr, uid, [leaf111.id], []), + {'default': leaf111.id}, 'Invalid address resolution, no default means contact itself') + branch11.write({'type': 'default'}) + self.assertEqual(self.res_partner.address_get(cr, uid, [leaf111.id], []), + {'default': branch11.id}, 'Invalid address resolution, branch11 should now be default') + + + def test_50_res_partner_commercial_sync(self): + cr, uid = self.cr, self.uid + p0 = self.res_partner.browse(cr, uid, self.res_partner.create(cr, uid, + {'name': 'Sigurd Sunknife', + 'email': 'ssunknife@gmail.com'})) + sunhelm = self.res_partner.browse(cr, uid, self.res_partner.create(cr, uid, + {'name': 'Sunhelm', + 'is_company': True, + 'street': 'Rainbow Street, 13', + 'phone': '1122334455', + 'email': 'info@sunhelm.com', + 'vat': 'BE0477472701', + 'child_ids': [(4, p0.id), + (0, 0, {'name': 'Alrik Greenthorn', + 'email': 'agr@sunhelm.com'})], + })) + p1 = self.res_partner.browse(cr, uid, self.res_partner.create(cr, uid, + {'name': 'Otto Blackwood', + 'email': 'otto.blackwood@sunhelm.com', + 'parent_id': sunhelm.id})) + p11 = self.res_partner.browse(cr, uid, self.res_partner.create(cr, uid, + {'name': 'Gini Graywool', + 'email': 'ggr@sunhelm.com', + 'parent_id': p1.id})) + p2 = self.res_partner.browse(cr, uid, self.res_partner.search(cr, uid, + [('email', '=', 'agr@sunhelm.com')])[0]) + + for p in (p0, p1, p11, p2): + p.refresh() + self.assertEquals(p.commercial_partner_id, sunhelm, 'Incorrect commercial entity resolution') + self.assertEquals(p.vat, sunhelm.vat, 'Commercial fields must be automatically synced') + sunhelmvat = 'BE0123456789' + sunhelm.write({'vat': sunhelmvat}) + for p in (p0, p1, p11, p2): + p.refresh() + self.assertEquals(p.vat, sunhelmvat, 'Commercial fields must be automatically and recursively synced') + + p1vat = 'BE0987654321' + p1.write({'vat': p1vat}) + for p in (sunhelm, p0, p11, p2): + p.refresh() + self.assertEquals(p.vat, sunhelmvat, 'Sync to children should only work downstream and on commercial entities') + + # promote p1 to commercial entity + vals = p1.onchange_type(is_company=True)['value'] + p1.write(dict(vals, parent_id=sunhelm.id, + is_company=True, + name='Sunhelm Subsidiary')) + p1.refresh() + self.assertEquals(p1.vat, p1vat, 'Setting is_company should stop auto-sync of commercial fields') + self.assertEquals(p1.commercial_partner_id, p1, 'Incorrect commercial entity resolution after setting is_company') + + # writing on parent should not touch child commercial entities + sunhelmvat2 = 'BE0112233445' + sunhelm.write({'vat': sunhelmvat2}) + p1.refresh() + self.assertEquals(p1.vat, p1vat, 'Setting is_company should stop auto-sync of commercial fields') + p0.refresh() + self.assertEquals(p0.vat, sunhelmvat2, 'Commercial fields must be automatically synced') + +class test_partner_recursion(common.TransactionCase): + + def setUp(self): + super(test_partner_recursion,self).setUp() + self.res_partner = self.registry('res.partner') + cr, uid = self.cr, self.uid + self.p1 = self.res_partner.name_create(cr, uid, 'Elmtree')[0] + self.p2 = self.res_partner.create(cr, uid, {'name': 'Elmtree Child 1', 'parent_id': self.p1}) + self.p3 = self.res_partner.create(cr, uid, {'name': 'Elmtree Grand-Child 1.1', 'parent_id': self.p2}) + + # split 101, 102, 103 tests to force SQL rollback between them + + def test_101_res_partner_recursion(self): + cr, uid, p1, p3 = self.cr, self.uid, self.p1, self.p3 + self.assertRaises(except_orm, self.res_partner.write, cr, uid, [p1], {'parent_id': p3}) + + def test_102_res_partner_recursion(self): + cr, uid, p2, p3 = self.cr, self.uid, self.p2, self.p3 + self.assertRaises(except_orm, self.res_partner.write, cr, uid, [p2], {'parent_id': p3}) + + def test_103_res_partner_recursion(self): + cr, uid, p3 = self.cr, self.uid, self.p3 + self.assertRaises(except_orm, self.res_partner.write, cr, uid, [p3], {'parent_id': p3}) + + def test_104_res_partner_recursion_indirect_cycle(self): + """ Indirect hacky write to create cycle in children """ + cr, uid, p2, p3 = self.cr, self.uid, self.p2, self.p3 + p3b = self.res_partner.create(cr, uid, {'name': 'Elmtree Grand-Child 1.2', 'parent_id': self.p2}) + self.assertRaises(except_orm, self.res_partner.write, cr, uid, [p2], + {'child_ids': [(1, p3, {'parent_id': p3b}), (1, p3b, {'parent_id': p3})]}) + + def test_110_res_partner_recursion_multi_update(self): + """ multi-write on several partners in same hierarchy must not trigger a false cycle detection """ + cr, uid, p1, p2, p3 = self.cr, self.uid, self.p1, self.p2, self.p3 + self.assertTrue(self.res_partner.write(cr, uid, [p1,p2,p3], {'phone': '123456'})) + if __name__ == '__main__': unittest2.main() diff --git a/openerp/addons/base/tests/test_expression.py b/openerp/addons/base/tests/test_expression.py index 49bc7d57ca1..4f533a5e170 100644 --- a/openerp/addons/base/tests/test_expression.py +++ b/openerp/addons/base/tests/test_expression.py @@ -263,7 +263,7 @@ class test_expression(common.TransactionCase): "_auto_join on: ('child_ids.bank_ids.id', 'in', [..]) query incorrect join condition") self.assertIn('"res_partner__child_ids"."id"="res_partner__child_ids__bank_ids"."partner_id"', sql_query[1], "_auto_join on: ('child_ids.bank_ids.id', 'in', [..]) query incorrect join condition") - self.assertEqual(set([b_aa, b_ba]), set(sql_query[2]), + self.assertEqual(set([b_aa, b_ba]), set(sql_query[2][-2:]), "_auto_join on: ('child_ids.bank_ids.id', 'in', [..]) query incorrect parameter") # -------------------------------------------------- diff --git a/openerp/osv/expression.py b/openerp/osv/expression.py index 530eb83a2cc..55e286d33f2 100644 --- a/openerp/osv/expression.py +++ b/openerp/osv/expression.py @@ -790,7 +790,7 @@ class expression(object): leaf.add_join_context(next_model, working_model._inherits[next_model._name], 'id', working_model._inherits[next_model._name]) push(leaf) - elif not field and left == 'id' and operator == 'child_of': + elif left == 'id' and operator == 'child_of': ids2 = to_ids(right, working_model, context) dom = child_of_domain(left, ids2, working_model) for dom_leaf in reversed(dom): diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index a451fbf6582..31b3bf5c807 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -4817,8 +4817,8 @@ class BaseModel(object): order_field = order_split[0].strip() order_direction = order_split[1].strip() if len(order_split) == 2 else '' inner_clause = None - if order_field == 'id': - order_by_elements.append('"%s"."id" %s' % (self._table, order_direction)) + if order_field == 'id' or (self._log_access and order_field in LOG_ACCESS_COLUMNS.keys()): + order_by_elements.append('"%s"."%s" %s' % (self._table, order_field, order_direction)) elif order_field in self._columns: order_column = self._columns[order_field] if order_column._classic_read: @@ -4836,6 +4836,8 @@ class BaseModel(object): inner_clause = self._generate_m2o_order_by(order_field, query) else: continue # ignore non-readable or "non-joinable" fields + else: + raise ValueError( _("Sorting field %s not found on model %s") %( order_field, self._name)) if inner_clause: if isinstance(inner_clause, list): for clause in inner_clause: @@ -5092,20 +5094,18 @@ class BaseModel(object): :param parent: optional parent field name (default: ``self._parent_name = parent_id``) :return: **True** if the operation can proceed safely, or **False** if an infinite loop is detected. """ - if not parent: parent = self._parent_name - ids_parent = ids[:] - query = 'SELECT distinct "%s" FROM "%s" WHERE id IN %%s' % (parent, self._table) - while ids_parent: - ids_parent2 = [] - for i in range(0, len(ids), cr.IN_MAX): - sub_ids_parent = ids_parent[i:i+cr.IN_MAX] - cr.execute(query, (tuple(sub_ids_parent),)) - ids_parent2.extend(filter(None, map(lambda x: x[0], cr.fetchall()))) - ids_parent = ids_parent2 - for i in ids_parent: - if i in ids: + + # must ignore 'active' flag, ir.rules, etc. => direct SQL query + query = 'SELECT "%s" FROM "%s" WHERE id = %%s' % (parent, self._table) + for id in ids: + current_id = id + while current_id is not None: + cr.execute(query, (current_id,)) + result = cr.fetchone() + current_id = result[0] if result else None + if current_id == id: return False return True diff --git a/openerp/report/render/rml2html/__init__.py b/openerp/report/render/rml2html/__init__.py index 8bb6fbb507b..5eb86ad0329 100644 --- a/openerp/report/render/rml2html/__init__.py +++ b/openerp/report/render/rml2html/__init__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- ############################################################################## -# +# # OpenERP, Open Source Management Solution # Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>). # @@ -15,7 +15,7 @@ # 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 <http://www.gnu.org/licenses/>. +# along with this program. If not, see <http://www.gnu.org/licenses/>. # ############################################################################## diff --git a/openerp/report/render/rml2html/rml2html.py b/openerp/report/render/rml2html/rml2html.py index 0e82a5d3ea8..c80698150b8 100644 --- a/openerp/report/render/rml2html/rml2html.py +++ b/openerp/report/render/rml2html/rml2html.py @@ -1,25 +1,8 @@ # -*- coding: utf-8 -*- ############################################################################## -# -# OpenERP, Open Source Management Solution -# Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>). # -# 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 <http://www.gnu.org/licenses/>. -# -############################################################################## - # Copyright (C) 2005, Fabien Pinckaers, UCL, FSA +# Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>). # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -34,6 +17,8 @@ # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +############################################################################## import sys import cStringIO diff --git a/openerp/report/render/rml2html/utils.py b/openerp/report/render/rml2html/utils.py index b811cc5d47b..c40118477f8 100644 --- a/openerp/report/render/rml2html/utils.py +++ b/openerp/report/render/rml2html/utils.py @@ -1,21 +1,22 @@ # -*- coding: utf-8 -*- ############################################################################## -# -# OpenERP, Open Source Management Solution -# Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>). # -# 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. +# Copyright (C) 2005, Fabien Pinckaers, UCL, FSA +# Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>). # -# 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. +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. # -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################## diff --git a/openerp/report/render/rml2pdf/trml2pdf.py b/openerp/report/render/rml2pdf/trml2pdf.py index 7973ac56ec8..e8d6375c98e 100644 --- a/openerp/report/render/rml2pdf/trml2pdf.py +++ b/openerp/report/render/rml2pdf/trml2pdf.py @@ -435,7 +435,7 @@ class _rml_canvas(object): self.canvas.circle(x_cen=utils.unit_get(node.get('x')), y_cen=utils.unit_get(node.get('y')), r=utils.unit_get(node.get('radius')), **utils.attr_get(node, [], {'fill':'bool','stroke':'bool'})) def _place(self, node): - flows = _rml_flowable(self.doc, self.localcontext, images=self.images, path=self.path, title=self.title).render(node) + flows = _rml_flowable(self.doc, self.localcontext, images=self.images, path=self.path, title=self.title, canvas=self.canvas).render(node) infos = utils.attr_get(node, ['x','y','width','height']) infos['y']+=infos['height'] @@ -616,7 +616,7 @@ class _rml_Illustration(platypus.flowables.Flowable): drw.render(self.canv, None) class _rml_flowable(object): - def __init__(self, doc, localcontext, images=None, path='.', title=None): + def __init__(self, doc, localcontext, images=None, path='.', title=None, canvas=None): if images is None: images = {} self.localcontext = localcontext @@ -625,6 +625,7 @@ class _rml_flowable(object): self.images = images self.path = path self.title = title + self.canvas = canvas def _textual(self, node): rc1 = utils._process_text(self, node.text or '') @@ -634,7 +635,10 @@ class _rml_flowable(object): if key in ('rml_except', 'rml_loop', 'rml_tag'): del txt_n.attrib[key] if not n.tag == 'bullet': - txt_n.text = utils.xml2str(self._textual(n)) + if n.tag == 'pageNumber': + txt_n.text = self.canvas and str(self.canvas.getPageNumber()) or '' + else: + txt_n.text = utils.xml2str(self._textual(n)) txt_n.tail = n.tail and utils.xml2str(utils._process_text(self, n.tail.replace('\n',''))) or '' rc1 += etree.tostring(txt_n) return rc1 @@ -983,7 +987,7 @@ class _rml_template(object): if self.localcontext and not self.localcontext.get('internal_header',False): del self.localcontext['internal_header'] fis = [] - r = _rml_flowable(self.doc,self.localcontext, images=self.images, path=self.path, title=self.title) + r = _rml_flowable(self.doc,self.localcontext, images=self.images, path=self.path, title=self.title, canvas=None) story_cnt = 0 for node_story in node_stories: if story_cnt > 0: diff --git a/openerp/service/workers.py b/openerp/service/workers.py index 68433472aaf..ba831988a36 100644 --- a/openerp/service/workers.py +++ b/openerp/service/workers.py @@ -241,6 +241,9 @@ class Worker(object): self.request_max = multi.limit_request self.request_count = 0 + def setproctitle(self, title=""): + setproctitle('openerp: %s %s %s' % (self.__class__.__name__, self.pid, title)) + def close(self): os.close(self.watchdog_pipe[0]) os.close(self.watchdog_pipe[1]) @@ -290,7 +293,7 @@ class Worker(object): def start(self): self.pid = os.getpid() - setproctitle('openerp: %s %s' % (self.__class__.__name__, self.pid)) + self.setproctitle() _logger.info("Worker %s (%s) alive", self.__class__.__name__, self.pid) # Reseed the random number generator random.seed() @@ -413,6 +416,7 @@ class WorkerCron(Worker): if len(db_names): self.db_index = (self.db_index + 1) % len(db_names) db_name = db_names[self.db_index] + self.setproctitle(db_name) if rpc_request_flag: start_time = time.time() start_rss, start_vms = psutil.Process(os.getpid()).get_memory_info() @@ -422,6 +426,7 @@ class WorkerCron(Worker): import openerp.addons.base as base acquired = base.ir.ir_cron.ir_cron._acquire_job(db_name) if not acquired: + openerp.modules.registry.RegistryManager.delete(db_name) break # dont keep cursors in multi database mode if len(db_names) > 1: |
