From e033ca626d64029eae7be9603f98c7668b031d58 Mon Sep 17 00:00:00 2001 From: Holger Brunn Date: Mon, 15 Aug 2016 14:53:04 +0200 Subject: [PATCH 1/3] [RFR] v8 api, guidelines, tests --- partner_relations/__init__.py | 23 +- partner_relations/__openerp__.py | 13 +- partner_relations/data/demo.xml | 1 + partner_relations/model/__init__.py | 26 -- partner_relations/model/res_partner.py | 339 ------------------ .../res_partner_relation_type_selection.py | 175 --------- partner_relations/models/__init__.py | 8 + partner_relations/models/res_partner.py | 213 +++++++++++ .../{model => models}/res_partner_relation.py | 286 +++++++-------- .../res_partner_relation_all.py | 71 ++-- .../res_partner_relation_type.py | 43 +-- .../res_partner_relation_type_selection.py | 149 ++++++++ partner_relations/test/test_allow.yml | 22 -- .../tests/test_partner_relations.py | 178 +++++++-- partner_relations/{view => views}/menu.xml | 0 .../{view => views}/res_partner.xml | 0 .../{view => views}/res_partner_relation.xml | 0 .../res_partner_relation_all.xml | 8 +- .../res_partner_relation_type.xml | 17 +- 19 files changed, 707 insertions(+), 865 deletions(-) delete mode 100644 partner_relations/model/__init__.py delete mode 100644 partner_relations/model/res_partner.py delete mode 100644 partner_relations/model/res_partner_relation_type_selection.py create mode 100644 partner_relations/models/__init__.py create mode 100644 partner_relations/models/res_partner.py rename partner_relations/{model => models}/res_partner_relation.py (57%) rename partner_relations/{model => models}/res_partner_relation_all.py (74%) rename partner_relations/{model => models}/res_partner_relation_type.py (51%) create mode 100644 partner_relations/models/res_partner_relation_type_selection.py delete mode 100644 partner_relations/test/test_allow.yml rename partner_relations/{view => views}/menu.xml (100%) rename partner_relations/{view => views}/res_partner.xml (100%) rename partner_relations/{view => views}/res_partner_relation.xml (100%) rename partner_relations/{view => views}/res_partner_relation_all.xml (91%) rename partner_relations/{view => views}/res_partner_relation_type.xml (79%) diff --git a/partner_relations/__init__.py b/partner_relations/__init__.py index 057c357a1..91d6405f3 100644 --- a/partner_relations/__init__.py +++ b/partner_relations/__init__.py @@ -1,22 +1,5 @@ # -*- coding: utf-8 -*- -############################################################################## -# -# OpenERP, Open Source Management Solution -# This module copyright (C) 2013 Therp BV (). -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# -############################################################################## -from . import model +# © 2013-2016 Therp BV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from . import models from . import tests diff --git a/partner_relations/__openerp__.py b/partner_relations/__openerp__.py index 48936fb10..9a04dcae5 100644 --- a/partner_relations/__openerp__.py +++ b/partner_relations/__openerp__.py @@ -31,15 +31,12 @@ "demo": [ "data/demo.xml", ], - "test": [ - "test/test_allow.yml", - ], "data": [ - "view/res_partner_relation_all.xml", - 'view/res_partner_relation.xml', - 'view/res_partner.xml', - 'view/res_partner_relation_type.xml', - 'view/menu.xml', + "views/res_partner_relation_all.xml", + 'views/res_partner_relation.xml', + 'views/res_partner.xml', + 'views/res_partner_relation_type.xml', + 'views/menu.xml', 'security/ir.model.access.csv', ], "auto_install": False, diff --git a/partner_relations/data/demo.xml b/partner_relations/data/demo.xml index d177cecd2..200310595 100644 --- a/partner_relations/data/demo.xml +++ b/partner_relations/data/demo.xml @@ -12,6 +12,7 @@ Is competitor of c c + Has worked for diff --git a/partner_relations/model/__init__.py b/partner_relations/model/__init__.py deleted file mode 100644 index a493a9d0e..000000000 --- a/partner_relations/model/__init__.py +++ /dev/null @@ -1,26 +0,0 @@ -# -*- coding: utf-8 -*- -############################################################################## -# -# OpenERP, Open Source Management Solution -# This module copyright (C) 2013 Therp BV (). -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# -############################################################################## - -from . import res_partner -from . import res_partner_relation -from . import res_partner_relation_type -from . import res_partner_relation_all -from . import res_partner_relation_type_selection diff --git a/partner_relations/model/res_partner.py b/partner_relations/model/res_partner.py deleted file mode 100644 index 514b8101a..000000000 --- a/partner_relations/model/res_partner.py +++ /dev/null @@ -1,339 +0,0 @@ -# -*- coding: utf-8 -*- -'''Extend res.partner model''' -############################################################################## -# -# OpenERP, Open Source Management Solution -# This module copyright (C) 2013 Therp BV (). -# -# 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 . -# -############################################################################## -import time -from openerp import osv, models, fields, exceptions, api -from openerp.osv.expression import is_leaf, AND, OR, FALSE_LEAF -from openerp.tools import DEFAULT_SERVER_DATE_FORMAT -from openerp.tools.translate import _ - -PADDING = 10 - - -def get_partner_type(partner): - """Get partner type for relation. - - :param partner: a res.partner either a company or not - :return: 'c' for company or 'p' for person - :rtype: str - """ - return 'c' if partner.is_company else 'p' - - -class ResPartner(models.Model): - _inherit = 'res.partner' - - relation_count = fields.Integer( - 'Relation Count', - compute="_count_relations" - ) - - @api.one - @api.depends("relation_ids") - def _count_relations(self): - """Count the number of relations this partner has for Smart Button - - Don't count inactive relations. - """ - self.relation_count = len([r for r in self.relation_ids if r.active]) - - def _get_relation_ids_select(self, cr, uid, ids, field_name, arg, - context=None): - '''return the partners' relations as tuple - (id, left_partner_id, right_partner_id)''' - cr.execute( - '''select id, left_partner_id, right_partner_id - from res_partner_relation - where (left_partner_id in %s or right_partner_id in %s)''' + - ' order by ' + self.pool['res.partner.relation']._order, - (tuple(ids), tuple(ids)) - ) - return cr.fetchall() - - def _get_relation_ids( - self, cr, uid, ids, field_name, arg, context=None): - '''getter for relation_ids''' - if context is None: - context = {} - result = dict([(i, []) for i in ids]) - # TODO: do a permission test on returned ids - for row in self._get_relation_ids_select( - cr, uid, ids, field_name, arg, context=context): - if row[1] in result: - result[row[1]].append(row[0]) - if row[2] in result: - result[row[2]].append(row[0]) - return result - - def _set_relation_ids( - self, cr, uid, ids, dummy_name, field_value, dummy_arg, - context=None): - '''setter for relation_ids''' - if context is None: - context = {} - relation_obj = self.pool.get('res.partner.relation') - context2 = self.with_partner_relations_context( - cr, uid, ids, context=context).env.context - for value in field_value: - if value[0] == 0: - relation_obj.create(cr, uid, value[2], context=context2) - if value[0] == 1: - # if we write partner_id_display, we also need to pass - # type_selection_id in order to have this write end up on - # the correct field - if 'partner_id_display' in value[2] and 'type_selection_id'\ - not in value[2]: - relation_data = relation_obj.read( - cr, uid, [value[1]], ['type_selection_id'], - context=context)[0] - value[2]['type_selection_id'] =\ - relation_data['type_selection_id'] - relation_obj.write( - cr, uid, value[1], value[2], context=context2) - if value[0] == 2: - relation_obj.unlink(cr, uid, value[1], context=context2) - - def _search_relation_id( - self, cr, uid, dummy_obj, name, args, context=None): - result = [] - for arg in args: - if isinstance(arg, tuple) and arg[0] == name: - if arg[1] not in ['=', '!=', 'like', 'not like', 'ilike', - 'not ilike', 'in', 'not in']: - raise exceptions.ValidationError( - _('Unsupported search operand "%s"') % arg[1]) - - relation_type_selection_ids = [] - relation_type_selection = self\ - .pool['res.partner.relation.type.selection'] - - if arg[1] == '=' and isinstance(arg[2], (long, int)): - relation_type_selection_ids.append(arg[2]) - elif arg[1] == '!=' and isinstance(arg[2], (long, int)): - type_id, is_inverse = ( - relation_type_selection.browse(cr, uid, arg[2], - context=context) - .get_type_from_selection_id() - ) - result = OR([ - result, - [ - ('relation_all_ids.type_id', '!=', type_id), - ] - ]) - continue - else: - relation_type_selection_ids = relation_type_selection\ - .search( - cr, uid, - [ - ('type_id.name', arg[1], arg[2]), - ('record_type', '=', 'a'), - ], - context=context) - relation_type_selection_ids.extend( - relation_type_selection.search( - cr, uid, - [ - ('type_id.name_inverse', arg[1], arg[2]), - ('record_type', '=', 'b'), - ], - context=context)) - - if not relation_type_selection_ids: - result = AND([result, [FALSE_LEAF]]) - - for relation_type_selection_id in relation_type_selection_ids: - type_id, is_inverse = ( - relation_type_selection.browse( - cr, uid, relation_type_selection_id, - context=context - ).get_type_from_selection_id() - ) - - result = OR([ - result, - [ - '&', - ('relation_all_ids.type_id', '=', type_id), - ('relation_all_ids.record_type', '=', - 'b' if is_inverse else 'a') - ], - ]) - - return result - - def _search_relation_date(self, cr, uid, obj, name, args, context=None): - result = [] - for arg in args: - if isinstance(arg, tuple) and arg[0] == name: - # TODO: handle {<,>}{,=} - if arg[1] != '=': - continue - - result.extend([ - '&', - '|', - ('relation_all_ids.date_start', '=', False), - ('relation_all_ids.date_start', '<=', arg[2]), - '|', - ('relation_all_ids.date_end', '=', False), - ('relation_all_ids.date_end', '>=', arg[2]), - ]) - - return result - - def _search_related_partner_id( - self, cr, uid, dummy_obj, name, args, context=None): - result = [] - for arg in args: - if isinstance(arg, tuple) and arg[0] == name: - result.append( - ( - 'relation_all_ids.other_partner_id', - arg[1], - arg[2], - )) - - return result - - def _search_related_partner_category_id( - self, cr, uid, dummy_obj, name, args, context=None): - result = [] - for arg in args: - if isinstance(arg, tuple) and arg[0] == name: - result.append( - ( - 'relation_all_ids.other_partner_id.category_id', - arg[1], - arg[2], - )) - - return result - - _columns = { - 'relation_ids': osv.fields.function( - lambda self, *args, **kwargs: self._get_relation_ids( - *args, **kwargs), - fnct_inv=_set_relation_ids, - type='one2many', obj='res.partner.relation', - string='Relations', - selectable=False, - ), - 'relation_all_ids': osv.fields.one2many( - 'res.partner.relation.all', 'this_partner_id', - string='All relations with current partner', - auto_join=True, - selectable=False, - ), - 'search_relation_id': osv.fields.function( - lambda self, cr, uid, ids, *args: dict([ - (i, False) for i in ids]), - fnct_search=_search_relation_id, - string='Has relation of type', - type='many2one', obj='res.partner.relation.type.selection' - ), - 'search_relation_partner_id': osv.fields.function( - lambda self, cr, uid, ids, *args: dict([ - (i, False) for i in ids]), - fnct_search=_search_related_partner_id, - string='Has relation with', - type='many2one', obj='res.partner' - ), - 'search_relation_date': osv.fields.function( - lambda self, cr, uid, ids, *args: dict([ - (i, False) for i in ids]), - fnct_search=_search_relation_date, - string='Relation valid', type='date' - ), - 'search_relation_partner_category_id': osv.fields.function( - lambda self, cr, uid, ids, *args: dict([ - (i, False) for i in ids]), - fnct_search=_search_related_partner_category_id, - string='Has relation with a partner in category', - type='many2one', obj='res.partner.category' - ), - } - - def copy_data(self, cr, uid, id, default=None, context=None): - if default is None: - default = {} - default.setdefault('relation_ids', []) - default.setdefault('relation_all_ids', []) - return super(ResPartner, self).copy_data(cr, uid, id, default=default, - context=context) - - def search(self, cr, uid, args, offset=0, limit=None, order=None, - context=None, count=False): - if context is None: - context = {} - # inject searching for current relation date if we search for relation - # properties and no explicit date was given - date_args = [] - for arg in args: - if is_leaf(arg) and arg[0].startswith('search_relation'): - if arg[0] == 'search_relation_date': - date_args = [] - break - if not date_args: - date_args = [ - ('search_relation_date', '=', time.strftime( - DEFAULT_SERVER_DATE_FORMAT))] - - # because of auto_join, we have to do the active test by hand - active_args = [] - if context.get('active_test', True): - for arg in args: - if is_leaf(arg) and\ - arg[0].startswith('search_relation'): - active_args = [('relation_all_ids.active', '=', True)] - break - - return super(ResPartner, self).search( - cr, uid, args + date_args + active_args, offset=offset, - limit=limit, order=order, context=context, count=count) - - @api.v7 - def read(self, cr, user, ids, fields=None, context=None, - load='_classic_read'): - return super(ResPartner, self).read( - cr, user, ids, fields=fields, context=context, load=load) - - @api.v8 - def read(self, fields=None, load='_classic_read'): - return super(ResPartner, self.with_partner_relations_context())\ - .read(fields=fields, load=load) - - @api.multi - def write(self, vals): - return super(ResPartner, self.with_partner_relations_context())\ - .write(vals) - - @api.multi - def with_partner_relations_context(self): - context = dict(self.env.context) - if context.get('active_model', self._name) == self._name: - existing = self.exists() - context.setdefault( - 'active_id', existing.ids[0] if existing.ids else None) - context.setdefault('active_ids', existing.ids) - context.setdefault('active_model', self._name) - return self.with_context(context) diff --git a/partner_relations/model/res_partner_relation_type_selection.py b/partner_relations/model/res_partner_relation_type_selection.py deleted file mode 100644 index 138989b3f..000000000 --- a/partner_relations/model/res_partner_relation_type_selection.py +++ /dev/null @@ -1,175 +0,0 @@ -# -*- coding: utf-8 -*- -''' -Created on 23 may 2014 - -@author: Ronald Portier, Therp - -rportier@therp.nl -http://www.therp.nl - -For the model defined here _auto is set to False to prevent creating a -database file. All i/o operations are overridden to use a sql SELECT that -takes data from res_partner_connection_type where each type is included in the -result set twice, so it appears that the connection type and the inverse -type are separate records.. - -The original function _auto_init is still called because this function -normally (if _auto == True) not only creates the db tables, but it also takes -care of registering all fields in ir_model_fields. This is needed to make -the field labels translatable. - -example content for last lines of _statement: -select id, record_type, - customer_id, customer_name, customer_city, customer_zip, customer_street, - caller_id, caller_name, caller_phone, caller_fax, caller_email -from FULL_LIST as ResPartnerRelationTypeSelection where record_type = 'c' -ORDER BY ResPartnerRelationTypeSelection.customer_name asc, -ResPartnerRelationTypeSelection.caller_name asc; - -''' - -from openerp import api -from openerp.osv import fields -from openerp.osv import orm -from openerp.tools import drop_view_if_exists -from .res_partner_relation_type import ResPartnerRelationType -from .res_partner import PADDING - - -class ResPartnerRelationTypeSelection(orm.Model): - '''Virtual relation types''' - - _RECORD_TYPES = [ - ('a', 'Type'), - ('b', 'Inverse type'), - ] - - _auto = False # Do not try to create table in _auto_init(..) - _log_access = False - - @api.multi - def get_type_from_selection_id(self): - """Selection id ic computed from id of underlying type and the - kind of record. This function does the inverse computation to give - back the original type id, and about the record type.""" - type_id = self.id / PADDING - is_reverse = (self.id % PADDING) > 0 - return type_id, is_reverse - - def _auto_init(self, cr, context=None): - drop_view_if_exists(cr, self._table) - # TODO: we lose field value's translations here. - # probably we need to patch ir_translation.get_source for that - # to get res_partner_relation_type's translations - cr.execute( - '''create or replace view %(table)s as - select - id * %(padding)d as id, - id as type_id, - cast('a' as char(1)) as record_type, - name as name, - contact_type_left as contact_type_this, - contact_type_right as contact_type_other, - partner_category_left as partner_category_this, - partner_category_right as partner_category_other - from %(underlying_table)s - union select - id * %(padding)d + 1, - id, - cast('b' as char(1)), - name_inverse, - contact_type_right, - contact_type_left, - partner_category_right, - partner_category_left - from %(underlying_table)s''' % { - 'table': self._table, - 'padding': PADDING, - 'underlying_table': 'res_partner_relation_type', - }) - - return super(ResPartnerRelationTypeSelection, self)._auto_init( - cr, context=context) - - def _search_partner_category_this(self, cr, uid, obj, field_name, args, - context=None): - category_ids = [] - - for arg in args: - if isinstance(arg, tuple) and arg[0] == field_name\ - and (arg[1] == '=' or arg[1] == 'in'): - # TODO don't we have an api function to eval that? - for delta in arg[2]: - if delta[0] == 6: - category_ids.extend(delta[2]) - - if category_ids: - return [ - '|', - ('partner_category_this', '=', False), - ('partner_category_this', 'in', category_ids), - ] - else: - return [('partner_category_this', '=', False)] - - _name = 'res.partner.relation.type.selection' - _description = 'All relation types' - _foreign_keys = [] - _columns = { - 'record_type': fields.selection(_RECORD_TYPES, 'Record type', size=16), - 'type_id': fields.many2one( - 'res.partner.relation.type', 'Type'), - 'name': fields.char('Name', size=64), - 'contact_type_this': fields.selection( - ResPartnerRelationType._get_partner_types.im_func, - 'Current record\'s partner type'), - 'contact_type_other': fields.selection( - ResPartnerRelationType._get_partner_types.im_func, - 'Other record\'s partner type'), - 'partner_category_this': fields.many2one( - 'res.partner.category', 'Current record\'s category'), - 'partner_category_other': fields.many2one( - 'res.partner.category', 'Other record\'s category'), - # search field to handle many2many deltas from the client - 'search_partner_category_this': fields.function( - lambda self, cr, uid, ids, context=None: dict( - [(i, False) for i in ids]), - fnct_search=_search_partner_category_this, - type='many2many', obj='res.partner.category', - string='Current record\'s category'), - } - _order = 'name asc' - - def name_get(self, cr, uid, ids, context=None): - 'translate name using translations from res.partner.relation.type' - result = super(ResPartnerRelationTypeSelection, self).name_get( - cr, uid, ids, context=context) - ir_translation = self.pool['ir.translation'] - return [ - (i, ir_translation._get_source( - cr, uid, - 'res.partner.relation.type,name_inverse' - if self.get_type_from_selection_id(cr, uid, i)[1] - else 'res.partner.relation.type,name', - 'model', context.get('lang'), name)) - for i, name in result] - - def name_search(self, cr, uid, name='', args=None, operator='ilike', - context=None, limit=100): - 'search for translated names in res.partner.relation.type' - res_partner_relation_type = self.pool['res.partner.relation.type'] - relation_ids = res_partner_relation_type.search( - cr, uid, [('name', operator, name)], - context=context) - inverse_relation_ids = res_partner_relation_type.search( - cr, uid, [('name_inverse', operator, name)], - context=context) - all_ids = self.search( - cr, uid, - [ - ('id', 'in', - map(lambda x: x * PADDING, relation_ids) + - map(lambda x: x * PADDING + 1, inverse_relation_ids)), - ] + (args or []), - context=context, limit=limit) - return self.name_get(cr, uid, all_ids, context=context) diff --git a/partner_relations/models/__init__.py b/partner_relations/models/__init__.py new file mode 100644 index 000000000..8aef17344 --- /dev/null +++ b/partner_relations/models/__init__.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +# © 2013-2016 Therp BV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from . import res_partner +from . import res_partner_relation +from . import res_partner_relation_type +from . import res_partner_relation_all +from . import res_partner_relation_type_selection diff --git a/partner_relations/models/res_partner.py b/partner_relations/models/res_partner.py new file mode 100644 index 000000000..590aeab96 --- /dev/null +++ b/partner_relations/models/res_partner.py @@ -0,0 +1,213 @@ +# -*- coding: utf-8 -*- +# © 2013-2016 Therp BV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +import numbers +from openerp import _, models, fields, exceptions, api +from openerp.osv.expression import is_leaf, OR, FALSE_LEAF + +PADDING = 10 + + +class ResPartner(models.Model): + _inherit = 'res.partner' + + relation_count = fields.Integer( + 'Relation Count', + compute="_compute_relation_count" + ) + + relation_ids = fields.One2many( + 'res.partner.relation', string='Relations', + compute='_compute_relation_ids', + selectable=False, + ) + + relation_all_ids = fields.One2many( + 'res.partner.relation.all', 'this_partner_id', + string='All relations with current partner', + auto_join=True, selectable=False, copy=False, + ) + + search_relation_id = fields.Many2one( + 'res.partner.relation.type.selection', compute=lambda self: None, + search='_search_relation_id', string='Has relation of type', + ) + + search_relation_partner_id = fields.Many2one( + 'res.partner', compute=lambda self: None, + search='_search_related_partner_id', string='Has relation with', + ) + + search_relation_date = fields.Date( + compute=lambda self: None, search='_search_relation_date', + string='Relation valid', + ) + + search_relation_partner_category_id = fields.Many2one( + 'res.partner.category', compute=lambda self: None, + search='_search_related_partner_category_id', + string='Has relation with a partner in category', + ) + + @api.one + @api.depends("relation_ids") + def _compute_relation_count(self): + """Count the number of relations this partner has for Smart Button + + Don't count inactive relations. + """ + self.relation_count = len([r for r in self.relation_ids if r.active]) + + @api.multi + def _compute_relation_ids(self): + '''getter for relation_ids''' + self.env.cr.execute( + "select p.id, array_agg(r.id) " + "from res_partner p join res_partner_relation r " + "on r.left_partner_id=p.id or r.right_partner_id=p.id " + "where p.id in %s " + "group by p.id", + (tuple(self.ids),) + ) + partner2relation = dict(self.env.cr.fetchall()) + for this in self: + this.relation_ids += self.env['res.partner.relation'].browse( + partner2relation.get(this.id, []), + ) + + @api.model + def _search_relation_id(self, operator, value): + result = [] + + if operator not in [ + '=', '!=', 'like', 'not like', 'ilike', 'not ilike', 'in', 'not in' + ]: + raise exceptions.ValidationError( + _('Unsupported search operator "%s"') % operator) + + relation_type_selection = [] + + if operator == '=' and isinstance(value, numbers.Integral): + relation_type_selection += self\ + .env['res.partner.relation.type.selection']\ + .browse(value) + elif operator == '!=' and isinstance(value, numbers.Integral): + relation_type_selection = self\ + .env['res.partner.relation.type.selection']\ + .search([ + ('id', operator, value), + ]) + else: + relation_type_selection = self\ + .env['res.partner.relation.type.selection']\ + .search([ + ('type_id.name', operator, value), + ]) + + if not relation_type_selection: + result = [FALSE_LEAF] + + for relation_type in relation_type_selection: + type_id, is_inverse = relation_type.get_type_from_selection_id() + + result = OR([ + result, + [ + '&', + ('relation_all_ids.type_id', '=', type_id), + ( + 'relation_all_ids.record_type', 'in', + ['a', 'b'] + if relation_type.type_id.symmetric + else + (['b'] if is_inverse else ['a']) + ) + ], + ]) + + return result + + @api.model + def _search_related_partner_id(self, operator, value): + return [ + ('relation_all_ids.other_partner_id', operator, value), + ] + + @api.model + def _search_relation_date(self, operator, value): + if operator != '=': + raise exceptions.ValidationError( + _('Unsupported search operator "%s"') % operator) + + return [ + '&', + '|', + ('relation_all_ids.date_start', '=', False), + ('relation_all_ids.date_start', '<=', value), + '|', + ('relation_all_ids.date_end', '=', False), + ('relation_all_ids.date_end', '>=', value), + ] + + @api.model + def _search_related_partner_category_id(self, operator, value): + return [ + ('relation_all_ids.other_partner_id.category_id', operator, value), + ] + + @api.model + def search(self, args, offset=0, limit=None, order=None, count=False): + # inject searching for current relation date if we search for relation + # properties and no explicit date was given + date_args = [] + for arg in args: + if is_leaf(arg) and arg[0].startswith('search_relation'): + if arg[0] == 'search_relation_date': + date_args = [] + break + if not date_args: + date_args = [ + ('search_relation_date', '=', fields.Date.today()), + ] + + # because of auto_join, we have to do the active test by hand + active_args = [] + if self.env.context.get('active_test', True): + for arg in args: + if is_leaf(arg) and arg[0].startswith('search_relation'): + active_args = [('relation_all_ids.active', '=', True)] + break + + return super(ResPartner, self).search( + args + date_args + active_args, offset=offset, limit=limit, + order=order, count=count) + + @api.multi + def read(self, fields=None, load='_classic_read'): + return super(ResPartner, self.with_partner_relations_context())\ + .read(fields=fields, load=load) + + @api.multi + def write(self, vals): + return super(ResPartner, self.with_partner_relations_context())\ + .write(vals) + + @api.multi + def with_partner_relations_context(self): + context = dict(self.env.context) + if context.get('active_model', self._name) == self._name: + existing = self.exists() + context.setdefault( + 'active_id', existing.ids[0] if existing.ids else None) + context.setdefault('active_ids', existing.ids) + context.setdefault('active_model', self._name) + return self.with_context(context) + + @api.multi + def get_partner_type(self): + """Get partner type for relation. + :return: 'c' for company or 'p' for person + :rtype: str + """ + self.ensure_one() + return 'c' if self.is_company else 'p' diff --git a/partner_relations/model/res_partner_relation.py b/partner_relations/models/res_partner_relation.py similarity index 57% rename from partner_relations/model/res_partner_relation.py rename to partner_relations/models/res_partner_relation.py index 448b02889..a898c750f 100644 --- a/partner_relations/model/res_partner_relation.py +++ b/partner_relations/models/res_partner_relation.py @@ -1,28 +1,9 @@ # -*- coding: utf-8 -*- -'''Define model res.partner.relation''' -############################################################################## -# -# OpenERP, Open Source Management Solution -# This module copyright (C) 2013 Therp BV (). -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# -############################################################################## - -from openerp import osv, models, fields, api, exceptions, _ - -from .res_partner import get_partner_type +# © 2013-2016 Therp BV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from openerp import models, fields, api, exceptions, _ +from openerp.osv.expression import FALSE_LEAF +from .res_partner import PADDING class ResPartnerRelation(models.Model): @@ -41,73 +22,40 @@ class ResPartnerRelation(models.Model): _description = 'Partner relation' _order = 'active desc, date_start desc, date_end desc' - def _search_any_partner_id(self, operator, value): - return [ - '|', - ('left_partner_id', operator, value), - ('right_partner_id', operator, value), - ] + type_selection_id = fields.Many2one( + 'res.partner.relation.type.selection', + compute='_compute_fields', + fnct_inv=lambda *args: None, + string='Type', + ) - def _get_computed_fields( - self, cr, uid, ids, field_names, arg, context=None): - '''Return a dictionary of dictionaries, with for every partner for - ids, the computed values.''' - def get_values(self, dummy_field_names, dummy_arg, context=None): - '''Get computed values for record''' - values = {} - on_right_partner = self._on_right_partner(self.right_partner_id.id) - # type_selection_id - values['type_selection_id'] = ( - ((self.type_id.id) * 10) + (on_right_partner and 1 or 0)) - # partner_id_display - values['partner_id_display'] = ( - self.left_partner_id.id - if on_right_partner - else self.right_partner_id.id - ) - return values - - return dict([ - (i.id, get_values(i, field_names, arg, context=context)) - for i in self.browse(cr, uid, ids, context=context) - ]) - - _columns = { - 'type_selection_id': osv.fields.function( - _get_computed_fields, - multi="computed_fields", - fnct_inv=lambda *args: None, - type='many2one', obj='res.partner.relation.type.selection', - string='Type', - ), - 'partner_id_display': osv.fields.function( - _get_computed_fields, - multi="computed_fields", - fnct_inv=lambda *args: None, - type='many2one', obj='res.partner', - string='Partner' - ), - } + partner_id_display = fields.Many2one( + 'res.partner', + compute='_compute_fields', + fnct_inv=lambda *args: None, + string='Partner', + ) allow_self = fields.Boolean(related='type_id.allow_self') + left_contact_type = fields.Selection( lambda s: s.env['res.partner.relation.type']._get_partner_types(), 'Left Partner Type', - compute='_get_partner_type_any', + compute='_compute_any_partner_id', store=True, ) right_contact_type = fields.Selection( lambda s: s.env['res.partner.relation.type']._get_partner_types(), 'Right Partner Type', - compute='_get_partner_type_any', + compute='_compute_any_partner_id', store=True, ) any_partner_id = fields.Many2many( 'res.partner', string='Partner', - compute='_get_partner_type_any', + compute='_compute_any_partner_id', search='_search_any_partner_id' ) @@ -138,55 +86,111 @@ class ResPartnerRelation(models.Model): date_end = fields.Date('Ending date') active = fields.Boolean('Active', default=True) + @api.multi + def _compute_fields(self): + for this in self: + on_right_partner = this._on_right_partner() + this.type_selection_id = self\ + .env['res.partner.relation.type.selection']\ + .browse(this.type_id.id * PADDING + + (on_right_partner and 1 or 0)) + this.partner_id_display = ( + this.left_partner_id + if on_right_partner + else this.right_partner_id + ) + + @api.onchange('type_selection_id') + def _onchange_type_selection_id(self): + '''Set domain on partner_id_display, when selection a relation type''' + result = { + 'domain': {'partner_id_display': [FALSE_LEAF]}, + } + if not self.type_selection_id: + return result + type_id, is_reverse = self.type_selection_id\ + .get_type_from_selection_id() + self.type_id = self.env['res.partner.relation.type'].browse(type_id) + partner_domain = [] + check_contact_type = self.type_id.contact_type_right + check_partner_category = self.type_id.partner_category_right + if is_reverse: + # partner_id_display is left partner + check_contact_type = self.type_id.contact_type_left + check_partner_category = self.type_id.partner_category_left + if check_contact_type == 'c': + partner_domain.append(('is_company', '=', True)) + if check_contact_type == 'p': + partner_domain.append(('is_company', '=', False)) + if check_partner_category: + partner_domain.append( + ('category_id', 'child_of', check_partner_category.ids)) + result['domain']['partner_id_display'] = partner_domain + return result + @api.one @api.depends('left_partner_id', 'right_partner_id') - def _get_partner_type_any(self): - self.left_contact_type = get_partner_type(self.left_partner_id) - self.right_contact_type = get_partner_type(self.right_partner_id) - + def _compute_any_partner_id(self): + self.left_contact_type = self.left_partner_id.get_partner_type() + self.right_contact_type = self.right_partner_id.get_partner_type() self.any_partner_id = self.left_partner_id + self.right_partner_id - def _on_right_partner(self, cr, uid, right_partner_id, context=None): + @api.model + def _search_any_partner_id(self, operator, value): + return [ + '|', + ('left_partner_id', operator, value), + ('right_partner_id', operator, value), + ] + + @api.multi + def _on_right_partner(self): '''Determine wether functions are called in a situation where the active partner is the right partner. Default False! ''' - if (context and 'active_ids' in context and - right_partner_id in context.get('active_ids', [])): - return True - return False + return set(self.mapped('right_partner_id').ids) &\ + set(self.env.context.get('active_ids', [])) + @api.model def _correct_vals(self, vals): """Fill type and left and right partner id, according to whether we have a normal relation type or an inverse relation type """ vals = vals.copy() - # If type_selection_id ends in 1, it is a reverse relation type - if 'type_selection_id' in vals: - prts_model = self.env['res.partner.relation.type.selection'] - type_selection_id = vals['type_selection_id'] - (type_id, is_reverse) = ( - prts_model.browse(type_selection_id). - get_type_from_selection_id() - ) - vals['type_id'] = type_id - if self._context.get('active_id'): - if is_reverse: - vals['right_partner_id'] = self._context['active_id'] - else: - vals['left_partner_id'] = self._context['active_id'] - if vals.get('partner_id_display'): - if is_reverse: - vals['left_partner_id'] = vals['partner_id_display'] - else: - vals['right_partner_id'] = vals['partner_id_display'] - if vals.get('other_partner_id'): - if is_reverse: - vals['left_partner_id'] = vals['other_partner_id'] - else: - vals['right_partner_id'] = vals['other_partner_id'] - del vals['other_partner_id'] - if vals.get('contact_type'): - del vals['contact_type'] + if 'type_selection_id' not in vals: + return vals + + type_id, is_reverse = self\ + .env['res.partner.relation.type.selection']\ + .browse(vals['type_selection_id'])\ + .get_type_from_selection_id() + + vals['type_id'] = type_id + + if self._context.get('active_id'): + if is_reverse: + vals['right_partner_id'] = self._context['active_id'] + else: + vals['left_partner_id'] = self._context['active_id'] + if vals.get('partner_id_display'): + if is_reverse: + vals['left_partner_id'] = vals['partner_id_display'] + else: + vals['right_partner_id'] = vals['partner_id_display'] + if vals.get('other_partner_id'): + if is_reverse: + vals['left_partner_id'] = vals['other_partner_id'] + else: + vals['right_partner_id'] = vals['other_partner_id'] + del vals['other_partner_id'] + if vals.get('this_partner_id'): + if is_reverse: + vals['right_partner_id'] = vals['this_partner_id'] + else: + vals['left_partner_id'] = vals['this_partner_id'] + del vals['this_partner_id'] + if vals.get('contact_type'): + del vals['contact_type'] return vals @api.multi @@ -201,46 +205,6 @@ class ResPartnerRelation(models.Model): vals = self._correct_vals(vals) return super(ResPartnerRelation, self).create(vals) - def on_change_type_selection_id( - self, cr, uid, dummy_ids, type_selection_id, context=None): - '''Set domain on partner_id_display, when selection a relation type''' - result = { - 'domain': {'partner_id_display': []}, - 'value': {'type_id': False} - } - if not type_selection_id: - return result - prts_model = self.pool['res.partner.relation.type.selection'] - type_model = self.pool['res.partner.relation.type'] - (type_id, is_reverse) = ( - prts_model.get_type_from_selection_id( - cr, uid, type_selection_id) - ) - result['value']['type_id'] = type_id - type_obj = type_model.browse(cr, uid, type_id, context=context) - partner_domain = [] - check_contact_type = type_obj.contact_type_right - check_partner_category = ( - type_obj.partner_category_right and - type_obj.partner_category_right.id - ) - if is_reverse: - # partner_id_display is left partner - check_contact_type = type_obj.contact_type_left - check_partner_category = ( - type_obj.partner_category_left and - type_obj.partner_category_left.id - ) - if check_contact_type == 'c': - partner_domain.append(('is_company', '=', True)) - if check_contact_type == 'p': - partner_domain.append(('is_company', '=', False)) - if check_partner_category: - partner_domain.append( - ('category_id', 'child_of', check_partner_category)) - result['domain']['partner_id_display'] = partner_domain - return result - @api.one @api.constrains('date_start', 'date_end') def _check_dates(self): @@ -329,25 +293,24 @@ class ResPartnerRelation(models.Model): _('There is already a similar relation with overlapping dates') ) - def get_action_related_partners(self, cr, uid, ids, context=None): + @api.multi + def get_action_related_partners(self): '''return a window action showing a list of partners taking part in the relations names by ids. Context key 'partner_relations_show_side' determines if we show 'left' side, 'right' side or 'all' (default) partners. If active_model is res.partner.relation.all, left=this and right=other''' - if context is None: - context = {} - field_names = {} - if context.get('active_model', self._name) == self._name: + if self.env.context.get('active_model', self._name) == self._name: field_names = { 'left': ['left'], 'right': ['right'], 'all': ['left', 'right'] } - elif context.get('active_model') == 'res.partner.relation.all': + elif self.env.context.get('active_model') ==\ + 'res.partner.relation.all': field_names = { 'left': ['this'], 'right': ['other'], @@ -356,21 +319,22 @@ class ResPartnerRelation(models.Model): else: assert False, 'Unknown active_model!' - partner_ids = [] + partners = self.env['res.partner'].browse([]) field_names = field_names[ - context.get('partner_relations_show_side', 'all')] + self.env.context.get('partner_relations_show_side', 'all') + ] field_names = ['%s_partner_id' % n for n in field_names] - for relation in self.pool[context.get('active_model')].read( - cr, uid, ids, context=context, load='_classic_write'): + for relation in self.env[self.env.context.get('active_model')].browse( + self.ids): for name in field_names: - partner_ids.append(relation[name]) + partners += relation[name] return { 'name': _('Related partners'), 'type': 'ir.actions.act_window', 'res_model': 'res.partner', - 'domain': [('id', 'in', partner_ids)], + 'domain': [('id', 'in', partners.ids)], 'views': [(False, 'tree'), (False, 'form')], 'view_type': 'form' } diff --git a/partner_relations/model/res_partner_relation_all.py b/partner_relations/models/res_partner_relation_all.py similarity index 74% rename from partner_relations/model/res_partner_relation_all.py rename to partner_relations/models/res_partner_relation_all.py index 59bc12bc7..7efff67c9 100644 --- a/partner_relations/model/res_partner_relation_all.py +++ b/partner_relations/models/res_partner_relation_all.py @@ -1,29 +1,12 @@ # -*- coding: utf-8 -*- -############################################################################## -# -# OpenERP, Open Source Management Solution -# This module copyright (C) 2014 Therp BV (). -# -# 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 . -# -############################################################################## - +# © 2014-2016 Therp BV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from psycopg2.extensions import AsIs from openerp import models, fields, api from openerp.tools import drop_view_if_exists -from .res_partner_relation_type_selection import \ +from .res_partner_relation_type_selection import\ ResPartnerRelationTypeSelection -from .res_partner import get_partner_type, PADDING +from .res_partner import PADDING class ResPartnerRelationAll(models.AbstractModel): @@ -101,7 +84,7 @@ class ResPartnerRelationAll(models.AbstractModel): cr.execute( '''create or replace view %(table)s as select - id * %(padding)d as id, + id * %(padding)s as id, id as relation_id, type_id, cast('a' as char(1)) as record_type, @@ -111,11 +94,11 @@ class ResPartnerRelationAll(models.AbstractModel): date_start, date_end, active, - type_id * %(padding)d as type_selection_id + type_id * %(padding)s as type_selection_id %(additional_view_fields)s from %(underlying_table)s union select - id * %(padding)d + 1, + id * %(padding)s + 1, id, type_id, cast('b' as char(1)), @@ -125,28 +108,32 @@ class ResPartnerRelationAll(models.AbstractModel): date_start, date_end, active, - type_id * %(padding)d + 1 + type_id * %(padding)s + 1 %(additional_view_fields)s - from %(underlying_table)s''' % { - 'table': self._table, + from %(underlying_table)s''', + { + 'table': AsIs(self._table), 'padding': PADDING, - 'additional_view_fields': additional_view_fields, - 'underlying_table': 'res_partner_relation', + 'additional_view_fields': AsIs(additional_view_fields), + 'underlying_table': AsIs('res_partner_relation'), } ) return super(ResPartnerRelationAll, self)._auto_init( cr, context=context) + @api.multi def _get_underlying_object(self): """Get the record on which this record is overlaid""" - return self.env[self._overlays].browse(self.id / PADDING) + return self.env[self._overlays].browse( + i / PADDING for i in self.ids) + @api.multi def _get_default_contact_type(self): partner_id = self._context.get('default_this_partner_id') if partner_id: partner = self.env['res.partner'].browse(partner_id) - return get_partner_type(partner) + return partner.get_partner_type() return False @api.multi @@ -154,7 +141,7 @@ class ResPartnerRelationAll(models.AbstractModel): return { this.id: '%s %s %s' % ( this.this_partner_id.name, - this.type_selection_id.name_get()[0][1], + this.type_selection_id.display_name, this.other_partner_id.name, ) for this in self @@ -189,7 +176,7 @@ class ResPartnerRelationAll(models.AbstractModel): '|', ('contact_type_this', '=', False), ('contact_type_this', '=', - 'c' if self.this_partner_id else 'p'), + self.this_partner_id.get_partner_type()), '|', ('partner_category_this', '=', False), ('partner_category_this', 'in', @@ -198,19 +185,15 @@ class ResPartnerRelationAll(models.AbstractModel): }, } - @api.one + @api.multi def write(self, vals): """divert non-problematic writes to underlying table""" underlying_objs = self._get_underlying_object() vals = { key: val for key, val in vals.iteritems() - if not self._columns[key].readonly + if not self._fields[key].readonly } - vals['type_selection_id'] = vals.get( - 'type_selection_id', - underlying_objs.type_selection_id.id - ) return underlying_objs.write(vals) @api.model @@ -222,16 +205,12 @@ class ResPartnerRelationAll(models.AbstractModel): vals = { key: val for key, val in vals.iteritems() - if not self._columns[key].readonly + if not self._fields[key].readonly } - vals['type_selection_id'] = vals.get( - 'type_selection_id', - False, - ) res = self.env[self._overlays].create(vals) return self.browse(res.id * PADDING) - @api.one + @api.multi def unlink(self): """divert non-problematic creates to underlying table""" return self._get_underlying_object().unlink() diff --git a/partner_relations/model/res_partner_relation_type.py b/partner_relations/models/res_partner_relation_type.py similarity index 51% rename from partner_relations/model/res_partner_relation_type.py rename to partner_relations/models/res_partner_relation_type.py index 624ac4988..218991f74 100644 --- a/partner_relations/model/res_partner_relation_type.py +++ b/partner_relations/models/res_partner_relation_type.py @@ -1,25 +1,6 @@ # -*- coding: utf-8 -*- -'''Define model res.partner.relation.type''' -############################################################################## -# -# OpenERP, Open Source Management Solution -# This module copyright (C) 2013 Therp BV (). -# -# 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 . -# -############################################################################## - +# © 2013-2016 Therp BV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). from openerp import models, fields, api, _ @@ -31,13 +12,11 @@ class ResPartnerRelationType(models.Model): name = fields.Char( 'Name', - size=128, required=True, translate=True, ) name_inverse = fields.Char( 'Inverse name', - size=128, required=True, translate=True, ) @@ -58,7 +37,15 @@ class ResPartnerRelationType(models.Model): 'Right partner category', ) allow_self = fields.Boolean( - 'Allow both sides to be the same', + 'Reflexive', + help='This relation can be set up with the same partner left and ' + 'right', + default=False, + ) + symmetric = fields.Boolean( + 'Symmetric', + help='This relation is the same from right to left as from left to ' + 'right', default=False, ) @@ -68,3 +55,11 @@ class ResPartnerRelationType(models.Model): ('c', _('Company')), ('p', _('Person')), ] + + @api.onchange('symmetric') + def _onchange_symmetric(self): + self.update({ + 'name_inverse': self.name, + 'contact_type_right': self.contact_type_left, + 'partner_category_right': self.partner_category_left, + }) diff --git a/partner_relations/models/res_partner_relation_type_selection.py b/partner_relations/models/res_partner_relation_type_selection.py new file mode 100644 index 000000000..3ee150f09 --- /dev/null +++ b/partner_relations/models/res_partner_relation_type_selection.py @@ -0,0 +1,149 @@ +# -*- coding: utf-8 -*- +# © 2014-2016 Therp BV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +''' +Created on 23 may 2014 + +@author: Ronald Portier, Therp + +rportier@therp.nl +http://www.therp.nl + +For the model defined here _auto is set to False to prevent creating a +database file. All i/o operations are overridden to use a sql SELECT that +takes data from res_partner_connection_type where each type is included in the +result set twice, so it appears that the connection type and the inverse +type are separate records.. + +The original function _auto_init is still called because this function +normally (if _auto == True) not only creates the db tables, but it also takes +care of registering all fields in ir_model_fields. This is needed to make +the field labels translatable. + +example content for last lines of _statement: +select id, record_type, + customer_id, customer_name, customer_city, customer_zip, customer_street, + caller_id, caller_name, caller_phone, caller_fax, caller_email +from FULL_LIST as ResPartnerRelationTypeSelection where record_type = 'c' +ORDER BY ResPartnerRelationTypeSelection.customer_name asc, +ResPartnerRelationTypeSelection.caller_name asc; + +''' +from psycopg2.extensions import AsIs +from openerp import api, fields, models +from openerp.tools import drop_view_if_exists +from .res_partner_relation_type import ResPartnerRelationType +from .res_partner import PADDING + + +class ResPartnerRelationTypeSelection(models.Model): + '''Virtual relation types''' + _name = 'res.partner.relation.type.selection' + _description = 'All relation types' + _auto = False # Do not try to create table in _auto_init(..) + _foreign_keys = [] + _log_access = False + _order = 'name asc' + + _RECORD_TYPES = [ + ('a', 'Type'), + ('b', 'Inverse type'), + ] + + record_type = fields.Selection(_RECORD_TYPES, 'Record type') + type_id = fields.Many2one('res.partner.relation.type', 'Type') + name = fields.Char('Name') + contact_type_this = fields.Selection( + ResPartnerRelationType._get_partner_types.im_func, + 'Current record\'s partner type') + contact_type_other = fields.Selection( + ResPartnerRelationType._get_partner_types.im_func, + 'Other record\'s partner type') + partner_category_this = fields.Many2one( + 'res.partner.category', 'Current record\'s category') + partner_category_other = fields.Many2one( + 'res.partner.category', 'Other record\'s category') + + def _auto_init(self, cr, context=None): + drop_view_if_exists(cr, self._table) + cr.execute( + '''create or replace view %(table)s as + select + id * %(padding)s as id, + id as type_id, + cast('a' as char(1)) as record_type, + name as name, + contact_type_left as contact_type_this, + contact_type_right as contact_type_other, + partner_category_left as partner_category_this, + partner_category_right as partner_category_other + from %(underlying_table)s + union select + id * %(padding)s + 1, + id, + cast('b' as char(1)), + name_inverse, + contact_type_right, + contact_type_left, + partner_category_right, + partner_category_left + from %(underlying_table)s''', + { + 'table': AsIs(self._table), + 'padding': PADDING, + 'underlying_table': AsIs('res_partner_relation_type'), + }) + + return super(ResPartnerRelationTypeSelection, self)._auto_init( + cr, context=context) + + @api.multi + def name_get(self): + """translate name using translations from res.partner.relation.type""" + ir_translation = self.env['ir.translation'] + return [ + ( + this.id, + ir_translation._get_source( + 'res.partner.relation.type,name_inverse' + if this.get_type_from_selection_id()[1] + else 'res.partner.relation.type,name', + ('model',), + self.env.context.get('lang'), + this.name, + this.type_id.id + ) + ) + for this in self + ] + + @api.model + def name_search(self, name='', args=None, operator='ilike', limit=100): + """search for translated names in res.partner.relation.type""" + res_partner_relation_type = self.env['res.partner.relation.type'] + relations = res_partner_relation_type.search([ + ('name', operator, name) + ]) + inverse_relations = res_partner_relation_type.search([ + ('name_inverse', operator, name), + ('symmetric', '=', False), + ]) + return self.search( + [ + ( + 'id', 'in', + map(lambda x: x * PADDING, relations.ids) + + map(lambda x: x * PADDING + 1, inverse_relations.ids) + ), + ] + (args or []), + limit=limit + ).name_get() + + @api.multi + def get_type_from_selection_id(self): + """Selection id ic computed from id of underlying type and the + kind of record. This function does the inverse computation to give + back the original type id, and about the record type.""" + type_id = self.id / PADDING + is_reverse = (self.id % PADDING) > 0 + return type_id, is_reverse diff --git a/partner_relations/test/test_allow.yml b/partner_relations/test/test_allow.yml deleted file mode 100644 index cbd89f803..000000000 --- a/partner_relations/test/test_allow.yml +++ /dev/null @@ -1,22 +0,0 @@ -- - I create a relation allowing the same partner at both ends. -- - !record {model: res.partner.relation.type, id: partner_relations.allow_self}: - name: 'Relation Allow' - name_inverse: 'Inverse Relation Allow' - contact_type_right: 'p' - contact_type_left: 'p' - allow_self: True -- - I create a partner U for testing purposes -- - !record {model: res.partner, id: partner_relations.test_U}: - name: 'unittests.U' - image: '' -- - I create relation instance U -- (allow) --> U -- - !record {model: res.partner.relation, id: partner_relations.test_allow}: - left_partner_id: partner_relations.test_U - right_partner_id: partner_relations.test_U - type_id: partner_relations.allow_self diff --git a/partner_relations/tests/test_partner_relations.py b/partner_relations/tests/test_partner_relations.py index 6d471fc02..9e60c3680 100644 --- a/partner_relations/tests/test_partner_relations.py +++ b/partner_relations/tests/test_partner_relations.py @@ -1,20 +1,8 @@ # -*- coding: utf-8 -*- -# Author: Charbel Jacquin -# Copyright 2015 Camptocamp SA -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - +# Copyright 2015 Camptocamp SA +# Copyright 2016 Therp BV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from openerp import fields from openerp.tests import common from openerp.exceptions import ValidationError @@ -30,13 +18,13 @@ class TestPartnerRelation(common.TransactionCase): self.relation_model = self.env['res.partner.relation'] self.partner_1 = self.partner_model.create({ - 'name': 'Test User 1', - 'is_company': False + 'name': 'Test User 1', + 'is_company': False, }) self.partner_2 = self.partner_model.create({ - 'name': 'Test User 2', - 'is_company': False + 'name': 'Test Company', + 'is_company': True, }) self.relation_allow = self.relation_type_model.create({ @@ -62,20 +50,150 @@ class TestPartnerRelation(common.TransactionCase): 'contact_type_right': 'p', }) - def test_self_allowed(self): + self.relation_mixed = self.relation_type_model.create({ + 'name': 'mixed', + 'name_inverse': 'mixed_inverse', + 'contact_type_left': 'c', + 'contact_type_right': 'p', + }) - self.relation_model.create({'type_id': self.relation_allow.id, - 'left_partner_id': self.partner_1.id, - 'right_partner_id': self.partner_1.id}) + self.relation_symmetric = self.relation_type_model.create({ + 'name': 'sym', + 'name_inverse': 'sym', + 'symmetric': True, + }) + + def test_self_allowed(self): + self.relation_model.create({ + 'type_id': self.relation_allow.id, + 'left_partner_id': self.partner_1.id, + 'right_partner_id': self.partner_1.id, + }) def test_self_disallowed(self): with self.assertRaises(ValidationError): - self.relation_model.create({'type_id': self.relation_disallow.id, - 'left_partner_id': self.partner_1.id, - 'right_partner_id': self.partner_1.id}) + self.relation_model.create({ + 'type_id': self.relation_disallow.id, + 'left_partner_id': self.partner_1.id, + 'right_partner_id': self.partner_1.id, + }) def test_self_default(self): with self.assertRaises(ValidationError): - self.relation_model.create({'type_id': self.relation_default.id, - 'left_partner_id': self.partner_1.id, - 'right_partner_id': self.partner_1.id}) + self.relation_model.create({ + 'type_id': self.relation_default.id, + 'left_partner_id': self.partner_1.id, + 'right_partner_id': self.partner_1.id, + }) + + def test_self_mixed(self): + with self.assertRaises(ValidationError): + self.relation_model.create({ + 'type_id': self.relation_mixed.id, + 'left_partner_id': self.partner_1.id, + 'right_partner_id': self.partner_2.id, + }) + + def test_searching(self): + relation = self.relation_model.create({ + 'type_id': self.relation_mixed.id, + 'left_partner_id': self.partner_2.id, + 'right_partner_id': self.partner_1.id, + }) + partners = self.env['res.partner'].search([ + ('search_relation_id', '=', relation.type_selection_id.id) + ]) + self.assertTrue(self.partner_2 in partners) + + partners = self.env['res.partner'].search([ + ('search_relation_id', '!=', relation.type_selection_id.id) + ]) + self.assertTrue(self.partner_1 in partners) + + partners = self.env['res.partner'].search([ + ('search_relation_id', '=', self.relation_mixed.name) + ]) + self.assertTrue(self.partner_1 in partners) + self.assertTrue(self.partner_2 in partners) + + partners = self.env['res.partner'].search([ + ('search_relation_id', '=', 'unknown relation') + ]) + self.assertFalse(partners) + + partners = self.env['res.partner'].search([ + ('search_relation_partner_id', '=', self.partner_2.id), + ]) + self.assertTrue(self.partner_1 in partners) + + partners = self.env['res.partner'].search([ + ('search_relation_date', '=', fields.Date.today()), + ]) + self.assertTrue(self.partner_1 in partners) + self.assertTrue(self.partner_2 in partners) + + def test_ui_functions(self): + relation = self.relation_model.create({ + 'type_id': self.relation_mixed.id, + 'left_partner_id': self.partner_2.id, + 'right_partner_id': self.partner_1.id, + }) + self.assertEqual(relation.type_selection_id.type_id, relation.type_id) + relation = relation.with_context( + active_id=self.partner_1.id, + active_ids=self.partner_1.ids, + active_model='res.partner.relation', + ) + relation.read() + domain = relation._onchange_type_selection_id()['domain'] + self.assertTrue( + ('is_company', '=', True) in domain['partner_id_display'] + ) + relation.write({ + 'type_selection_id': relation.type_selection_id.id, + }) + action = relation.get_action_related_partners() + self.assertTrue(self.partner_1.id in action['domain'][0][2]) + + def test_relation_all(self): + relation_all_record = self.env['res.partner.relation.all']\ + .with_context( + active_id=self.partner_2.id, + active_ids=self.partner_2.ids, + ).create({ + 'other_partner_id': self.partner_1.id, + 'type_selection_id': self.relation_mixed.id * 10, + }) + self.assertEqual( + relation_all_record.display_name, '%s %s %s' % ( + self.partner_2.name, + 'mixed', + self.partner_1.name, + ) + ) + + domain = relation_all_record.onchange_type_selection_id()['domain'] + self.assertTrue( + ('is_company', '=', False) in domain['other_partner_id']) + domain = relation_all_record.onchange_this_partner_id()['domain'] + self.assertTrue( + ('contact_type_this', '=', 'c') in domain['type_selection_id']) + + relation_all_record.write({ + 'type_id': self.relation_mixed.id, + }) + relation = relation_all_record.relation_id + relation_all_record.unlink() + self.assertFalse(relation.exists()) + + def test_symmetric(self): + relation = self.relation_model.create({ + 'type_id': self.relation_symmetric.id, + 'left_partner_id': self.partner_2.id, + 'right_partner_id': self.partner_1.id, + }) + partners = self.env['res.partner'].search([ + ('search_relation_id', '=', relation.type_selection_id.id) + ]) + self.assertTrue(self.partner_1 in partners) + self.assertTrue(self.partner_2 in partners) diff --git a/partner_relations/view/menu.xml b/partner_relations/views/menu.xml similarity index 100% rename from partner_relations/view/menu.xml rename to partner_relations/views/menu.xml diff --git a/partner_relations/view/res_partner.xml b/partner_relations/views/res_partner.xml similarity index 100% rename from partner_relations/view/res_partner.xml rename to partner_relations/views/res_partner.xml diff --git a/partner_relations/view/res_partner_relation.xml b/partner_relations/views/res_partner_relation.xml similarity index 100% rename from partner_relations/view/res_partner_relation.xml rename to partner_relations/views/res_partner_relation.xml diff --git a/partner_relations/view/res_partner_relation_all.xml b/partner_relations/views/res_partner_relation_all.xml similarity index 91% rename from partner_relations/view/res_partner_relation_all.xml rename to partner_relations/views/res_partner_relation_all.xml index 49a820710..58e1e7761 100644 --- a/partner_relations/view/res_partner_relation_all.xml +++ b/partner_relations/views/res_partner_relation_all.xml @@ -12,16 +12,11 @@ > - diff --git a/partner_relations/view/res_partner_relation_type.xml b/partner_relations/views/res_partner_relation_type.xml similarity index 79% rename from partner_relations/view/res_partner_relation_type.xml rename to partner_relations/views/res_partner_relation_type.xml index 415dbe154..2e4444980 100644 --- a/partner_relations/view/res_partner_relation_type.xml +++ b/partner_relations/views/res_partner_relation_type.xml @@ -2,42 +2,45 @@ res.partner.relation.type - tree - + + res.partner.relation.type - form -
+ - + + + +
From 0bdfc077f6c322de3ace363030305b580809c135 Mon Sep 17 00:00:00 2001 From: Ronald Portier Date: Thu, 18 Aug 2016 10:11:22 +0200 Subject: [PATCH 2/3] [FIX] A lot of bugfixes, logic improvements, general fiddling --- partner_relations/__openerp__.py | 22 +- partner_relations/data/demo.xml | 14 +- partner_relations/i18n/da.po | 2 +- partner_relations/i18n/de.po | 2 +- partner_relations/i18n/es.po | 2 +- partner_relations/i18n/fi.po | 2 +- partner_relations/i18n/fr.po | 2 +- partner_relations/i18n/it.po | 2 +- partner_relations/i18n/nl.po | 401 +++++++++----- partner_relations/i18n/partner_relations.pot | 297 ++++++++--- partner_relations/i18n/pt_BR.po | 2 +- partner_relations/i18n/sl.po | 2 +- partner_relations/models/res_partner.py | 170 +++--- .../models/res_partner_relation.py | 283 ++-------- .../models/res_partner_relation_all.py | 494 +++++++++++++----- .../models/res_partner_relation_type.py | 174 +++++- .../res_partner_relation_type_selection.py | 154 +++--- .../tests/test_partner_relations.py | 473 ++++++++++++----- partner_relations/views/menu.xml | 2 +- partner_relations/views/res_partner.xml | 34 +- .../views/res_partner_relation.xml | 97 ---- .../views/res_partner_relation_all.xml | 86 +-- .../views/res_partner_relation_type.xml | 12 +- 23 files changed, 1627 insertions(+), 1102 deletions(-) delete mode 100644 partner_relations/views/res_partner_relation.xml diff --git a/partner_relations/__openerp__.py b/partner_relations/__openerp__.py index 9a04dcae5..a08746048 100644 --- a/partner_relations/__openerp__.py +++ b/partner_relations/__openerp__.py @@ -1,23 +1,6 @@ # -*- coding: utf-8 -*- -############################################################################## -# -# OpenERP, Open Source Management Solution -# This module copyright (C) 2013 Therp BV (). -# -# 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 . -# -############################################################################## +# © 2013-2016 Therp BV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). { "name": "Partner relations", "version": "8.0.1.1.1", @@ -33,7 +16,6 @@ ], "data": [ "views/res_partner_relation_all.xml", - 'views/res_partner_relation.xml', 'views/res_partner.xml', 'views/res_partner_relation_type.xml', 'views/menu.xml', diff --git a/partner_relations/data/demo.xml b/partner_relations/data/demo.xml index 200310595..2710b08c5 100644 --- a/partner_relations/data/demo.xml +++ b/partner_relations/data/demo.xml @@ -2,21 +2,21 @@ - Is assistant of - Has assistant + is assistant of + has assistant p p - Is competitor of - Is competitor of + is competitor of + is competitor of c c - + - Has worked for - Has former employee + works for + has employee p c diff --git a/partner_relations/i18n/da.po b/partner_relations/i18n/da.po index 1045749bb..9ddb7d1b8 100644 --- a/partner_relations/i18n/da.po +++ b/partner_relations/i18n/da.po @@ -124,7 +124,7 @@ msgid "Has former employee" msgstr "" #. module: partner_relations -#: field:res.partner,search_relation_id:0 +#: field:res.partner,search_relation_type_id:0 msgid "Has relation of type" msgstr "" diff --git a/partner_relations/i18n/de.po b/partner_relations/i18n/de.po index 133446d03..45ac46e96 100644 --- a/partner_relations/i18n/de.po +++ b/partner_relations/i18n/de.po @@ -124,7 +124,7 @@ msgid "Has former employee" msgstr "Hat ehemaligen Mitarbeiter" #. module: partner_relations -#: field:res.partner,search_relation_id:0 +#: field:res.partner,search_relation_type_id:0 msgid "Has relation of type" msgstr "Hat Beziehungsart" diff --git a/partner_relations/i18n/es.po b/partner_relations/i18n/es.po index f1248c972..df981f1c7 100644 --- a/partner_relations/i18n/es.po +++ b/partner_relations/i18n/es.po @@ -125,7 +125,7 @@ msgid "Has former employee" msgstr "Tiene ex-empleado" #. module: partner_relations -#: field:res.partner,search_relation_id:0 +#: field:res.partner,search_relation_type_id:0 msgid "Has relation of type" msgstr "Tiene una relación de tipo" diff --git a/partner_relations/i18n/fi.po b/partner_relations/i18n/fi.po index a47c0c4b5..e180d1e6c 100644 --- a/partner_relations/i18n/fi.po +++ b/partner_relations/i18n/fi.po @@ -123,7 +123,7 @@ msgid "Has former employee" msgstr "" #. module: partner_relations -#: field:res.partner,search_relation_id:0 +#: field:res.partner,search_relation_type_id:0 msgid "Has relation of type" msgstr "" diff --git a/partner_relations/i18n/fr.po b/partner_relations/i18n/fr.po index 89e5e8e71..58479aa0a 100644 --- a/partner_relations/i18n/fr.po +++ b/partner_relations/i18n/fr.po @@ -126,7 +126,7 @@ msgid "Has former employee" msgstr "" #. module: partner_relations -#: field:res.partner,search_relation_id:0 +#: field:res.partner,search_relation_type_id:0 msgid "Has relation of type" msgstr "A une relation de type" diff --git a/partner_relations/i18n/it.po b/partner_relations/i18n/it.po index 9fdc25d33..cf3e7f1de 100644 --- a/partner_relations/i18n/it.po +++ b/partner_relations/i18n/it.po @@ -125,7 +125,7 @@ msgid "Has former employee" msgstr "" #. module: partner_relations -#: field:res.partner,search_relation_id:0 +#: field:res.partner,search_relation_type_id:0 msgid "Has relation of type" msgstr "" diff --git a/partner_relations/i18n/nl.po b/partner_relations/i18n/nl.po index 0deb6508f..159989791 100644 --- a/partner_relations/i18n/nl.po +++ b/partner_relations/i18n/nl.po @@ -1,63 +1,75 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * partner_relations -# +# # Translators: +# OCA Transbot , 2016. +# msgid "" msgstr "" "Project-Id-Version: partner-contact (8.0)\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-05-07 07:18+0000\n" -"PO-Revision-Date: 2016-05-06 15:15+0000\n" +"POT-Creation-Date: 2016-09-02 12:39+0000\n" +"PO-Revision-Date: 2016-09-02 15:03+0200\n" "Last-Translator: OCA Transbot \n" -"Language-Team: Dutch (http://www.transifex.com/oca/OCA-partner-contact-8-0/language/nl/)\n" +"Language-Team: Dutch (http://www.transifex.com/oca/OCA-partner-contact-8-0/" +"language/nl/)\n" +"Language: nl\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: nl\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Gtranslator 2.91.6\n" + +#. module: partner_relations +#: code:addons/partner_relations/models/res_partner_relation_all.py:178 +#, python-format +msgid "%s partner incompatible with relation type." +msgstr "%s relatie is onverenigbaar met gekozen connectietype." #. module: partner_relations -#: model:ir.actions.act_window,help:partner_relations.action_res_partner_relation #: model:ir.actions.act_window,help:partner_relations.action_res_partner_relation_all msgid "" "

\n" -" Record and track your partners' relations. Relations may be linked to other partners with a type either directly or inversely.\n" +" Record and track your partners' relations. Relations " +"may\n" +" be linked to other partners with a type either directly\n" +" or inversely.\n" "

\n" " " msgstr "" +"

\n" +"Onderhoud de connecties tussen uw relaties. Relaties mogen gekoppeld\n" +"worden zowel via een normale connectie, als met een omgekeerde\n" +"connectie.

" #. module: partner_relations -#: field:res.partner.relation,active:0 field:res.partner.relation.all,active:0 +#: field:res.partner.relation.all,active:0 msgid "Active" -msgstr "" +msgstr "Actief" #. module: partner_relations #: model:ir.model,name:partner_relations.model_res_partner_relation_all msgid "All (non-inverse + inverse) relations between partners" -msgstr "" +msgstr "Alle connecties tussen relaties (gewoon en omgekeerd)." #. module: partner_relations #: model:ir.model,name:partner_relations.model_res_partner_relation_type_selection msgid "All relation types" -msgstr "" +msgstr "Alle connectietypes" #. module: partner_relations #: field:res.partner,relation_all_ids:0 msgid "All relations with current partner" -msgstr "" +msgstr "Alle connecties vanuit de huidige relatie" #. module: partner_relations -#: field:res.partner.relation,allow_self:0 -#: field:res.partner.relation.type,allow_self:0 -msgid "Allow both sides to be the same" -msgstr "" - -#. module: partner_relations -#: code:addons/partner_relations/model/res_partner_relation_type.py:68 +#: code:addons/partner_relations/models/res_partner_relation_type.py:13 +#: selection:res.partner.relation.type,handle_invalid_onchange:0 #, python-format -msgid "Company" -msgstr "Bedrijf" +msgid "Allow existing relations that do not fit changed conditions" +msgstr "" +"Sta bestaande connecties toe die niet voldoen aan de gewijzigde criteria" #. module: partner_relations #: field:res.partner.relation,create_uid:0 @@ -71,26 +83,27 @@ msgstr "Aangemaakt door" msgid "Created on" msgstr "Aangemaakt op" -#. module: partner_relations -#: field:res.partner.relation.all,this_partner_id:0 -msgid "Current Partner" -msgstr "" - #. module: partner_relations #: field:res.partner.relation.type.selection,partner_category_this:0 -#: field:res.partner.relation.type.selection,search_partner_category_this:0 msgid "Current record's category" -msgstr "" +msgstr "Categorie van het huidige record" #. module: partner_relations #: field:res.partner.relation.type.selection,contact_type_this:0 msgid "Current record's partner type" -msgstr "" +msgstr "Type van de huidige relatie" + +#. module: partner_relations +#: code:addons/partner_relations/models/res_partner_relation_type.py:17 +#: selection:res.partner.relation.type,handle_invalid_onchange:0 +#, python-format +msgid "Delete relations that do not fit changed conditions" +msgstr "Verwijder connecties die niet voldoen aan de gewijzigde condities" #. module: partner_relations #: field:res.partner.relation,right_partner_id:0 msgid "Destination Partner" -msgstr "" +msgstr "Doel relatie" #. module: partner_relations #: field:res.partner.relation,display_name:0 @@ -98,49 +111,71 @@ msgstr "" #: field:res.partner.relation.type,display_name:0 #: field:res.partner.relation.type.selection,display_name:0 msgid "Display Name" +msgstr "Te tonen naam" + +#. module: partner_relations +#: code:addons/partner_relations/models/res_partner_relation_type.py:11 +#: selection:res.partner.relation.type,handle_invalid_onchange:0 +#, python-format +msgid "Do not allow change that will result in invalid relations" +msgstr "Sta geen wijziging toe die zal resulteren in ongeldige connecties" + +#. module: partner_relations +#: code:addons/partner_relations/models/res_partner_relation_type.py:15 +#: selection:res.partner.relation.type,handle_invalid_onchange:0 +#, python-format +msgid "End relations per today, if they do not fit changed conditions" msgstr "" +"Beëindig wijzigingen per vandaag indien ze niet voldoen aan de gewijzigde " +"condities" #. module: partner_relations #: field:res.partner.relation,date_end:0 #: field:res.partner.relation.all,date_end:0 msgid "Ending date" -msgstr "" +msgstr "Einddatum" + +#. module: partner_relations +#: code:addons/partner_relations/models/res_partner_relation_all.py:184 +#: code:addons/partner_relations/models/res_partner_relation_all.py:259 +#, python-format +msgid "Error!" +msgstr "Fout!" #. module: partner_relations -#: view:res.partner.relation:partner_relations.search_res_partner_relation #: view:res.partner.relation.all:partner_relations.search_res_partner_relation_all msgid "Group By" -msgstr "" +msgstr "Groepeer op" #. module: partner_relations #: model:res.partner.relation.type,name_inverse:partner_relations.rel_type_assistant msgid "Has assistant" -msgstr "" +msgstr "Heeft assistent" #. module: partner_relations #: model:res.partner.relation.type,name_inverse:partner_relations.rel_type_has_worked_for msgid "Has former employee" -msgstr "" +msgstr "Heeft voormalig werknemer" #. module: partner_relations -#: field:res.partner,search_relation_id:0 +#: field:res.partner,search_relation_type_id:0 msgid "Has relation of type" -msgstr "" +msgstr "heeft connectie van type" #. module: partner_relations #: field:res.partner,search_relation_partner_id:0 msgid "Has relation with" -msgstr "" +msgstr "Heeft connectie met" #. module: partner_relations #: field:res.partner,search_relation_partner_category_id:0 msgid "Has relation with a partner in category" -msgstr "" +msgstr "Heeft connectie met een relatie in de categorie" #. module: partner_relations #: model:res.partner.relation.type,name:partner_relations.rel_type_has_worked_for msgid "Has worked for" -msgstr "" +msgstr "Heeft gewerkt voor" #. module: partner_relations #: field:res.partner.relation,id:0 field:res.partner.relation.all,id:0 @@ -149,27 +184,41 @@ msgstr "" msgid "ID" msgstr "ID" +#. module: partner_relations +#: view:res.partner.relation.all:partner_relations.search_res_partner_relation_all +msgid "Include past records" +msgstr "Inclusief beëindigde connecties" + +#. module: partner_relations +#: field:res.partner.relation.type,handle_invalid_onchange:0 +msgid "Invalid relation handling" +msgstr "Afhandeling van ongeldige connecties" + #. module: partner_relations #: field:res.partner.relation.type,name_inverse:0 msgid "Inverse name" -msgstr "" +msgstr "Omgekeerde naam" #. module: partner_relations -#: selection:res.partner.relation.all,record_type:0 -#: selection:res.partner.relation.type.selection,record_type:0 -msgid "Inverse type" -msgstr "" +#: help:res.partner.relation.type.selection,is_inverse:0 +msgid "Inverse relations are from right to left partner." +msgstr "Omgekeerde connecties zijn vanaf de rechter relatie naar de linker." #. module: partner_relations #: model:res.partner.relation.type,name:partner_relations.rel_type_assistant msgid "Is assistant of" -msgstr "" +msgstr "Is assistent van" #. module: partner_relations #: model:res.partner.relation.type,name:partner_relations.rel_type_competitor #: model:res.partner.relation.type,name_inverse:partner_relations.rel_type_competitor msgid "Is competitor of" -msgstr "" +msgstr "Is concurrent van" + +#. module: partner_relations +#: field:res.partner.relation.type.selection,is_inverse:0 +msgid "Is reverse type?" +msgstr "Is omgekeerd type?" #. module: partner_relations #: field:res.partner.relation,__last_update:0 @@ -177,7 +226,7 @@ msgstr "" #: field:res.partner.relation.type,__last_update:0 #: field:res.partner.relation.type.selection,__last_update:0 msgid "Last Modified on" -msgstr "" +msgstr "Laatst bijgewerkt op" #. module: partner_relations #: field:res.partner.relation,write_uid:0 @@ -191,240 +240,310 @@ msgstr "Laatst bijgewerkt door" msgid "Last Updated on" msgstr "Laatst bijgewerkt op" -#. module: partner_relations -#: view:res.partner.relation:partner_relations.search_res_partner_relation -msgid "Left Partner" -msgstr "" - -#. module: partner_relations -#: field:res.partner.relation,left_contact_type:0 -msgid "Left Partner Type" -msgstr "" - #. module: partner_relations #: field:res.partner.relation.type,partner_category_left:0 msgid "Left partner category" -msgstr "" +msgstr "Categorie linkerrelatie" + +#. module: partner_relations +#: selection:res.partner.relation.all,record_type:0 +msgid "Left partner to right partner" +msgstr "Van linker naar rechterrelatie" #. module: partner_relations #: field:res.partner.relation.type,contact_type_left:0 msgid "Left partner type" -msgstr "" +msgstr "Type linkerrelatie" #. module: partner_relations #: view:res.partner.relation.type:partner_relations.form_res_partner_relation_type msgid "Left side of relation" -msgstr "" +msgstr "Linkerkant van relatie" + +#. module: partner_relations +#: view:res.partner.relation.all:partner_relations.search_res_partner_relation_all +msgid "Left to right" +msgstr "Links naar rechts" #. module: partner_relations #: field:res.partner.relation.type,name:0 #: field:res.partner.relation.type.selection,name:0 msgid "Name" -msgstr "" +msgstr "Naam" + +#. module: partner_relations +#: code:addons/partner_relations/models/res_partner_relation_all.py:181 +#, python-format +msgid "No %s partner available for relation type." +msgstr "Geen %s relatie beschikbaar voor dit connectietype." + +#. module: partner_relations +#: code:addons/partner_relations/models/res_partner_relation_all.py:256 +#, python-format +msgid "No relation type available for selected partners." +msgstr "Geen connectietype beschikbaar voor verbinden van deze relaties." + +#. module: partner_relations +#: view:res.partner.relation.all:partner_relations.search_res_partner_relation_all +#: field:res.partner.relation.all,this_partner_id:0 +msgid "One Partner" +msgstr "De ene relatie" + +#. module: partner_relations +#: code:addons/partner_relations/models/res_partner_relation_type.py:82 +#, python-format +msgid "Organisation" +msgstr "Organisatie" #. module: partner_relations #: view:res.partner.relation.all:partner_relations.search_res_partner_relation_all #: field:res.partner.relation.all,other_partner_id:0 msgid "Other Partner" -msgstr "" +msgstr "De andere relatie" #. module: partner_relations #: field:res.partner.relation.type.selection,partner_category_other:0 msgid "Other record's category" -msgstr "" +msgstr "Categorie andere relatie" #. module: partner_relations #: field:res.partner.relation.type.selection,contact_type_other:0 msgid "Other record's partner type" -msgstr "" +msgstr "Type andere relatie" #. module: partner_relations #: model:ir.model,name:partner_relations.model_res_partner -#: field:res.partner.relation,any_partner_id:0 -#: field:res.partner.relation,partner_id_display:0 +#: field:res.partner.relation.all,any_partner_id:0 msgid "Partner" -msgstr "Partner" - -#. module: partner_relations -#: view:res.partner.relation:partner_relations.form_res_partner_relation -msgid "Partner Relation" -msgstr "" +msgstr "Relatie" #. module: partner_relations #: model:ir.model,name:partner_relations.model_res_partner_relation_type msgid "Partner Relation Type" -msgstr "" +msgstr "Type connectie" #. module: partner_relations -#: view:res.partner.relation:partner_relations.tree_res_partner_relation #: view:res.partner.relation.all:partner_relations.tree_res_partner_relation_all msgid "Partner Relations" -msgstr "" +msgstr "Connecties" #. module: partner_relations #: model:ir.actions.act_window,name:partner_relations.action_res_partner_relation_type #: model:ir.ui.menu,name:partner_relations.menu_res_partner_relation_type msgid "Partner Relations Types" -msgstr "" - -#. module: partner_relations -#: field:res.partner.relation.all,contact_type:0 -msgid "Partner Type" -msgstr "" +msgstr "Connectietypes" #. module: partner_relations #: model:ir.model,name:partner_relations.model_res_partner_relation -#: view:res.partner.relation.all:partner_relations.form_res_partner_relation_all -#: view:res.partner.relation.type:partner_relations.form_res_partner_relation_type -#: view:res.partner.relation.type:partner_relations.tree_res_partner_relation_type msgid "Partner relation" -msgstr "" +msgstr "Connectie" #. module: partner_relations -#: code:addons/partner_relations/model/res_partner_relation.py:301 +#: code:addons/partner_relations/models/res_partner_relation.py:115 #, python-format msgid "Partners cannot have a relation with themselves." -msgstr "" +msgstr "Relaties kunnen geen connectie met zichzelf hebben" #. module: partner_relations -#: code:addons/partner_relations/model/res_partner_relation_type.py:69 +#: code:addons/partner_relations/models/res_partner_relation_type.py:83 #, python-format msgid "Person" -msgstr "" +msgstr "Persoon" + +#. module: partner_relations +#: view:res.partner.relation.type:partner_relations.form_res_partner_relation_type +msgid "Properties" +msgstr "Eigenschappen" #. module: partner_relations #: field:res.partner.relation.all,record_type:0 msgid "Record Type" -msgstr "" +msgstr "Recordtype" #. module: partner_relations -#: field:res.partner.relation.type.selection,record_type:0 -msgid "Record type" -msgstr "" +#: help:res.partner.relation.all,active:0 +msgid "Records with date_end in the past are inactive" +msgstr "Connecties met een datum in het verleden zijn inactief" #. module: partner_relations -#: code:addons/partner_relations/model/res_partner_relation.py:370 -#, python-format -msgid "Related partners" -msgstr "" +#: field:res.partner.relation.type,allow_self:0 +#: field:res.partner.relation.type.selection,allow_self:0 +msgid "Reflexive" +msgstr "Wederkerig" #. module: partner_relations #: field:res.partner.relation.all,relation_id:0 msgid "Relation" -msgstr "" +msgstr "Connectie" #. module: partner_relations #: field:res.partner,relation_count:0 msgid "Relation Count" -msgstr "" +msgstr "Aantal connecties" #. module: partner_relations -#: field:res.partner.relation.all,type_id:0 #: field:res.partner.relation.all,type_selection_id:0 msgid "Relation Type" -msgstr "" +msgstr "Type connectie" + +#. module: partner_relations +#: code:addons/partner_relations/models/res_partner_relation_all.py:252 +#, python-format +msgid "Relation type incompatible with selected partner(s)." +msgstr "Type connectie komt niet overeen met geselecteerde relatie(s)." #. module: partner_relations #: field:res.partner,search_relation_date:0 msgid "Relation valid" -msgstr "" +msgstr "Geldige connectie" #. module: partner_relations -#: model:ir.actions.act_window,name:partner_relations.action_res_partner_relation #: model:ir.actions.act_window,name:partner_relations.action_res_partner_relation_all #: model:ir.ui.menu,name:partner_relations.menu_res_partner_relation_sales #: view:res.partner:partner_relations.view_partner_form -#: field:res.partner,relation_ids:0 msgid "Relations" -msgstr "" +msgstr "Connecties" #. module: partner_relations -#: view:res.partner.relation:partner_relations.search_res_partner_relation #: view:res.partner.relation.all:partner_relations.search_res_partner_relation_all msgid "Relationship Type" -msgstr "" - -#. module: partner_relations -#: view:res.partner.relation:partner_relations.search_res_partner_relation -msgid "Right Partner" -msgstr "" - -#. module: partner_relations -#: field:res.partner.relation,right_contact_type:0 -msgid "Right Partner Type" -msgstr "" +msgstr "Type connectie" #. module: partner_relations #: field:res.partner.relation.type,partner_category_right:0 msgid "Right partner category" -msgstr "" +msgstr "Categorie rechterrelatie" + +#. module: partner_relations +#: selection:res.partner.relation.all,record_type:0 +msgid "Right partner to left partner" +msgstr "Rechter naar linkerrelatie" #. module: partner_relations #: field:res.partner.relation.type,contact_type_right:0 msgid "Right partner type" -msgstr "" +msgstr "Type rechterrelatie" #. module: partner_relations #: view:res.partner.relation.type:partner_relations.form_res_partner_relation_type msgid "Right side of relation" -msgstr "" +msgstr "Rechterkant van de connectie" + +#. module: partner_relations +#: view:res.partner.relation.all:partner_relations.search_res_partner_relation_all +msgid "Right to left" +msgstr "Rechts naar links" #. module: partner_relations -#: view:res.partner.relation:partner_relations.search_res_partner_relation #: view:res.partner.relation.all:partner_relations.search_res_partner_relation_all msgid "Search Relations" -msgstr "" +msgstr "Zoek connecties" #. module: partner_relations #: model:ir.actions.act_window,name:partner_relations.action_show_partner_relations msgid "Show partner's relations" -msgstr "" - -#. module: partner_relations -#: model:ir.actions.server,name:partner_relations.action_show_right_relation_partners -msgid "Show partners" -msgstr "" +msgstr "Toon connecties van relatie" #. module: partner_relations #: field:res.partner.relation,left_partner_id:0 msgid "Source Partner" -msgstr "" +msgstr "Bron relatie" #. module: partner_relations #: field:res.partner.relation,date_start:0 #: field:res.partner.relation.all,date_start:0 msgid "Starting date" -msgstr "" +msgstr "Datum ingang" #. module: partner_relations -#: code:addons/partner_relations/model/res_partner_relation.py:287 +#: field:res.partner.relation.type,is_symmetric:0 +#: field:res.partner.relation.type.selection,is_symmetric:0 +msgid "Symmetric" +msgstr "Symmetrisch" + +#. module: partner_relations +#: code:addons/partner_relations/models/res_partner_relation.py:101 +#, fuzzy, python-format +msgid "The %s partner does not have category %s." +msgstr "De %s relatie is niet geldig voor dit type connectie." + +#. module: partner_relations +#: code:addons/partner_relations/models/res_partner_relation.py:95 #, python-format msgid "The %s partner is not applicable for this relation type." -msgstr "" +msgstr "De %s relatie is niet geldig voor dit type connectie." #. module: partner_relations -#: code:addons/partner_relations/model/res_partner_relation.py:254 +#: code:addons/partner_relations/models/res_partner_relation.py:61 #, python-format msgid "The starting date cannot be after the ending date." +msgstr "De ingangsdatum kan niet na de einddatum liggen." + +#. module: partner_relations +#: code:addons/partner_relations/models/res_partner_relation_type.py:168 +#, python-format +msgid "" +"There are already relations not satisfying the conditions for partner type " +"or category." msgstr "" +"Er zijn al connecties die niet voldoen aan de criteria voor type relatie of " +"categorie." #. module: partner_relations -#: code:addons/partner_relations/model/res_partner_relation.py:329 +#: code:addons/partner_relations/models/res_partner_relation.py:154 #, python-format msgid "There is already a similar relation with overlapping dates" +msgstr "Er is al een gelijkaardige connectie met overlappende geldigheid" + +#. module: partner_relations +#: help:res.partner.relation.type,allow_self:0 +msgid "This relation can be set up with the same partner left and right" +msgstr "Deze connectie kan een relatie met zichzelf verbinden" + +#. module: partner_relations +#: help:res.partner.relation.type,is_symmetric:0 +msgid "This relation is the same from right to left as from left to right" msgstr "" +"Deze connectie is van rechts naar links hetzelfde als van links naar rechts" #. module: partner_relations #: field:res.partner.relation,type_id:0 -#: field:res.partner.relation,type_selection_id:0 -#: selection:res.partner.relation.all,record_type:0 -#: selection:res.partner.relation.type.selection,record_type:0 #: field:res.partner.relation.type.selection,type_id:0 msgid "Type" -msgstr "" +msgstr "Type" #. module: partner_relations -#: code:addons/partner_relations/model/res_partner.py:122 +#: code:addons/partner_relations/models/res_partner.py:80 +#: code:addons/partner_relations/models/res_partner.py:121 #, python-format -msgid "Unsupported search operand \"%s\"" +msgid "Unsupported search operator \"%s\"" +msgstr "Zoek operator \"%s\" wordt niet ondersteund " + +#. module: partner_relations +#: help:res.partner.relation.type,handle_invalid_onchange:0 +msgid "" +"When adding relations criteria like partner type and category are checked.\n" +"However when you change the criteria, there might be relations that do not " +"fit the new criteria.\n" +"Specify how this situation should be handled." msgstr "" +"Bij het aanmaken van connecties vinden controles plaats op type en categorie " +"van de relatie.\n" +"Echter, wanneer u de criteria verandert, dan kunnen er al connecties bestaan " +"die daar niet aan voldoen.\n" +"Geef aan hoe zo'n situatie moet worden afgehandeld. " + +#. module: partner_relations +#: code:addons/partner_relations/models/res_partner_relation_all.py:221 +#, python-format +msgid "other" +msgstr "andere" + +#. module: partner_relations +#: code:addons/partner_relations/models/res_partner_relation_all.py:215 +#, python-format +msgid "this" +msgstr "deze" + +#~ msgid "Company" +#~ msgstr "Bedrijf" diff --git a/partner_relations/i18n/partner_relations.pot b/partner_relations/i18n/partner_relations.pot index 9b700725f..6bb4b07ea 100644 --- a/partner_relations/i18n/partner_relations.pot +++ b/partner_relations/i18n/partner_relations.pot @@ -6,8 +6,8 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 8.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2015-04-15 16:09+0000\n" -"PO-Revision-Date: 2015-04-15 16:09+0000\n" +"POT-Creation-Date: 2016-09-02 12:39+0000\n" +"PO-Revision-Date: 2016-09-02 12:39+0000\n" "Last-Translator: <>\n" "Language-Team: \n" "MIME-Version: 1.0\n" @@ -16,16 +16,22 @@ msgstr "" "Plural-Forms: \n" #. module: partner_relations -#: model:ir.actions.act_window,help:partner_relations.action_res_partner_relation +#: code:addons/partner_relations/models/res_partner_relation_all.py:178 +#, python-format +msgid "%s partner incompatible with relation type." +msgstr "" + +#. module: partner_relations #: model:ir.actions.act_window,help:partner_relations.action_res_partner_relation_all msgid "

\n" -" Record and track your partners' relations. Relations may be linked to other partners with a type either directly or inversely.\n" +" Record and track your partners' relations. Relations may\n" +" be linked to other partners with a type either directly\n" +" or inversely.\n" "

\n" " " msgstr "" #. module: partner_relations -#: field:res.partner.relation,active:0 #: field:res.partner.relation.all,active:0 msgid "Active" msgstr "" @@ -46,9 +52,10 @@ msgid "All relations with current partner" msgstr "" #. module: partner_relations -#: code:addons/partner_relations/model/res_partner_relation_type.py:64 +#: code:addons/partner_relations/models/res_partner_relation_type.py:13 +#: selection:res.partner.relation.type,handle_invalid_onchange:0 #, python-format -msgid "Company" +msgid "Allow existing relations that do not fit changed conditions" msgstr "" #. module: partner_relations @@ -63,14 +70,8 @@ msgstr "" msgid "Created on" msgstr "" -#. module: partner_relations -#: field:res.partner.relation.all,this_partner_id:0 -msgid "Current Partner" -msgstr "" - #. module: partner_relations #: field:res.partner.relation.type.selection,partner_category_this:0 -#: field:res.partner.relation.type.selection,search_partner_category_this:0 msgid "Current record's category" msgstr "" @@ -79,11 +80,40 @@ msgstr "" msgid "Current record's partner type" msgstr "" +#. module: partner_relations +#: code:addons/partner_relations/models/res_partner_relation_type.py:17 +#: selection:res.partner.relation.type,handle_invalid_onchange:0 +#, python-format +msgid "Delete relations that do not fit changed conditions" +msgstr "" + #. module: partner_relations #: field:res.partner.relation,right_partner_id:0 msgid "Destination Partner" msgstr "" +#. module: partner_relations +#: field:res.partner.relation,display_name:0 +#: field:res.partner.relation.all,display_name:0 +#: field:res.partner.relation.type,display_name:0 +#: field:res.partner.relation.type.selection,display_name:0 +msgid "Display Name" +msgstr "" + +#. module: partner_relations +#: code:addons/partner_relations/models/res_partner_relation_type.py:11 +#: selection:res.partner.relation.type,handle_invalid_onchange:0 +#, python-format +msgid "Do not allow change that will result in invalid relations" +msgstr "" + +#. module: partner_relations +#: code:addons/partner_relations/models/res_partner_relation_type.py:15 +#: selection:res.partner.relation.type,handle_invalid_onchange:0 +#, python-format +msgid "End relations per today, if they do not fit changed conditions" +msgstr "" + #. module: partner_relations #: field:res.partner.relation,date_end:0 #: field:res.partner.relation.all,date_end:0 @@ -91,13 +121,29 @@ msgid "Ending date" msgstr "" #. module: partner_relations -#: view:res.partner.relation:partner_relations.search_res_partner_relation +#: code:addons/partner_relations/models/res_partner_relation_all.py:184 +#: code:addons/partner_relations/models/res_partner_relation_all.py:259 +#, python-format +msgid "Error!" +msgstr "" + +#. module: partner_relations #: view:res.partner.relation.all:partner_relations.search_res_partner_relation_all msgid "Group By" msgstr "" #. module: partner_relations -#: field:res.partner,search_relation_id:0 +#: model:res.partner.relation.type,name_inverse:partner_relations.rel_type_assistant +msgid "Has assistant" +msgstr "" + +#. module: partner_relations +#: model:res.partner.relation.type,name_inverse:partner_relations.rel_type_has_worked_for +msgid "Has former employee" +msgstr "" + +#. module: partner_relations +#: field:res.partner,search_relation_type_id:0 msgid "Has relation of type" msgstr "" @@ -111,6 +157,11 @@ msgstr "" msgid "Has relation with a partner in category" msgstr "" +#. module: partner_relations +#: model:res.partner.relation.type,name:partner_relations.rel_type_has_worked_for +msgid "Has worked for" +msgstr "" + #. module: partner_relations #: field:res.partner.relation,id:0 #: field:res.partner.relation.all,id:0 @@ -119,15 +170,48 @@ msgstr "" msgid "ID" msgstr "" +#. module: partner_relations +#: view:res.partner.relation.all:partner_relations.search_res_partner_relation_all +msgid "Include past records" +msgstr "" + +#. module: partner_relations +#: field:res.partner.relation.type,handle_invalid_onchange:0 +msgid "Invalid relation handling" +msgstr "" + #. module: partner_relations #: field:res.partner.relation.type,name_inverse:0 msgid "Inverse name" msgstr "" #. module: partner_relations -#: selection:res.partner.relation.all,record_type:0 -#: selection:res.partner.relation.type.selection,record_type:0 -msgid "Inverse type" +#: help:res.partner.relation.type.selection,is_inverse:0 +msgid "Inverse relations are from right to left partner." +msgstr "" + +#. module: partner_relations +#: model:res.partner.relation.type,name:partner_relations.rel_type_assistant +msgid "Is assistant of" +msgstr "" + +#. module: partner_relations +#: model:res.partner.relation.type,name:partner_relations.rel_type_competitor +#: model:res.partner.relation.type,name_inverse:partner_relations.rel_type_competitor +msgid "Is competitor of" +msgstr "" + +#. module: partner_relations +#: field:res.partner.relation.type.selection,is_inverse:0 +msgid "Is reverse type?" +msgstr "" + +#. module: partner_relations +#: field:res.partner.relation,__last_update:0 +#: field:res.partner.relation.all,__last_update:0 +#: field:res.partner.relation.type,__last_update:0 +#: field:res.partner.relation.type.selection,__last_update:0 +msgid "Last Modified on" msgstr "" #. module: partner_relations @@ -143,18 +227,13 @@ msgid "Last Updated on" msgstr "" #. module: partner_relations -#: view:res.partner.relation:partner_relations.search_res_partner_relation -msgid "Left Partner" -msgstr "" - -#. module: partner_relations -#: field:res.partner.relation,left_contact_type:0 -msgid "Left Partner Type" +#: field:res.partner.relation.type,partner_category_left:0 +msgid "Left partner category" msgstr "" #. module: partner_relations -#: field:res.partner.relation.type,partner_category_left:0 -msgid "Left partner category" +#: selection:res.partner.relation.all,record_type:0 +msgid "Left partner to right partner" msgstr "" #. module: partner_relations @@ -167,12 +246,41 @@ msgstr "" msgid "Left side of relation" msgstr "" +#. module: partner_relations +#: view:res.partner.relation.all:partner_relations.search_res_partner_relation_all +msgid "Left to right" +msgstr "" + #. module: partner_relations #: field:res.partner.relation.type,name:0 #: field:res.partner.relation.type.selection,name:0 msgid "Name" msgstr "" +#. module: partner_relations +#: code:addons/partner_relations/models/res_partner_relation_all.py:181 +#, python-format +msgid "No %s partner available for relation type." +msgstr "" + +#. module: partner_relations +#: code:addons/partner_relations/models/res_partner_relation_all.py:256 +#, python-format +msgid "No relation type available for selected partners." +msgstr "" + +#. module: partner_relations +#: view:res.partner.relation.all:partner_relations.search_res_partner_relation_all +#: field:res.partner.relation.all,this_partner_id:0 +msgid "One Partner" +msgstr "" + +#. module: partner_relations +#: code:addons/partner_relations/models/res_partner_relation_type.py:82 +#, python-format +msgid "Organisation" +msgstr "" + #. module: partner_relations #: view:res.partner.relation.all:partner_relations.search_res_partner_relation_all #: field:res.partner.relation.all,other_partner_id:0 @@ -191,22 +299,16 @@ msgstr "" #. module: partner_relations #: model:ir.model,name:partner_relations.model_res_partner -#: field:res.partner.relation,partner_id_display:0 +#: field:res.partner.relation.all,any_partner_id:0 msgid "Partner" msgstr "" -#. module: partner_relations -#: view:res.partner.relation:partner_relations.form_res_partner_relation -msgid "Partner Relation" -msgstr "" - #. module: partner_relations #: model:ir.model,name:partner_relations.model_res_partner_relation_type msgid "Partner Relation Type" msgstr "" #. module: partner_relations -#: view:res.partner.relation:partner_relations.tree_res_partner_relation #: view:res.partner.relation.all:partner_relations.tree_res_partner_relation_all msgid "Partner Relations" msgstr "" @@ -217,45 +319,42 @@ msgstr "" msgid "Partner Relations Types" msgstr "" -#. module: partner_relations -#: field:res.partner.relation.all,contact_type:0 -msgid "Partner Type" -msgstr "" - #. module: partner_relations #: model:ir.model,name:partner_relations.model_res_partner_relation -#: view:res.partner.relation.all:partner_relations.form_res_partner_relation_all -#: view:res.partner.relation.type:partner_relations.form_res_partner_relation_type -#: view:res.partner.relation.type:partner_relations.tree_res_partner_relation_type msgid "Partner relation" msgstr "" #. module: partner_relations -#: code:addons/partner_relations/model/res_partner_relation.py:291 +#: code:addons/partner_relations/models/res_partner_relation.py:115 #, python-format msgid "Partners cannot have a relation with themselves." msgstr "" #. module: partner_relations -#: code:addons/partner_relations/model/res_partner_relation_type.py:65 +#: code:addons/partner_relations/models/res_partner_relation_type.py:83 #, python-format msgid "Person" msgstr "" +#. module: partner_relations +#: view:res.partner.relation.type:partner_relations.form_res_partner_relation_type +msgid "Properties" +msgstr "" + #. module: partner_relations #: field:res.partner.relation.all,record_type:0 msgid "Record Type" msgstr "" #. module: partner_relations -#: field:res.partner.relation.type.selection,record_type:0 -msgid "Record type" +#: help:res.partner.relation.all,active:0 +msgid "Records with date_end in the past are inactive" msgstr "" #. module: partner_relations -#: code:addons/partner_relations/model/res_partner_relation.py:360 -#, python-format -msgid "Related partners" +#: field:res.partner.relation.type,allow_self:0 +#: field:res.partner.relation.type.selection,allow_self:0 +msgid "Reflexive" msgstr "" #. module: partner_relations @@ -264,44 +363,46 @@ msgid "Relation" msgstr "" #. module: partner_relations -#: field:res.partner.relation.all,type_id:0 +#: field:res.partner,relation_count:0 +msgid "Relation Count" +msgstr "" + +#. module: partner_relations #: field:res.partner.relation.all,type_selection_id:0 msgid "Relation Type" msgstr "" +#. module: partner_relations +#: code:addons/partner_relations/models/res_partner_relation_all.py:252 +#, python-format +msgid "Relation type incompatible with selected partner(s)." +msgstr "" + #. module: partner_relations #: field:res.partner,search_relation_date:0 msgid "Relation valid" msgstr "" #. module: partner_relations -#: model:ir.actions.act_window,name:partner_relations.action_res_partner_relation #: model:ir.actions.act_window,name:partner_relations.action_res_partner_relation_all #: model:ir.ui.menu,name:partner_relations.menu_res_partner_relation_sales #: view:res.partner:partner_relations.view_partner_form -#: field:res.partner,relation_ids:0 msgid "Relations" msgstr "" #. module: partner_relations -#: view:res.partner.relation:partner_relations.search_res_partner_relation #: view:res.partner.relation.all:partner_relations.search_res_partner_relation_all msgid "Relationship Type" msgstr "" #. module: partner_relations -#: view:res.partner.relation:partner_relations.search_res_partner_relation -msgid "Right Partner" -msgstr "" - -#. module: partner_relations -#: field:res.partner.relation,right_contact_type:0 -msgid "Right Partner Type" +#: field:res.partner.relation.type,partner_category_right:0 +msgid "Right partner category" msgstr "" #. module: partner_relations -#: field:res.partner.relation.type,partner_category_right:0 -msgid "Right partner category" +#: selection:res.partner.relation.all,record_type:0 +msgid "Right partner to left partner" msgstr "" #. module: partner_relations @@ -315,19 +416,18 @@ msgid "Right side of relation" msgstr "" #. module: partner_relations -#: view:res.partner.relation:partner_relations.search_res_partner_relation #: view:res.partner.relation.all:partner_relations.search_res_partner_relation_all -msgid "Search Relations" +msgid "Right to left" msgstr "" #. module: partner_relations -#: model:ir.actions.act_window,name:partner_relations.action_show_partner_relations -msgid "Show partner's relations" +#: view:res.partner.relation.all:partner_relations.search_res_partner_relation_all +msgid "Search Relations" msgstr "" #. module: partner_relations -#: model:ir.actions.server,name:partner_relations.action_show_right_relation_partners -msgid "Show partners" +#: model:ir.actions.act_window,name:partner_relations.action_show_partner_relations +msgid "Show partner's relations" msgstr "" #. module: partner_relations @@ -342,35 +442,80 @@ msgid "Starting date" msgstr "" #. module: partner_relations -#: code:addons/partner_relations/model/res_partner_relation.py:278 +#: field:res.partner.relation.type,is_symmetric:0 +#: field:res.partner.relation.type.selection,is_symmetric:0 +msgid "Symmetric" +msgstr "" + +#. module: partner_relations +#: code:addons/partner_relations/models/res_partner_relation.py:101 +#, python-format +msgid "The %s partner does not have category %s." +msgstr "" + +#. module: partner_relations +#: code:addons/partner_relations/models/res_partner_relation.py:95 #, python-format msgid "The %s partner is not applicable for this relation type." msgstr "" #. module: partner_relations -#: code:addons/partner_relations/model/res_partner_relation.py:245 +#: code:addons/partner_relations/models/res_partner_relation.py:61 #, python-format msgid "The starting date cannot be after the ending date." msgstr "" #. module: partner_relations -#: code:addons/partner_relations/model/res_partner_relation.py:319 +#: code:addons/partner_relations/models/res_partner_relation_type.py:168 +#, python-format +msgid "There are already relations not satisfying the conditions for partner type or category." +msgstr "" + +#. module: partner_relations +#: code:addons/partner_relations/models/res_partner_relation.py:154 #, python-format msgid "There is already a similar relation with overlapping dates" msgstr "" +#. module: partner_relations +#: help:res.partner.relation.type,allow_self:0 +msgid "This relation can be set up with the same partner left and right" +msgstr "" + +#. module: partner_relations +#: help:res.partner.relation.type,is_symmetric:0 +msgid "This relation is the same from right to left as from left to right" +msgstr "" + #. module: partner_relations #: field:res.partner.relation,type_id:0 -#: field:res.partner.relation,type_selection_id:0 -#: selection:res.partner.relation.all,record_type:0 -#: selection:res.partner.relation.type.selection,record_type:0 #: field:res.partner.relation.type.selection,type_id:0 msgid "Type" msgstr "" #. module: partner_relations -#: code:addons/partner_relations/model/res_partner.py:109 +#: code:addons/partner_relations/models/res_partner.py:80 +#: code:addons/partner_relations/models/res_partner.py:121 +#, python-format +msgid "Unsupported search operator \"%s\"" +msgstr "" + +#. module: partner_relations +#: help:res.partner.relation.type,handle_invalid_onchange:0 +msgid "When adding relations criteria like partner type and category are checked.\n" +"However when you change the criteria, there might be relations that do not fit the new criteria.\n" +"Specify how this situation should be handled." +msgstr "" + +#. module: partner_relations +#: code:addons/partner_relations/models/res_partner_relation_all.py:221 +#, python-format +msgid "other" +msgstr "" + +#. module: partner_relations +#: code:addons/partner_relations/models/res_partner_relation_all.py:215 #, python-format -msgid "Unsupported search operand \"%s\"" +msgid "this" msgstr "" diff --git a/partner_relations/i18n/pt_BR.po b/partner_relations/i18n/pt_BR.po index f1b3c44a5..4f78272a3 100644 --- a/partner_relations/i18n/pt_BR.po +++ b/partner_relations/i18n/pt_BR.po @@ -125,7 +125,7 @@ msgid "Has former employee" msgstr "Tem ex-empregado" #. module: partner_relations -#: field:res.partner,search_relation_id:0 +#: field:res.partner,search_relation_type_id:0 msgid "Has relation of type" msgstr "Tem relação do tipo" diff --git a/partner_relations/i18n/sl.po b/partner_relations/i18n/sl.po index 39f3694ac..0f4fc9b64 100644 --- a/partner_relations/i18n/sl.po +++ b/partner_relations/i18n/sl.po @@ -124,7 +124,7 @@ msgid "Has former employee" msgstr "Ima bivšega zaposlenega" #. module: partner_relations -#: field:res.partner,search_relation_id:0 +#: field:res.partner,search_relation_type_id:0 msgid "Has relation of type" msgstr "Ima odnos tipa" diff --git a/partner_relations/models/res_partner.py b/partner_relations/models/res_partner.py index 590aeab96..1833c1498 100644 --- a/partner_relations/models/res_partner.py +++ b/partner_relations/models/res_partner.py @@ -1,144 +1,124 @@ # -*- coding: utf-8 -*- # © 2013-2016 Therp BV # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +"""Support connections between partners.""" import numbers -from openerp import _, models, fields, exceptions, api -from openerp.osv.expression import is_leaf, OR, FALSE_LEAF -PADDING = 10 +from openerp import _, api, exceptions, fields, models +from openerp.osv.expression import is_leaf, OR, FALSE_LEAF class ResPartner(models.Model): + """Extend partner with relations and allow to search for relations + in various ways. + """ + # pylint: disable=invalid-name + # pylint: disable=no-member _inherit = 'res.partner' relation_count = fields.Integer( - 'Relation Count', + string='Relation Count', compute="_compute_relation_count" ) - - relation_ids = fields.One2many( - 'res.partner.relation', string='Relations', - compute='_compute_relation_ids', - selectable=False, - ) - relation_all_ids = fields.One2many( - 'res.partner.relation.all', 'this_partner_id', + comodel_name='res.partner.relation.all', + inverse_name='this_partner_id', string='All relations with current partner', - auto_join=True, selectable=False, copy=False, + auto_join=True, + selectable=False, + copy=False, ) - - search_relation_id = fields.Many2one( - 'res.partner.relation.type.selection', compute=lambda self: None, - search='_search_relation_id', string='Has relation of type', + search_relation_type_id = fields.Many2one( + comodel_name='res.partner.relation.type.selection', + compute=lambda self: None, + search='_search_relation_type_id', + string='Has relation of type', ) - search_relation_partner_id = fields.Many2one( - 'res.partner', compute=lambda self: None, - search='_search_related_partner_id', string='Has relation with', + comodel_name='res.partner', + compute=lambda self: None, + search='_search_related_partner_id', + string='Has relation with', ) - search_relation_date = fields.Date( - compute=lambda self: None, search='_search_relation_date', + compute=lambda self: None, + search='_search_relation_date', string='Relation valid', ) - search_relation_partner_category_id = fields.Many2one( - 'res.partner.category', compute=lambda self: None, + comodel_name='res.partner.category', + compute=lambda self: None, search='_search_related_partner_category_id', string='Has relation with a partner in category', ) - @api.one - @api.depends("relation_ids") + @api.depends("relation_all_ids") def _compute_relation_count(self): """Count the number of relations this partner has for Smart Button Don't count inactive relations. """ - self.relation_count = len([r for r in self.relation_ids if r.active]) - - @api.multi - def _compute_relation_ids(self): - '''getter for relation_ids''' - self.env.cr.execute( - "select p.id, array_agg(r.id) " - "from res_partner p join res_partner_relation r " - "on r.left_partner_id=p.id or r.right_partner_id=p.id " - "where p.id in %s " - "group by p.id", - (tuple(self.ids),) - ) - partner2relation = dict(self.env.cr.fetchall()) - for this in self: - this.relation_ids += self.env['res.partner.relation'].browse( - partner2relation.get(this.id, []), - ) + for rec in self: + rec.relation_count = len(rec.relation_all_ids.filtered('active')) @api.model - def _search_relation_id(self, operator, value): + def _search_relation_type_id(self, operator, value): + """Search partners based on their type of relations.""" result = [] - - if operator not in [ - '=', '!=', 'like', 'not like', 'ilike', 'not ilike', 'in', 'not in' - ]: + SUPPORTED_OPERATORS = ( + '=', + '!=', + 'like', + 'not like', + 'ilike', + 'not ilike', + 'in', + 'not in', + ) + if operator not in SUPPORTED_OPERATORS: raise exceptions.ValidationError( _('Unsupported search operator "%s"') % operator) - + type_selection_model = self.env['res.partner.relation.type.selection'] relation_type_selection = [] - if operator == '=' and isinstance(value, numbers.Integral): - relation_type_selection += self\ - .env['res.partner.relation.type.selection']\ - .browse(value) + relation_type_selection += type_selection_model.browse(value) elif operator == '!=' and isinstance(value, numbers.Integral): - relation_type_selection = self\ - .env['res.partner.relation.type.selection']\ - .search([ - ('id', operator, value), - ]) + relation_type_selection = type_selection_model.search([ + ('id', operator, value), + ]) else: - relation_type_selection = self\ - .env['res.partner.relation.type.selection']\ - .search([ - ('type_id.name', operator, value), - ]) - + relation_type_selection = type_selection_model.search([ + '|', + ('type_id.name', operator, value), + ('type_id.name_inverse', operator, value), + ]) if not relation_type_selection: result = [FALSE_LEAF] - for relation_type in relation_type_selection: - type_id, is_inverse = relation_type.get_type_from_selection_id() - result = OR([ result, [ - '&', - ('relation_all_ids.type_id', '=', type_id), - ( - 'relation_all_ids.record_type', 'in', - ['a', 'b'] - if relation_type.type_id.symmetric - else - (['b'] if is_inverse else ['a']) - ) + ('relation_all_ids.type_selection_id.id', '=', + relation_type.id), ], ]) - return result @api.model def _search_related_partner_id(self, operator, value): + """Find partner based on relation with other partner.""" + # pylint: disable=no-self-use return [ ('relation_all_ids.other_partner_id', operator, value), ] @api.model def _search_relation_date(self, operator, value): + """Look only for relations valid at date of search.""" + # pylint: disable=no-self-use if operator != '=': raise exceptions.ValidationError( _('Unsupported search operator "%s"') % operator) - return [ '&', '|', @@ -151,14 +131,19 @@ class ResPartner(models.Model): @api.model def _search_related_partner_category_id(self, operator, value): + """Search for partner related to a partner with search category.""" + # pylint: disable=no-self-use return [ ('relation_all_ids.other_partner_id.category_id', operator, value), ] @api.model def search(self, args, offset=0, limit=None, order=None, count=False): - # inject searching for current relation date if we search for relation - # properties and no explicit date was given + """Inject searching for current relation date if we search for + relation properties and no explicit date was given. + """ + # pylint: disable=arguments-differ + # pylint: disable=no-value-for-parameter date_args = [] for arg in args: if is_leaf(arg) and arg[0].startswith('search_relation'): @@ -169,7 +154,6 @@ class ResPartner(models.Model): date_args = [ ('search_relation_date', '=', fields.Date.today()), ] - # because of auto_join, we have to do the active test by hand active_args = [] if self.env.context.get('active_test', True): @@ -177,32 +161,10 @@ class ResPartner(models.Model): if is_leaf(arg) and arg[0].startswith('search_relation'): active_args = [('relation_all_ids.active', '=', True)] break - return super(ResPartner, self).search( args + date_args + active_args, offset=offset, limit=limit, order=order, count=count) - @api.multi - def read(self, fields=None, load='_classic_read'): - return super(ResPartner, self.with_partner_relations_context())\ - .read(fields=fields, load=load) - - @api.multi - def write(self, vals): - return super(ResPartner, self.with_partner_relations_context())\ - .write(vals) - - @api.multi - def with_partner_relations_context(self): - context = dict(self.env.context) - if context.get('active_model', self._name) == self._name: - existing = self.exists() - context.setdefault( - 'active_id', existing.ids[0] if existing.ids else None) - context.setdefault('active_ids', existing.ids) - context.setdefault('active_model', self._name) - return self.with_context(context) - @api.multi def get_partner_type(self): """Get partner type for relation. diff --git a/partner_relations/models/res_partner_relation.py b/partner_relations/models/res_partner_relation.py index a898c750f..2ccf0facd 100644 --- a/partner_relations/models/res_partner_relation.py +++ b/partner_relations/models/res_partner_relation.py @@ -1,208 +1,51 @@ # -*- coding: utf-8 -*- # © 2013-2016 Therp BV # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -from openerp import models, fields, api, exceptions, _ -from openerp.osv.expression import FALSE_LEAF -from .res_partner import PADDING +"""Store relations (connections) between partners.""" +from openerp import _, api, exceptions, fields, models class ResPartnerRelation(models.Model): - '''Model res.partner.relation is used to describe all links or relations + """Model res.partner.relation is used to describe all links or relations between partners in the database. - In many parts of the code we have to know whether the active partner is - the left partner, or the right partner. If the active partner is the - right partner we have to show the inverse name. - - Because the active partner is crucial for the working of partner - relationships, we make sure on the res.partner model that the partner id - is set in the context where needed. - ''' + This model is actually only used to store the data. The model + res.partner.relation.all, based on a view that contains each record + two times, once for the normal relation, once for the inverse relation, + will be used to maintain the data. + """ _name = 'res.partner.relation' _description = 'Partner relation' - _order = 'active desc, date_start desc, date_end desc' - - type_selection_id = fields.Many2one( - 'res.partner.relation.type.selection', - compute='_compute_fields', - fnct_inv=lambda *args: None, - string='Type', - ) - - partner_id_display = fields.Many2one( - 'res.partner', - compute='_compute_fields', - fnct_inv=lambda *args: None, - string='Partner', - ) - - allow_self = fields.Boolean(related='type_id.allow_self') - - left_contact_type = fields.Selection( - lambda s: s.env['res.partner.relation.type']._get_partner_types(), - 'Left Partner Type', - compute='_compute_any_partner_id', - store=True, - ) - - right_contact_type = fields.Selection( - lambda s: s.env['res.partner.relation.type']._get_partner_types(), - 'Right Partner Type', - compute='_compute_any_partner_id', - store=True, - ) - - any_partner_id = fields.Many2many( - 'res.partner', - string='Partner', - compute='_compute_any_partner_id', - search='_search_any_partner_id' - ) left_partner_id = fields.Many2one( - 'res.partner', + comodel_name='res.partner', string='Source Partner', required=True, auto_join=True, ondelete='cascade', ) - right_partner_id = fields.Many2one( - 'res.partner', + comodel_name='res.partner', string='Destination Partner', required=True, auto_join=True, ondelete='cascade', ) - type_id = fields.Many2one( - 'res.partner.relation.type', + comodel_name='res.partner.relation.type', string='Type', required=True, auto_join=True, ) - date_start = fields.Date('Starting date') date_end = fields.Date('Ending date') - active = fields.Boolean('Active', default=True) - - @api.multi - def _compute_fields(self): - for this in self: - on_right_partner = this._on_right_partner() - this.type_selection_id = self\ - .env['res.partner.relation.type.selection']\ - .browse(this.type_id.id * PADDING + - (on_right_partner and 1 or 0)) - this.partner_id_display = ( - this.left_partner_id - if on_right_partner - else this.right_partner_id - ) - - @api.onchange('type_selection_id') - def _onchange_type_selection_id(self): - '''Set domain on partner_id_display, when selection a relation type''' - result = { - 'domain': {'partner_id_display': [FALSE_LEAF]}, - } - if not self.type_selection_id: - return result - type_id, is_reverse = self.type_selection_id\ - .get_type_from_selection_id() - self.type_id = self.env['res.partner.relation.type'].browse(type_id) - partner_domain = [] - check_contact_type = self.type_id.contact_type_right - check_partner_category = self.type_id.partner_category_right - if is_reverse: - # partner_id_display is left partner - check_contact_type = self.type_id.contact_type_left - check_partner_category = self.type_id.partner_category_left - if check_contact_type == 'c': - partner_domain.append(('is_company', '=', True)) - if check_contact_type == 'p': - partner_domain.append(('is_company', '=', False)) - if check_partner_category: - partner_domain.append( - ('category_id', 'child_of', check_partner_category.ids)) - result['domain']['partner_id_display'] = partner_domain - return result - - @api.one - @api.depends('left_partner_id', 'right_partner_id') - def _compute_any_partner_id(self): - self.left_contact_type = self.left_partner_id.get_partner_type() - self.right_contact_type = self.right_partner_id.get_partner_type() - self.any_partner_id = self.left_partner_id + self.right_partner_id - - @api.model - def _search_any_partner_id(self, operator, value): - return [ - '|', - ('left_partner_id', operator, value), - ('right_partner_id', operator, value), - ] - - @api.multi - def _on_right_partner(self): - '''Determine wether functions are called in a situation where the - active partner is the right partner. Default False! - ''' - return set(self.mapped('right_partner_id').ids) &\ - set(self.env.context.get('active_ids', [])) - - @api.model - def _correct_vals(self, vals): - """Fill type and left and right partner id, according to whether - we have a normal relation type or an inverse relation type - """ - vals = vals.copy() - if 'type_selection_id' not in vals: - return vals - - type_id, is_reverse = self\ - .env['res.partner.relation.type.selection']\ - .browse(vals['type_selection_id'])\ - .get_type_from_selection_id() - - vals['type_id'] = type_id - - if self._context.get('active_id'): - if is_reverse: - vals['right_partner_id'] = self._context['active_id'] - else: - vals['left_partner_id'] = self._context['active_id'] - if vals.get('partner_id_display'): - if is_reverse: - vals['left_partner_id'] = vals['partner_id_display'] - else: - vals['right_partner_id'] = vals['partner_id_display'] - if vals.get('other_partner_id'): - if is_reverse: - vals['left_partner_id'] = vals['other_partner_id'] - else: - vals['right_partner_id'] = vals['other_partner_id'] - del vals['other_partner_id'] - if vals.get('this_partner_id'): - if is_reverse: - vals['right_partner_id'] = vals['this_partner_id'] - else: - vals['left_partner_id'] = vals['this_partner_id'] - del vals['this_partner_id'] - if vals.get('contact_type'): - del vals['contact_type'] - return vals - - @api.multi - def write(self, vals): - """Override write to correct values, before being stored.""" - vals = self._correct_vals(vals) - return super(ResPartnerRelation, self).write(vals) @api.model def create(self, vals): """Override create to correct values, before being stored.""" - vals = self._correct_vals(vals) + context = self.env.context + if 'left_partner_id' not in vals and context.get('active_id'): + vals['left_partner_id'] = context.get('active_id') return super(ResPartnerRelation, self).create(vals) @api.one @@ -220,37 +63,44 @@ class ResPartnerRelation(models.Model): @api.one @api.constrains('left_partner_id', 'type_id') - def _check_partner_type_left(self): + def _check_partner_left(self): """Check left partner for required company or person :raises exceptions.Warning: When constraint is violated """ - self._check_partner_type("left") + self._check_partner("left") @api.one @api.constrains('right_partner_id', 'type_id') - def _check_partner_type_right(self): + def _check_partner_right(self): """Check right partner for required company or person :raises exceptions.Warning: When constraint is violated """ - self._check_partner_type("right") + self._check_partner("right") @api.one - def _check_partner_type(self, side): - """Check partner to left or right for required company or person + def _check_partner(self, side): + """Check partner for required company or person, and for category :param str side: left or right :raises exceptions.Warning: When constraint is violated """ assert side in ['left', 'right'] ptype = getattr(self.type_id, "contact_type_%s" % side) - company = getattr(self, '%s_partner_id' % side).is_company - if (ptype == 'c' and not company) or (ptype == 'p' and company): + partner = getattr(self, '%s_partner_id' % side) + if ((ptype == 'c' and not partner.is_company) or + (ptype == 'p' and partner.is_company)): raise exceptions.Warning( _('The %s partner is not applicable for this relation type.') % side ) + category = getattr(self.type_id, "partner_category_%s" % side) + if category and category.id not in partner.category_id.ids: + raise exceptions.Warning( + _('The %s partner does not have category %s.') % + (side, category.name) + ) @api.one @api.constrains('left_partner_id', 'right_partner_id') @@ -260,81 +110,46 @@ class ResPartnerRelation(models.Model): :raises exceptions.Warning: When constraint is violated """ if self.left_partner_id == self.right_partner_id: - if not self.allow_self: + if not (self.type_id and self.type_id.allow_self): raise exceptions.Warning( _('Partners cannot have a relation with themselves.') ) @api.one - @api.constrains('left_partner_id', 'right_partner_id', 'active') + @api.constrains( + 'left_partner_id', + 'type_id', + 'right_partner_id', + 'date_start', + 'date_end', + ) def _check_relation_uniqueness(self): """Forbid multiple active relations of the same type between the same partners :raises exceptions.Warning: When constraint is violated """ - if not self.active: - return + # pylint: disable=no-member + # pylint: disable=no-value-for-parameter domain = [ ('type_id', '=', self.type_id.id), - ('active', '=', True), ('id', '!=', self.id), ('left_partner_id', '=', self.left_partner_id.id), ('right_partner_id', '=', self.right_partner_id.id), ] if self.date_start: - domain += ['|', ('date_end', '=', False), - ('date_end', '>=', self.date_start)] + domain += [ + '|', + ('date_end', '=', False), + ('date_end', '>=', self.date_start), + ] if self.date_end: - domain += ['|', ('date_start', '=', False), - ('date_start', '<=', self.date_end)] + domain += [ + '|', + ('date_start', '=', False), + ('date_start', '<=', self.date_end), + ] if self.search(domain): raise exceptions.Warning( _('There is already a similar relation with overlapping dates') ) - - @api.multi - def get_action_related_partners(self): - '''return a window action showing a list of partners taking part in the - relations names by ids. Context key 'partner_relations_show_side' - determines if we show 'left' side, 'right' side or 'all' (default) - partners. - If active_model is res.partner.relation.all, left=this and - right=other''' - field_names = {} - - if self.env.context.get('active_model', self._name) == self._name: - field_names = { - 'left': ['left'], - 'right': ['right'], - 'all': ['left', 'right'] - } - elif self.env.context.get('active_model') ==\ - 'res.partner.relation.all': - field_names = { - 'left': ['this'], - 'right': ['other'], - 'all': ['this', 'other'] - } - else: - assert False, 'Unknown active_model!' - - partners = self.env['res.partner'].browse([]) - field_names = field_names[ - self.env.context.get('partner_relations_show_side', 'all') - ] - field_names = ['%s_partner_id' % n for n in field_names] - - for relation in self.env[self.env.context.get('active_model')].browse( - self.ids): - for name in field_names: - partners += relation[name] - - return { - 'name': _('Related partners'), - 'type': 'ir.actions.act_window', - 'res_model': 'res.partner', - 'domain': [('id', 'in', partners.ids)], - 'views': [(False, 'tree'), (False, 'form')], - 'view_type': 'form' - } diff --git a/partner_relations/models/res_partner_relation_all.py b/partner_relations/models/res_partner_relation_all.py index 7efff67c9..552c0efa4 100644 --- a/partner_relations/models/res_partner_relation_all.py +++ b/partner_relations/models/res_partner_relation_all.py @@ -1,23 +1,35 @@ # -*- coding: utf-8 -*- # © 2014-2016 Therp BV # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +"""Abstract model to show each relation from two sides.""" from psycopg2.extensions import AsIs -from openerp import models, fields, api + +from openerp import _, api, fields, models from openerp.tools import drop_view_if_exists -from .res_partner_relation_type_selection import\ - ResPartnerRelationTypeSelection -from .res_partner import PADDING + + +PADDING = 10 +_RECORD_TYPES = [ + ('a', 'Left partner to right partner'), + ('b', 'Right partner to left partner'), +] class ResPartnerRelationAll(models.AbstractModel): + """Abstract model to show each relation from two sides.""" _auto = False _log_access = False _name = 'res.partner.relation.all' - _overlays = 'res.partner.relation' _description = 'All (non-inverse + inverse) relations between partners' + _order = ( + 'this_partner_id, type_selection_id,' + 'date_end desc, date_start desc' + ) + + _overlays = 'res.partner.relation' _additional_view_fields = [] - '''append to this list if you added fields to res_partner_relation that + """append to this list if you added fields to res_partner_relation that you need in this model and related fields are not adequate (ie for sorting) You must use the same name as in res_partner_relation. Don't overwrite this list in your declaration but append in _auto_init: @@ -28,53 +40,45 @@ class ResPartnerRelationAll(models.AbstractModel): cr, context=context) my_field = fields... - ''' + """ this_partner_id = fields.Many2one( - 'res.partner', - string='Current Partner', + comodel_name='res.partner', + string='One Partner', required=True, ) - other_partner_id = fields.Many2one( - 'res.partner', + comodel_name='res.partner', string='Other Partner', required=True, ) - - type_id = fields.Many2one( - 'res.partner.relation.type', - string='Relation Type', - required=True, - ) - type_selection_id = fields.Many2one( - 'res.partner.relation.type.selection', + comodel_name='res.partner.relation.type.selection', string='Relation Type', required=True, ) - relation_id = fields.Many2one( - 'res.partner.relation', - 'Relation', + comodel_name='res.partner.relation', + string='Relation', readonly=True, ) - record_type = fields.Selection( - ResPartnerRelationTypeSelection._RECORD_TYPES, - 'Record Type', + selection=_RECORD_TYPES, + string='Record Type', readonly=True, ) - - contact_type = fields.Selection( - lambda s: s.env['res.partner.relation.type']._get_partner_types(), - 'Partner Type', - default=lambda self: self._get_default_contact_type() - ) - date_start = fields.Date('Starting date') date_end = fields.Date('Ending date') - active = fields.Boolean('Active', default=True) + active = fields.Boolean( + string='Active', + help="Records with date_end in the past are inactive", + ) + any_partner_id = fields.Many2many( + comodel_name='res.partner', + string='Partner', + compute='_compute_any_partner_id', + search='_search_any_partner_id' + ) def _auto_init(self, cr, context=None): drop_view_if_exists(cr, self._table) @@ -82,59 +86,64 @@ class ResPartnerRelationAll(models.AbstractModel): additional_view_fields = (',' + additional_view_fields)\ if additional_view_fields else '' cr.execute( - '''create or replace view %(table)s as - select - id * %(padding)s as id, - id as relation_id, - type_id, - cast('a' as char(1)) as record_type, - left_contact_type as contact_type, - left_partner_id as this_partner_id, - right_partner_id as other_partner_id, - date_start, - date_end, - active, - type_id * %(padding)s as type_selection_id - %(additional_view_fields)s - from %(underlying_table)s - union select - id * %(padding)s + 1, - id, - type_id, - cast('b' as char(1)), - right_contact_type, - right_partner_id, - left_partner_id, - date_start, - date_end, - active, - type_id * %(padding)s + 1 - %(additional_view_fields)s - from %(underlying_table)s''', + """\ +CREATE OR REPLACE VIEW %(table)s AS + SELECT + rel.id * %(padding)s AS id, + rel.id AS relation_id, + cast('a' AS CHAR(1)) AS record_type, + rel.left_partner_id AS this_partner_id, + rel.right_partner_id AS other_partner_id, + rel.date_start, + rel.date_end, + (rel.date_end IS NULL OR rel.date_end >= current_date) AS active, + rel.type_id * %(padding)s AS type_selection_id + %(additional_view_fields)s + FROM res_partner_relation rel + UNION SELECT + rel.id * %(padding)s + 1, + rel.id, + CAST('b' AS CHAR(1)), + rel.right_partner_id, + rel.left_partner_id, + rel.date_start, + rel.date_end, + rel.date_end IS NULL OR rel.date_end >= current_date, + CASE + WHEN typ.is_symmetric THEN rel.type_id * %(padding)s + ELSE rel.type_id * %(padding)s + 1 + END + %(additional_view_fields)s + FROM res_partner_relation rel + JOIN res_partner_relation_type typ ON (rel.type_id = typ.id) + """, { 'table': AsIs(self._table), 'padding': PADDING, 'additional_view_fields': AsIs(additional_view_fields), - 'underlying_table': AsIs('res_partner_relation'), } ) - return super(ResPartnerRelationAll, self)._auto_init( - cr, context=context) + cr, context=context + ) - @api.multi - def _get_underlying_object(self): - """Get the record on which this record is overlaid""" - return self.env[self._overlays].browse( - i / PADDING for i in self.ids) + @api.depends('this_partner_id', 'other_partner_id') + def _compute_any_partner_id(self): + """Compute any_partner_id, used for searching for partner, independent + wether it is the one partner or the other partner in the relation. + """ + for rec in self: + rec.any_partner_id = rec.this_partner_id + rec.other_partner_id - @api.multi - def _get_default_contact_type(self): - partner_id = self._context.get('default_this_partner_id') - if partner_id: - partner = self.env['res.partner'].browse(partner_id) - return partner.get_partner_type() - return False + @api.model + def _search_any_partner_id(self, operator, value): + """Search relation with partner, no matter on which side.""" + # pylint: disable=no-self-use + return [ + '|', + ('this_partner_id', operator, value), + ('other_partner_id', operator, value), + ] @api.multi def name_get(self): @@ -149,68 +158,301 @@ class ResPartnerRelationAll(models.AbstractModel): @api.onchange('type_selection_id') def onchange_type_selection_id(self): - """Add domain on other_partner_id according to category_other and - contact_type_other""" - domain = [] + """Add domain on partners according to category and contact_type.""" + + def check_partner_domain(partner, partner_domain, side): + """Check wether partner_domain results in empty selection + for partner, or wrong selection of partner already selected. + """ + warning = {} + if not partner_domain: + return warning + if partner: + test_domain = [('id', '=', partner.id)] + partner_domain + else: + test_domain = partner_domain + partner_model = self.env['res.partner'] + partners_found = partner_model.search(test_domain, limit=1) + if not partners_found: + if partner: + message = _( + '%s partner incompatible with relation type.' % + side.title() + ) + else: + message = _( + 'No %s partner available for relation type.' % side + ) + warning = {'title': _('Error!'), 'message': message} + return warning + + this_partner_domain = [] + other_partner_domain = [] + if self.type_selection_id.contact_type_this: + this_partner_domain.append(( + 'is_company', '=', + self.type_selection_id.contact_type_this == 'c' + )) + if self.type_selection_id.partner_category_this: + this_partner_domain.append(( + 'category_id', 'in', + self.type_selection_id.partner_category_this.ids + )) if self.type_selection_id.contact_type_other: - domain.append( - ('is_company', '=', - self.type_selection_id.contact_type_other == 'c')) + other_partner_domain.append(( + 'is_company', '=', + self.type_selection_id.contact_type_other == 'c' + )) if self.type_selection_id.partner_category_other: - domain.append( - ('category_id', 'in', - self.type_selection_id.partner_category_other.ids)) - return { - 'domain': { - 'other_partner_id': domain, - } - } + other_partner_domain.append(( + 'category_id', 'in', + self.type_selection_id.partner_category_other.ids + )) + result = {'domain': { + 'this_partner_id': this_partner_domain, + 'other_partner_id': other_partner_domain, + }} + # Check wether domain results in no choice or wrong choice of partners: + warning = check_partner_domain( + self.this_partner_id, this_partner_domain, _('this') + ) + if warning: + result['warning'] = warning + else: + warning = check_partner_domain( + self.other_partner_id, other_partner_domain, _('other') + ) + if warning: + result['warning'] = warning + return result - @api.onchange('this_partner_id') - def onchange_this_partner_id(self): - if not self.this_partner_id: - return {'domain': {'type_selection_id': []}} - return { - 'domain': { - 'type_selection_id': [ - '|', - ('contact_type_this', '=', False), - ('contact_type_this', '=', - self.this_partner_id.get_partner_type()), - '|', - ('partner_category_this', '=', False), - ('partner_category_this', 'in', - self.this_partner_id.category_id.ids), - ], - }, - } + @api.onchange( + 'this_partner_id', + 'other_partner_id', + ) + def onchange_partner_id(self): + """Set domain on type_selection_id based on partner(s) selected.""" + + def check_type_selection_domain(type_selection_domain): + """Check wether type_selection_domain results in empty selection + for type_selection_id, or wrong selection if already selected. + """ + warning = {} + if not type_selection_domain: + return warning + if self.type_selection_id: + test_domain = ( + [('id', '=', self.type_selection_id.id)] + + type_selection_domain + ) + else: + test_domain = type_selection_domain + type_model = self.env['res.partner.relation.type.selection'] + types_found = type_model.search(test_domain, limit=1) + if not types_found: + if self.type_selection_id: + message = _( + 'Relation type incompatible with selected partner(s).' + ) + else: + message = _( + 'No relation type available for selected partners.' + ) + warning = {'title': _('Error!'), 'message': message} + return warning + + type_selection_domain = [] + if self.this_partner_id: + type_selection_domain += [ + '|', + ('contact_type_this', '=', False), + ('contact_type_this', '=', + self.this_partner_id.get_partner_type()), + '|', + ('partner_category_this', '=', False), + ('partner_category_this', 'in', + self.this_partner_id.category_id.ids), + ] + if self.other_partner_id: + type_selection_domain += [ + '|', + ('contact_type_other', '=', False), + ('contact_type_other', '=', + self.other_partner_id.get_partner_type()), + '|', + ('partner_category_other', '=', False), + ('partner_category_other', 'in', + self.other_partner_id.category_id.ids), + ] + result = {'domain': { + 'type_selection_id': type_selection_domain, + }} + # Check wether domain results in no choice or wrong choice for + # type_selection_id: + warning = check_type_selection_domain(type_selection_domain) + if warning: + result['warning'] = warning + return result + + @api.model + def _correct_vals(self, vals): + """Fill left and right partner from this and other partner.""" + vals = vals.copy() + if 'this_partner_id' in vals: + vals['left_partner_id'] = vals['this_partner_id'] + del vals['this_partner_id'] + if 'other_partner_id' in vals: + vals['right_partner_id'] = vals['other_partner_id'] + del vals['other_partner_id'] + if 'type_selection_id' not in vals: + return vals + selection = self.type_selection_id.browse(vals['type_selection_id']) + type_id = selection.type_id.id + is_inverse = selection.is_inverse + vals['type_id'] = type_id + del vals['type_selection_id'] + # Need to switch right and left partner if we are in reverse id: + if 'left_partner_id' in vals or 'right_partner_id' in vals: + if is_inverse: + left_partner_id = False + right_partner_id = False + if 'left_partner_id' in vals: + right_partner_id = vals['left_partner_id'] + del vals['left_partner_id'] + if 'right_partner_id' in vals: + left_partner_id = vals['right_partner_id'] + del vals['right_partner_id'] + if left_partner_id: + vals['left_partner_id'] = left_partner_id + if right_partner_id: + vals['right_partner_id'] = right_partner_id + return vals + + def check_type_selection_domain(type_selection_domain): + """Check wether type_selection_domain results in empty selection + for type_selection_id, or wrong selection if already selected. + """ + warning = {} + if not type_selection_domain: + return warning + if self.type_selection_id: + test_domain = ( + [('id', '=', self.type_selection_id.id)] + + type_selection_domain + ) + else: + test_domain = type_selection_domain + type_model = self.env['res.partner.relation.type.selection'] + types_found = type_model.search(test_domain, limit=1) + if not types_found: + if self.type_selection_id: + message = _( + 'Relation type incompatible with selected partner(s).' + ) + else: + message = _( + 'No relation type available for selected partners.' + ) + warning = {'title': _('Error!'), 'message': message} + return warning + + type_selection_domain = [] + if self.this_partner_id: + type_selection_domain += [ + '|', + ('contact_type_this', '=', False), + ('contact_type_this', '=', + self.this_partner_id.get_partner_type() + ), + '|', + ('partner_category_this', '=', False), + ('partner_category_this', 'in', + self.this_partner_id.category_id.ids), + ] + if self.other_partner_id: + type_selection_domain += [ + '|', + ('contact_type_other', '=', False), + ('contact_type_other', '=', + self.other_partner_id.get_partner_type()), + '|', + ('partner_category_other', '=', False), + ('partner_category_other', 'in', + self.other_partner_id.category_id.ids), + ] + result = {'domain': { + 'type_selection_id': type_selection_domain, + }} + # Check wether domain results in no choice or wrong choice for + # type_selection_id: + warning = check_type_selection_domain(type_selection_domain) + if warning: + result['warning'] = warning + return result + + @api.model + def _correct_vals(self, vals): + """Fill left and right partner from this and other partner.""" + vals = vals.copy() + if 'this_partner_id' in vals: + vals['left_partner_id'] = vals['this_partner_id'] + del vals['this_partner_id'] + if 'other_partner_id' in vals: + vals['right_partner_id'] = vals['other_partner_id'] + del vals['other_partner_id'] + if 'type_selection_id' not in vals: + return vals + selection = self.type_selection_id.browse(vals['type_selection_id']) + type_id = selection.type_id.id + is_inverse = selection.is_inverse + vals['type_id'] = type_id + del vals['type_selection_id'] + # Need to switch right and left partner if we are in reverse id: + if 'left_partner_id' in vals or 'right_partner_id' in vals: + if is_inverse: + left_partner_id = False + right_partner_id = False + if 'left_partner_id' in vals: + right_partner_id = vals['left_partner_id'] + del vals['left_partner_id'] + if 'right_partner_id' in vals: + left_partner_id = vals['right_partner_id'] + del vals['right_partner_id'] + if left_partner_id: + vals['left_partner_id'] = left_partner_id + if right_partner_id: + vals['right_partner_id'] = right_partner_id + return vals @api.multi def write(self, vals): """divert non-problematic writes to underlying table""" - underlying_objs = self._get_underlying_object() - vals = { - key: val - for key, val in vals.iteritems() - if not self._fields[key].readonly - } - return underlying_objs.write(vals) + vals = self._correct_vals(vals) + for rec in self: + rec.relation_id.write(vals) + return True @api.model def create(self, vals): - """divert non-problematic creates to underlying table + """Divert non-problematic creates to underlying table. - Create a res.partner.relation but return the converted id + Create a res.partner.relation but return the converted id. """ - vals = { - key: val - for key, val in vals.iteritems() - if not self._fields[key].readonly - } - res = self.env[self._overlays].create(vals) - return self.browse(res.id * PADDING) + is_inverse = False + if 'type_selection_id' in vals: + selection = self.type_selection_id.browse( + vals['type_selection_id'] + ) + is_inverse = selection.is_inverse + vals = self._correct_vals(vals) + res = self.relation_id.create(vals) + return_id = res.id * PADDING + (is_inverse and 1 or 0) + return self.browse(return_id) @api.multi def unlink(self): """divert non-problematic creates to underlying table""" - return self._get_underlying_object().unlink() + # pylint: disable=arguments-differ + for rec in self: + rec.relation_id.unlink() + return True diff --git a/partner_relations/models/res_partner_relation_type.py b/partner_relations/models/res_partner_relation_type.py index 218991f74..7ea1f94cc 100644 --- a/partner_relations/models/res_partner_relation_type.py +++ b/partner_relations/models/res_partner_relation_type.py @@ -1,7 +1,21 @@ # -*- coding: utf-8 -*- # © 2013-2016 Therp BV # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -from openerp import models, fields, api, _ +"""Define the type of relations that can exist between partners.""" +from openerp import _, api, fields, models +from openerp.exceptions import ValidationError + + +HANDLE_INVALID_ONCHANGE = [ + ('restrict', + _('Do not allow change that will result in invalid relations')), + ('ignore', + _('Allow existing relations that do not fit changed conditions')), + ('end', + _('End relations per today, if they do not fit changed conditions')), + ('delete', + _('Delete relations that do not fit changed conditions')), +] class ResPartnerRelationType(models.Model): @@ -11,55 +25,165 @@ class ResPartnerRelationType(models.Model): _order = 'name' name = fields.Char( - 'Name', + string='Name', required=True, translate=True, ) name_inverse = fields.Char( - 'Inverse name', + string='Inverse name', required=True, translate=True, ) contact_type_left = fields.Selection( - '_get_partner_types', - 'Left partner type', + selection='get_partner_types', + string='Left partner type', ) contact_type_right = fields.Selection( - '_get_partner_types', - 'Right partner type', + selection='get_partner_types', + string='Right partner type', ) partner_category_left = fields.Many2one( - 'res.partner.category', - 'Left partner category', + comodel_name='res.partner.category', + string='Left partner category', ) partner_category_right = fields.Many2one( - 'res.partner.category', - 'Right partner category', + comodel_name='res.partner.category', + string='Right partner category', ) allow_self = fields.Boolean( - 'Reflexive', + string='Reflexive', help='This relation can be set up with the same partner left and ' 'right', default=False, ) - symmetric = fields.Boolean( - 'Symmetric', - help='This relation is the same from right to left as from left to ' - 'right', + is_symmetric = fields.Boolean( + string='Symmetric', + old_name='symmetric', + help="This relation is the same from right to left as from left to" + " right", default=False, ) + handle_invalid_onchange = fields.Selection( + selection=HANDLE_INVALID_ONCHANGE, + string='Invalid relation handling', + required=True, + default='restrict', + help="When adding relations criteria like partner type and category" + " are checked.\n" + "However when you change the criteria, there might be relations" + " that do not fit the new criteria.\n" + "Specify how this situation should be handled.", + ) @api.model - def _get_partner_types(self): + def get_partner_types(self): + """A partner can be an organisation or an individual.""" + # pylint: disable=no-self-use return [ - ('c', _('Company')), + ('c', _('Organisation')), ('p', _('Person')), ] - @api.onchange('symmetric') - def _onchange_symmetric(self): - self.update({ - 'name_inverse': self.name, - 'contact_type_right': self.contact_type_left, - 'partner_category_right': self.partner_category_left, - }) + @api.onchange('is_symmetric') + def onchange_is_symmetric(self): + """Set right side to left side if symmetric.""" + if self.is_symmetric: + self.update({ + 'name_inverse': self.name, + 'contact_type_right': self.contact_type_left, + 'partner_category_right': self.partner_category_left, + }) + + @api.multi + def check_existing(self, vals): + """Check wether records exist that do not fit new criteria.""" + relation_model = self.env['res.partner.relation'] + for rec in self: + handling = ( + 'handle_invalid_onchange' in vals and + vals['handle_invalid_onchange'] or + self.handle_invalid_onchange + ) + if handling == 'ignore': + continue + # only look at relations for this type + invalid_domain = [ + ('type_id', '=', rec.id), + ] + contact_type_left = ( + 'contact_type_left' in vals and vals['contact_type_left'] or + False + ) + if contact_type_left == 'c': + # Valid records are companies: + invalid_domain.append( + ('left_partner_id.is_company', '=', False) + ) + if contact_type_left == 'p': + # Valid records are persons: + invalid_domain.append( + ('left_partner_id.is_company', '=', True) + ) + contact_type_right = ( + 'contact_type_right' in vals and vals['contact_type_right'] or + False + ) + if contact_type_right == 'c': + # Valid records are companies: + invalid_domain.append( + ('right_partner_id.is_company', '=', False) + ) + if contact_type_right == 'p': + # Valid records are persons: + invalid_domain.append( + ('right_partner_id.is_company', '=', True) + ) + partner_category_left = ( + 'partner_category_left' in vals and + vals['partner_category_left'] or + False + ) + if partner_category_left: + # records that do not have the specified category are invalid: + invalid_domain.append( + ('left_partner_id.category_id', 'not in', + partner_category_left) + ) + partner_category_right = ( + 'partner_category_right' in vals and + vals['partner_category_right'] or + False + ) + if partner_category_right: + # records that do not have the specified category are invalid: + invalid_domain.append( + ('right_partner_id.category_id', 'not in', + partner_category_right) + ) + invalid_relations = relation_model.with_context( + active_test=False + ).search(invalid_domain) + if invalid_relations: + if handling == 'restrict': + raise ValidationError( + _('There are already relations not satisfying the' + ' conditions for partner type or category.') + ) + elif handling == 'delete': + invalid_relations.unlink() + else: + # Delete future records, end other ones, ignore relations + # already ended: + cutoff_date = fields.Date.today() + for relation in invalid_relations: + if relation.date_start >= cutoff_date: + relation.unlink() + elif (not relation.date_end or + relation.date_end > cutoff_date): + relation.write({'date_end': cutoff_date}) + + @api.multi + def write(self, vals): + """Handle existing relations if conditions change.""" + self.check_existing(vals) + return super(ResPartnerRelationType, self).write(vals) diff --git a/partner_relations/models/res_partner_relation_type_selection.py b/partner_relations/models/res_partner_relation_type_selection.py index 3ee150f09..43ea80f94 100644 --- a/partner_relations/models/res_partner_relation_type_selection.py +++ b/partner_relations/models/res_partner_relation_type_selection.py @@ -1,17 +1,10 @@ # -*- coding: utf-8 -*- # © 2014-2016 Therp BV # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -''' -Created on 23 may 2014 - -@author: Ronald Portier, Therp - -rportier@therp.nl -http://www.therp.nl - +""" For the model defined here _auto is set to False to prevent creating a -database file. All i/o operations are overridden to use a sql SELECT that -takes data from res_partner_connection_type where each type is included in the +database file. The model is based on a SQL view based on +res_partner_relation_type where each type is included in the result set twice, so it appears that the connection type and the inverse type are separate records.. @@ -19,25 +12,20 @@ The original function _auto_init is still called because this function normally (if _auto == True) not only creates the db tables, but it also takes care of registering all fields in ir_model_fields. This is needed to make the field labels translatable. - -example content for last lines of _statement: -select id, record_type, - customer_id, customer_name, customer_city, customer_zip, customer_street, - caller_id, caller_name, caller_phone, caller_fax, caller_email -from FULL_LIST as ResPartnerRelationTypeSelection where record_type = 'c' -ORDER BY ResPartnerRelationTypeSelection.customer_name asc, -ResPartnerRelationTypeSelection.caller_name asc; - -''' +""" from psycopg2.extensions import AsIs + from openerp import api, fields, models from openerp.tools import drop_view_if_exists + from .res_partner_relation_type import ResPartnerRelationType -from .res_partner import PADDING + + +PADDING = 10 class ResPartnerRelationTypeSelection(models.Model): - '''Virtual relation types''' + """Virtual relation types""" _name = 'res.partner.relation.type.selection' _description = 'All relation types' _auto = False # Do not try to create table in _auto_init(..) @@ -45,105 +33,95 @@ class ResPartnerRelationTypeSelection(models.Model): _log_access = False _order = 'name asc' - _RECORD_TYPES = [ - ('a', 'Type'), - ('b', 'Inverse type'), - ] - - record_type = fields.Selection(_RECORD_TYPES, 'Record type') - type_id = fields.Many2one('res.partner.relation.type', 'Type') + type_id = fields.Many2one( + comodel_name='res.partner.relation.type', + string='Type', + ) name = fields.Char('Name') contact_type_this = fields.Selection( - ResPartnerRelationType._get_partner_types.im_func, - 'Current record\'s partner type') + selection=ResPartnerRelationType.get_partner_types.im_func, + string='Current record\'s partner type', + ) + is_inverse = fields.Boolean( + string="Is reverse type?", + help="Inverse relations are from right to left partner.", + ) contact_type_other = fields.Selection( - ResPartnerRelationType._get_partner_types.im_func, - 'Other record\'s partner type') + selection=ResPartnerRelationType.get_partner_types.im_func, + string='Other record\'s partner type', + ) partner_category_this = fields.Many2one( - 'res.partner.category', 'Current record\'s category') + comodel_name='res.partner.category', + string='Current record\'s category', + ) partner_category_other = fields.Many2one( - 'res.partner.category', 'Other record\'s category') + comodel_name='res.partner.category', + string='Other record\'s category', + ) + allow_self = fields.Boolean( + string='Reflexive', + ) + is_symmetric = fields.Boolean( + string='Symmetric', + ) def _auto_init(self, cr, context=None): drop_view_if_exists(cr, self._table) cr.execute( - '''create or replace view %(table)s as - select - id * %(padding)s as id, - id as type_id, - cast('a' as char(1)) as record_type, - name as name, - contact_type_left as contact_type_this, - contact_type_right as contact_type_other, - partner_category_left as partner_category_this, - partner_category_right as partner_category_other - from %(underlying_table)s - union select + """CREATE OR REPLACE VIEW %(table)s AS + SELECT + id * %(padding)s AS id, + id AS type_id, + name AS name, + False AS is_inverse, + contact_type_left AS contact_type_this, + contact_type_right AS contact_type_other, + partner_category_left AS partner_category_this, + partner_category_right AS partner_category_other, + allow_self, + is_symmetric + FROM %(underlying_table)s + UNION SELECT id * %(padding)s + 1, id, - cast('b' as char(1)), name_inverse, + True, contact_type_right, contact_type_left, partner_category_right, - partner_category_left - from %(underlying_table)s''', + partner_category_left, + allow_self, + is_symmetric + FROM %(underlying_table)s + WHERE not is_symmetric + """, { 'table': AsIs(self._table), 'padding': PADDING, 'underlying_table': AsIs('res_partner_relation_type'), }) - return super(ResPartnerRelationTypeSelection, self)._auto_init( cr, context=context) @api.multi def name_get(self): - """translate name using translations from res.partner.relation.type""" - ir_translation = self.env['ir.translation'] + """Get name or name_inverse from underlying model.""" return [ - ( - this.id, - ir_translation._get_source( - 'res.partner.relation.type,name_inverse' - if this.get_type_from_selection_id()[1] - else 'res.partner.relation.type,name', - ('model',), - self.env.context.get('lang'), - this.name, - this.type_id.id - ) - ) + (this.id, + this.is_inverse and this.type_id.name_inverse or + this.type_id.display_name) for this in self ] @api.model def name_search(self, name='', args=None, operator='ilike', limit=100): - """search for translated names in res.partner.relation.type""" - res_partner_relation_type = self.env['res.partner.relation.type'] - relations = res_partner_relation_type.search([ - ('name', operator, name) - ]) - inverse_relations = res_partner_relation_type.search([ - ('name_inverse', operator, name), - ('symmetric', '=', False), - ]) + """Search for name or inverse name in underlying model.""" + # pylint: disable=no-value-for-parameter return self.search( [ - ( - 'id', 'in', - map(lambda x: x * PADDING, relations.ids) + - map(lambda x: x * PADDING + 1, inverse_relations.ids) - ), + '|', + ('type_id.name', operator, name), + ('type_id.name_inverse', operator, name), ] + (args or []), limit=limit ).name_get() - - @api.multi - def get_type_from_selection_id(self): - """Selection id ic computed from id of underlying type and the - kind of record. This function does the inverse computation to give - back the original type id, and about the record type.""" - type_id = self.id / PADDING - is_reverse = (self.id % PADDING) > 0 - return type_id, is_reverse diff --git a/partner_relations/tests/test_partner_relations.py b/partner_relations/tests/test_partner_relations.py index 9e60c3680..041a6d526 100644 --- a/partner_relations/tests/test_partner_relations.py +++ b/partner_relations/tests/test_partner_relations.py @@ -10,190 +10,427 @@ from openerp.exceptions import ValidationError class TestPartnerRelation(common.TransactionCase): def setUp(self): - super(TestPartnerRelation, self).setUp() self.partner_model = self.env['res.partner'] - self.relation_type_model = self.env['res.partner.relation.type'] + self.category_model = self.env['res.partner.category'] + self.type_model = self.env['res.partner.relation.type'] + self.selection_model = self.env['res.partner.relation.type.selection'] self.relation_model = self.env['res.partner.relation'] - - self.partner_1 = self.partner_model.create({ + self.relation_all_model = self.env['res.partner.relation.all'] + self.partner_01_person = self.partner_model.create({ 'name': 'Test User 1', 'is_company': False, + 'ref': 'PR01', }) - - self.partner_2 = self.partner_model.create({ + self.partner_02_company = self.partner_model.create({ 'name': 'Test Company', 'is_company': True, + 'ref': 'PR02', + }) + self.type_company2person = self.type_model.create({ + 'name': 'mixed', + 'name_inverse': 'mixed_inverse', + 'contact_type_left': 'c', + 'contact_type_right': 'p', + }) + # Create partners with specific categories: + self.category_01_ngo = self.category_model.create({ + 'name': 'NGO', + }) + self.partner_03_ngo = self.partner_model.create({ + 'name': 'Test NGO', + 'is_company': True, + 'ref': 'PR03', + 'category_id': [(4, self.category_01_ngo.id)], + }) + self.category_02_volunteer = self.category_model.create({ + 'name': 'Volunteer', + }) + self.partner_04_volunteer = self.partner_model.create({ + 'name': 'Test Volunteer', + 'is_company': False, + 'ref': 'PR04', + 'category_id': [(4, self.category_02_volunteer.id)], + }) + # Determine the two records in res.partner.type.selection that came + # into existance by creating one res.partner.relation.type: + selection_types = self.selection_model.search([ + ('type_id', '=', self.type_company2person.id), + ]) + for st in selection_types: + if st.is_inverse: + self.selection_person2company = st + else: + self.selection_company2person = st + assert self.selection_person2company, ( + "Failed to create person to company selection in setup." + ) + assert self.selection_company2person, ( + "Failed to create company to person selection in setup." + ) + # Create realion type between NGO and volunteer, and then lookup + # resulting type_selection_id's: + self.type_ngo2volunteer = self.type_model.create({ + 'name': 'NGO has volunteer', + 'name_inverse': 'volunteer works for NGO', + 'contact_type_left': 'c', + 'contact_type_right': 'p', + 'partner_category_left': self.category_01_ngo.id, + 'partner_category_right': self.category_02_volunteer.id, }) + selection_types = self.selection_model.search([ + ('type_id', '=', self.type_ngo2volunteer.id), + ]) + for st in selection_types: + if st.is_inverse: + self.selection_volunteer2ngo = st + else: + self.selection_ngo2volunteer = st + assert self.selection_volunteer2ngo, ( + "Failed to create volunteer to NGO selection in setup." + ) + assert self.selection_ngo2volunteer, ( + "Failed to create NGO to volunteer selection in setup." + ) - self.relation_allow = self.relation_type_model.create({ + def test_self_allowed(self): + """Test creation of relation to same partner when type allows.""" + type_allow = self.type_model.create({ 'name': 'allow', 'name_inverse': 'allow_inverse', 'contact_type_left': 'p', 'contact_type_right': 'p', 'allow_self': True }) + self.relation_model.create({ + 'type_id': type_allow.id, + 'left_partner_id': self.partner_01_person.id, + 'right_partner_id': self.partner_01_person.id, + }) - self.relation_disallow = self.relation_type_model.create({ + def test_self_disallowed(self): + """Test creating relation to same partner when disallowed. + + Attempt to create a relation of a partner to the same partner should + raise an error when the type of relation explicitly disallows this. + """ + type_disallow = self.type_model.create({ 'name': 'disallow', 'name_inverse': 'disallow_inverse', 'contact_type_left': 'p', 'contact_type_right': 'p', 'allow_self': False }) + with self.assertRaises(ValidationError): + self.relation_model.create({ + 'type_id': type_disallow.id, + 'left_partner_id': self.partner_01_person.id, + 'right_partner_id': self.partner_01_person.id, + }) + + def test_self_default(self): + """Test default not to allow relation with same partner. - self.relation_default = self.relation_type_model.create({ + Attempt to create a relation of a partner to the same partner + raise an error when the type of relation does not explicitly allow + this. + """ + type_default = self.type_model.create({ 'name': 'default', 'name_inverse': 'default_inverse', 'contact_type_left': 'p', 'contact_type_right': 'p', }) - - self.relation_mixed = self.relation_type_model.create({ - 'name': 'mixed', - 'name_inverse': 'mixed_inverse', - 'contact_type_left': 'c', - 'contact_type_right': 'p', - }) - - self.relation_symmetric = self.relation_type_model.create({ - 'name': 'sym', - 'name_inverse': 'sym', - 'symmetric': True, - }) - - def test_self_allowed(self): - self.relation_model.create({ - 'type_id': self.relation_allow.id, - 'left_partner_id': self.partner_1.id, - 'right_partner_id': self.partner_1.id, - }) - - def test_self_disallowed(self): - with self.assertRaises(ValidationError): - self.relation_model.create({ - 'type_id': self.relation_disallow.id, - 'left_partner_id': self.partner_1.id, - 'right_partner_id': self.partner_1.id, - }) - - def test_self_default(self): with self.assertRaises(ValidationError): self.relation_model.create({ - 'type_id': self.relation_default.id, - 'left_partner_id': self.partner_1.id, - 'right_partner_id': self.partner_1.id, + 'type_id': type_default.id, + 'left_partner_id': self.partner_01_person.id, + 'right_partner_id': self.partner_01_person.id, }) def test_self_mixed(self): + """Test creation of relation with wrong types. + + Trying to create a relation between partners with an inappropiate + type should raise an error. + """ with self.assertRaises(ValidationError): self.relation_model.create({ - 'type_id': self.relation_mixed.id, - 'left_partner_id': self.partner_1.id, - 'right_partner_id': self.partner_2.id, + 'type_id': self.type_company2person.id, + 'left_partner_id': self.partner_01_person.id, + 'right_partner_id': self.partner_02_company.id, }) def test_searching(self): - relation = self.relation_model.create({ - 'type_id': self.relation_mixed.id, - 'left_partner_id': self.partner_2.id, - 'right_partner_id': self.partner_1.id, + """Test searching on relations. + + Interaction with the relations should always be through + res.partner.relation.all. + """ + relation = self.relation_all_model.create({ + 'type_selection_id': self.selection_company2person.id, + 'this_partner_id': self.partner_02_company.id, + 'other_partner_id': self.partner_01_person.id, }) - partners = self.env['res.partner'].search([ - ('search_relation_id', '=', relation.type_selection_id.id) + partners = self.partner_model.search([ + ('search_relation_type_id', '=', relation.type_selection_id.id) ]) - self.assertTrue(self.partner_2 in partners) - - partners = self.env['res.partner'].search([ - ('search_relation_id', '!=', relation.type_selection_id.id) + self.assertTrue(self.partner_02_company in partners) + partners = self.partner_model.search([ + ('search_relation_type_id', '!=', relation.type_selection_id.id) ]) - self.assertTrue(self.partner_1 in partners) - - partners = self.env['res.partner'].search([ - ('search_relation_id', '=', self.relation_mixed.name) + self.assertTrue(self.partner_01_person in partners) + partners = self.partner_model.search([ + ('search_relation_type_id', '=', self.type_company2person.name) ]) - self.assertTrue(self.partner_1 in partners) - self.assertTrue(self.partner_2 in partners) - - partners = self.env['res.partner'].search([ - ('search_relation_id', '=', 'unknown relation') + self.assertTrue(self.partner_01_person in partners) + self.assertTrue(self.partner_02_company in partners) + partners = self.partner_model.search([ + ('search_relation_type_id', '=', 'unknown relation') ]) self.assertFalse(partners) - - partners = self.env['res.partner'].search([ - ('search_relation_partner_id', '=', self.partner_2.id), + partners = self.partner_model.search([ + ('search_relation_partner_id', '=', self.partner_02_company.id), ]) - self.assertTrue(self.partner_1 in partners) - - partners = self.env['res.partner'].search([ + self.assertTrue(self.partner_01_person in partners) + partners = self.partner_model.search([ ('search_relation_date', '=', fields.Date.today()), ]) - self.assertTrue(self.partner_1 in partners) - self.assertTrue(self.partner_2 in partners) - - def test_ui_functions(self): - relation = self.relation_model.create({ - 'type_id': self.relation_mixed.id, - 'left_partner_id': self.partner_2.id, - 'right_partner_id': self.partner_1.id, - }) - self.assertEqual(relation.type_selection_id.type_id, relation.type_id) - relation = relation.with_context( - active_id=self.partner_1.id, - active_ids=self.partner_1.ids, - active_model='res.partner.relation', - ) - relation.read() - domain = relation._onchange_type_selection_id()['domain'] - self.assertTrue( - ('is_company', '=', True) in domain['partner_id_display'] - ) - relation.write({ - 'type_selection_id': relation.type_selection_id.id, - }) - action = relation.get_action_related_partners() - self.assertTrue(self.partner_1.id in action['domain'][0][2]) + self.assertTrue(self.partner_01_person in partners) + self.assertTrue(self.partner_02_company in partners) def test_relation_all(self): - relation_all_record = self.env['res.partner.relation.all']\ - .with_context( - active_id=self.partner_2.id, - active_ids=self.partner_2.ids, + """Test interactions through res.partner.relation.all.""" + # Check wether we can create connection from company to person, + # taking the particular company from the active records: + relation_all_record = self.relation_all_model.with_context( + active_id=self.partner_02_company.id, + active_ids=self.partner_02_company.ids, ).create({ - 'other_partner_id': self.partner_1.id, - 'type_selection_id': self.relation_mixed.id * 10, + 'other_partner_id': self.partner_01_person.id, + 'type_selection_id': self.selection_company2person.id, }) + # Check wether display name is what we should expect: self.assertEqual( relation_all_record.display_name, '%s %s %s' % ( - self.partner_2.name, - 'mixed', - self.partner_1.name, + self.partner_02_company.name, + self.selection_company2person.name, + self.partner_01_person.name, ) ) - + # Check wether the inverse record is present and looks like expected: + inverse_relation = self.relation_all_model.search([ + ('this_partner_id', '=', self.partner_01_person.id), + ('other_partner_id', '=', self.partner_02_company.id), + ]) + self.assertEqual(len(inverse_relation), 1) + self.assertEqual( + inverse_relation.type_selection_id.name, + self.selection_person2company.name + ) + # Check wether on_change_type_selection works as expected: domain = relation_all_record.onchange_type_selection_id()['domain'] self.assertTrue( - ('is_company', '=', False) in domain['other_partner_id']) - domain = relation_all_record.onchange_this_partner_id()['domain'] + ('is_company', '=', False) in domain['other_partner_id'] + ) + domain = relation_all_record.onchange_partner_id()['domain'] self.assertTrue( - ('contact_type_this', '=', 'c') in domain['type_selection_id']) - + ('contact_type_this', '=', 'c') in domain['type_selection_id'] + ) relation_all_record.write({ - 'type_id': self.relation_mixed.id, + 'type_id': self.type_company2person.id, }) + # Check wether underlying record is removed when record is removed: relation = relation_all_record.relation_id relation_all_record.unlink() self.assertFalse(relation.exists()) def test_symmetric(self): - relation = self.relation_model.create({ - 'type_id': self.relation_symmetric.id, - 'left_partner_id': self.partner_2.id, - 'right_partner_id': self.partner_1.id, + """Test creating symmetric relation.""" + # Start out with non symmetric relation: + type_symmetric = self.type_model.create({ + 'name': 'not yet symmetric', + 'name_inverse': 'the other side of not symmetric', + 'is_symmetric': False, + 'contact_type_left': False, + 'contact_type_right': 'p', + }) + # not yet symmetric relation should result in two records in + # selection: + selection_symmetric = self.selection_model.search([ + ('type_id', '=', type_symmetric.id), + ]) + self.assertEqual(len(selection_symmetric), 2) + # Now change to symmetric and test name and inverse name: + with self.env.do_in_draft(): + type_symmetric.write( + vals={ + 'name': 'sym', + 'is_symmetric': True, + } + ) + with self.env.do_in_onchange(): + type_symmetric.onchange_is_symmetric() + self.assertEqual(type_symmetric.is_symmetric, True) + self.assertEqual( + type_symmetric.name_inverse, + type_symmetric.name + ) + self.assertEqual( + type_symmetric.contact_type_right, + type_symmetric.contact_type_left + ) + # now update the database: + type_symmetric.write( + vals={ + 'name': type_symmetric.name, + 'is_symmetric': type_symmetric.is_symmetric, + 'name_inverse': type_symmetric.name_inverse, + 'contact_type_right': type_symmetric.contact_type_right, + } + ) + # symmetric relation should result in only one record in + # selection: + selection_symmetric = self.selection_model.search([ + ('type_id', '=', type_symmetric.id), + ]) + self.assertEqual(len(selection_symmetric), 1) + relation = self.relation_all_model.create({ + 'type_selection_id': selection_symmetric.id, + 'this_partner_id': self.partner_02_company.id, + 'other_partner_id': self.partner_01_person.id, }) - partners = self.env['res.partner'].search([ - ('search_relation_id', '=', relation.type_selection_id.id) + partners = self.partner_model.search([ + ('search_relation_type_id', '=', relation.type_selection_id.id) ]) - self.assertTrue(self.partner_1 in partners) - self.assertTrue(self.partner_2 in partners) + self.assertTrue(self.partner_01_person in partners) + self.assertTrue(self.partner_02_company in partners) + + def test_category_domain(self): + """Test check on category in relations.""" + # Check on left side: + with self.assertRaises(ValidationError): + self.relation_model.create({ + 'type_id': self.type_ngo2volunteer.id, + 'left_partner_id': self.partner_02_company.id, + 'right_partner_id': self.partner_04_volunteer.id, + }) + # Check on right side: + with self.assertRaises(ValidationError): + self.relation_model.create({ + 'type_id': self.type_ngo2volunteer.id, + 'left_partner_id': self.partner_03_ngo.id, + 'right_partner_id': self.partner_01_person.id, + }) + # Creating a relation with a type referring to a certain category + # should only allow partners for that category. + relation_all_record = self.relation_all_model.create({ + 'this_partner_id': self.partner_03_ngo.id, + 'type_selection_id': self.selection_ngo2volunteer.id, + 'other_partner_id': self.partner_04_volunteer.id, + }) + # Check wether on_change_type_selection works as expected: + domain = relation_all_record.onchange_type_selection_id()['domain'] + self.assertTrue( + ('category_id', 'in', [self.category_01_ngo.id]) in + domain['this_partner_id'] + ) + self.assertTrue( + ('category_id', 'in', [self.category_02_volunteer.id]) in + domain['other_partner_id'] + ) + + def test_relation_type_change(self): + """Test change in relation type conditions.""" + # First create a relation type having no particular conditions. + type_school2student = self.type_model.create({ + 'name': 'school has student', + 'name_inverse': 'studies at school', + }) + selection_types = self.selection_model.search([ + ('type_id', '=', type_school2student.id), + ]) + for st in selection_types: + if st.is_inverse: + student2school = st + else: + school2student = st + self.assertTrue(school2student) + self.assertTrue(student2school) + # Second create relations based on those conditions. + partner_school = self.partner_model.create({ + 'name': 'Test School', + 'is_company': True, + 'ref': 'TS', + }) + partner_bart = self.partner_model.create({ + 'name': 'Bart Simpson', + 'is_company': False, + 'ref': 'BS', + }) + partner_lisa = self.partner_model.create({ + 'name': 'Lisa Simpson', + 'is_company': False, + 'ref': 'LS', + }) + relation_school2bart = self.relation_all_model.create({ + 'type_selection_id': school2student.id, + 'this_partner_id': partner_school.id, + 'other_partner_id': partner_bart.id, + }) + self.assertTrue(relation_school2bart) + relation_school2lisa = self.relation_all_model.create({ + 'type_selection_id': school2student.id, + 'this_partner_id': partner_school.id, + 'other_partner_id': partner_lisa.id, + }) + self.assertTrue(relation_school2lisa) + relation_bart2lisa = self.relation_all_model.create({ + 'type_selection_id': school2student.id, + 'this_partner_id': partner_bart.id, + 'other_partner_id': partner_lisa.id, + }) + self.assertTrue(relation_bart2lisa) + # Third creata a category and make it a condition for the + # relation type. + # - Test restriction + # - Test ignore + category_student = self.category_model.create({ + 'name': 'Student', + }) + with self.assertRaises(ValidationError): + type_school2student.write({ + 'partner_category_right': category_student.id, + }) + self.assertFalse(type_school2student.partner_category_right.id) + type_school2student.write({ + 'handle_invalid_onchange': 'ignore', + 'partner_category_right': category_student.id, + }) + self.assertEqual( + type_school2student.partner_category_right.id, + category_student.id + ) + # Fourth make company type a condition for left partner + # - Test ending + # - Test deletion + partner_bart.write({ + 'category_id': [(4, category_student.id)], + }) + partner_lisa.write({ + 'category_id': [(4, category_student.id)], + }) + type_school2student.write({ + 'handle_invalid_onchange': 'end', + 'contact_type_left': 'c', + }) + self.assertEqual( + relation_bart2lisa.date_end, + fields.Date.today() + ) + type_school2student.write({ + 'handle_invalid_onchange': 'delete', + 'contact_type_left': 'c', + }) + self.assertFalse(relation_bart2lisa.exists()) diff --git a/partner_relations/views/menu.xml b/partner_relations/views/menu.xml index 40230348a..b60842f67 100644 --- a/partner_relations/views/menu.xml +++ b/partner_relations/views/menu.xml @@ -5,7 +5,7 @@ id="menu_res_partner_relation_sales" sequence="2" parent="base.menu_sales" - action="action_res_partner_relation" + action="action_res_partner_relation_all" /> - + @@ -24,19 +24,25 @@ res.partner - diff --git a/partner_relations/views/res_partner_relation.xml b/partner_relations/views/res_partner_relation.xml deleted file mode 100644 index 96bcb3982..000000000 --- a/partner_relations/views/res_partner_relation.xml +++ /dev/null @@ -1,97 +0,0 @@ - - - - - - res.partner.relation - -
- - - - - -
-
-
- - - res.partner.relation - - - - - - - - - - - - - - res.partner.relation - - - - - - - - - - - - - - - - - - Relations - res.partner.relation - form - tree - - - [('active', '=', True)] - -

- Record and track your partners' relations. Relations may be linked to other partners with a type either directly or inversely. -

-
-
- - - - code - ir.actions.server - - action = self.get_action_related_partners(cr, uid, context.get('active_ids', []), dict(context or {}, partner_relations_show_side='right')) - True - Show partners - - - - Show partners - action - client_action_multi - res.partner.relation.all - - - -
-
diff --git a/partner_relations/views/res_partner_relation_all.xml b/partner_relations/views/res_partner_relation_all.xml index 58e1e7761..6f529984f 100644 --- a/partner_relations/views/res_partner_relation_all.xml +++ b/partner_relations/views/res_partner_relation_all.xml @@ -7,77 +7,87 @@ + > + required="True" + options="{'no_create': True}" + /> + options="{'no_create': True}" + /> + required="True" + options="{'no_create': True}" + /> - +
- - res.partner.relation.all - -
- - - - - - - - - - -
-
-
- res.partner.relation.all + - - + + + + - - + + + - + Relations res.partner.relation.all form tree - - [('active', '=', True)] +

- Record and track your partners' relations. Relations may be linked to other partners with a type either directly or inversely. + Record and track your partners' relations. Relations may + be linked to other partners with a type either directly + or inversely.

diff --git a/partner_relations/views/res_partner_relation_type.xml b/partner_relations/views/res_partner_relation_type.xml index 2e4444980..56f883959 100644 --- a/partner_relations/views/res_partner_relation_type.xml +++ b/partner_relations/views/res_partner_relation_type.xml @@ -9,10 +9,11 @@ - +
+ res.partner.relation.type @@ -22,7 +23,7 @@ + > @@ -30,8 +31,8 @@ + attrs="{'invisible': [('is_symmetric', '=', True)]}" + > @@ -39,7 +40,8 @@ - + + From 1051dd19c18eb85f824b8d111a37f332200d9eb4 Mon Sep 17 00:00:00 2001 From: Ronald Portier Date: Thu, 8 Sep 2016 17:21:49 +0200 Subject: [PATCH 3/3] [ENH] Backport 9.0 improvements to partner_multi_relation. --- partner_relations/models/res_partner.py | 3 - .../models/res_partner_relation.py | 25 +- .../models/res_partner_relation_all.py | 167 ++---------- .../models/res_partner_relation_type.py | 93 +++---- partner_relations/tests/__init__.py | 8 +- ..._relations.py => test_partner_relation.py} | 250 ++++-------------- .../tests/test_partner_relation_all.py | 241 +++++++++++++++++ .../tests/test_partner_relation_common.py | 113 ++++++++ .../tests/test_partner_search.py | 76 ++++++ 9 files changed, 565 insertions(+), 411 deletions(-) rename partner_relations/tests/{test_partner_relations.py => test_partner_relation.py} (51%) create mode 100644 partner_relations/tests/test_partner_relation_all.py create mode 100644 partner_relations/tests/test_partner_relation_common.py create mode 100644 partner_relations/tests/test_partner_search.py diff --git a/partner_relations/models/res_partner.py b/partner_relations/models/res_partner.py index 1833c1498..3545a299c 100644 --- a/partner_relations/models/res_partner.py +++ b/partner_relations/models/res_partner.py @@ -116,9 +116,6 @@ class ResPartner(models.Model): def _search_relation_date(self, operator, value): """Look only for relations valid at date of search.""" # pylint: disable=no-self-use - if operator != '=': - raise exceptions.ValidationError( - _('Unsupported search operator "%s"') % operator) return [ '&', '|', diff --git a/partner_relations/models/res_partner_relation.py b/partner_relations/models/res_partner_relation.py index 2ccf0facd..0176fe9b2 100644 --- a/partner_relations/models/res_partner_relation.py +++ b/partner_relations/models/res_partner_relation.py @@ -2,7 +2,8 @@ # © 2013-2016 Therp BV # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). """Store relations (connections) between partners.""" -from openerp import _, api, exceptions, fields, models +from openerp import _, api, fields, models +from openerp.exceptions import ValidationError class ResPartnerRelation(models.Model): @@ -53,11 +54,11 @@ class ResPartnerRelation(models.Model): def _check_dates(self): """End date should not be before start date, if not filled - :raises exceptions.Warning: When constraint is violated + :raises ValidationError: When constraint is violated """ if (self.date_start and self.date_end and self.date_start > self.date_end): - raise exceptions.Warning( + raise ValidationError( _('The starting date cannot be after the ending date.') ) @@ -66,7 +67,7 @@ class ResPartnerRelation(models.Model): def _check_partner_left(self): """Check left partner for required company or person - :raises exceptions.Warning: When constraint is violated + :raises ValidationError: When constraint is violated """ self._check_partner("left") @@ -75,7 +76,7 @@ class ResPartnerRelation(models.Model): def _check_partner_right(self): """Check right partner for required company or person - :raises exceptions.Warning: When constraint is violated + :raises ValidationError: When constraint is violated """ self._check_partner("right") @@ -84,20 +85,20 @@ class ResPartnerRelation(models.Model): """Check partner for required company or person, and for category :param str side: left or right - :raises exceptions.Warning: When constraint is violated + :raises ValidationError: When constraint is violated """ assert side in ['left', 'right'] ptype = getattr(self.type_id, "contact_type_%s" % side) partner = getattr(self, '%s_partner_id' % side) if ((ptype == 'c' and not partner.is_company) or (ptype == 'p' and partner.is_company)): - raise exceptions.Warning( + raise ValidationError( _('The %s partner is not applicable for this relation type.') % side ) category = getattr(self.type_id, "partner_category_%s" % side) if category and category.id not in partner.category_id.ids: - raise exceptions.Warning( + raise ValidationError( _('The %s partner does not have category %s.') % (side, category.name) ) @@ -107,11 +108,11 @@ class ResPartnerRelation(models.Model): def _check_not_with_self(self): """Not allowed to link partner to same partner - :raises exceptions.Warning: When constraint is violated + :raises ValidationError: When constraint is violated """ if self.left_partner_id == self.right_partner_id: if not (self.type_id and self.type_id.allow_self): - raise exceptions.Warning( + raise ValidationError( _('Partners cannot have a relation with themselves.') ) @@ -127,7 +128,7 @@ class ResPartnerRelation(models.Model): """Forbid multiple active relations of the same type between the same partners - :raises exceptions.Warning: When constraint is violated + :raises ValidationError: When constraint is violated """ # pylint: disable=no-member # pylint: disable=no-value-for-parameter @@ -150,6 +151,6 @@ class ResPartnerRelation(models.Model): ('date_start', '<=', self.date_end), ] if self.search(domain): - raise exceptions.Warning( + raise ValidationError( _('There is already a similar relation with overlapping dates') ) diff --git a/partner_relations/models/res_partner_relation_all.py b/partner_relations/models/res_partner_relation_all.py index 552c0efa4..a87b4f765 100644 --- a/partner_relations/models/res_partner_relation_all.py +++ b/partner_relations/models/res_partner_relation_all.py @@ -76,7 +76,7 @@ class ResPartnerRelationAll(models.AbstractModel): any_partner_id = fields.Many2many( comodel_name='res.partner', string='Partner', - compute='_compute_any_partner_id', + compute=lambda self: None, search='_search_any_partner_id' ) @@ -127,14 +127,6 @@ CREATE OR REPLACE VIEW %(table)s AS cr, context=context ) - @api.depends('this_partner_id', 'other_partner_id') - def _compute_any_partner_id(self): - """Compute any_partner_id, used for searching for partner, independent - wether it is the one partner or the other partner in the relation. - """ - for rec in self: - rec.any_partner_id = rec.this_partner_id + rec.other_partner_id - @api.model def _search_any_partner_id(self, operator, value): """Search relation with partner, no matter on which side.""" @@ -165,8 +157,6 @@ CREATE OR REPLACE VIEW %(table)s AS for partner, or wrong selection of partner already selected. """ warning = {} - if not partner_domain: - return warning if partner: test_domain = [('id', '=', partner.id)] + partner_domain else: @@ -174,16 +164,17 @@ CREATE OR REPLACE VIEW %(table)s AS partner_model = self.env['res.partner'] partners_found = partner_model.search(test_domain, limit=1) if not partners_found: + warning['title'] = _('Error!') if partner: - message = _( - '%s partner incompatible with relation type.' % + warning['message'] = ( + _('%s partner incompatible with relation type.') % side.title() ) else: - message = _( - 'No %s partner available for relation type.' % side + warning['message'] = ( + _('No %s partner available for relation type.') % + side ) - warning = {'title': _('Error!'), 'message': message} return warning this_partner_domain = [] @@ -213,17 +204,17 @@ CREATE OR REPLACE VIEW %(table)s AS 'other_partner_id': other_partner_domain, }} # Check wether domain results in no choice or wrong choice of partners: - warning = check_partner_domain( - self.this_partner_id, this_partner_domain, _('this') - ) - if warning: - result['warning'] = warning - else: + warning = {} + if this_partner_domain: + warning = check_partner_domain( + self.this_partner_id, this_partner_domain, _('this') + ) + if not warning and other_partner_domain: warning = check_partner_domain( self.other_partner_id, other_partner_domain, _('other') ) - if warning: - result['warning'] = warning + if warning: + result['warning'] = warning return result @api.onchange( @@ -234,126 +225,25 @@ CREATE OR REPLACE VIEW %(table)s AS """Set domain on type_selection_id based on partner(s) selected.""" def check_type_selection_domain(type_selection_domain): - """Check wether type_selection_domain results in empty selection - for type_selection_id, or wrong selection if already selected. + """If type_selection_id already selected, check wether it + is compatible with the computed type_selection_domain. An empty + selection can practically only occur in a practically empty + database, and will not lead to problems. Therefore not tested. """ warning = {} - if not type_selection_domain: + if not (type_selection_domain and self.type_selection_id): return warning - if self.type_selection_id: - test_domain = ( - [('id', '=', self.type_selection_id.id)] + - type_selection_domain - ) - else: - test_domain = type_selection_domain + test_domain = ( + [('id', '=', self.type_selection_id.id)] + + type_selection_domain + ) type_model = self.env['res.partner.relation.type.selection'] types_found = type_model.search(test_domain, limit=1) if not types_found: - if self.type_selection_id: - message = _( - 'Relation type incompatible with selected partner(s).' - ) - else: - message = _( - 'No relation type available for selected partners.' - ) - warning = {'title': _('Error!'), 'message': message} - return warning - - type_selection_domain = [] - if self.this_partner_id: - type_selection_domain += [ - '|', - ('contact_type_this', '=', False), - ('contact_type_this', '=', - self.this_partner_id.get_partner_type()), - '|', - ('partner_category_this', '=', False), - ('partner_category_this', 'in', - self.this_partner_id.category_id.ids), - ] - if self.other_partner_id: - type_selection_domain += [ - '|', - ('contact_type_other', '=', False), - ('contact_type_other', '=', - self.other_partner_id.get_partner_type()), - '|', - ('partner_category_other', '=', False), - ('partner_category_other', 'in', - self.other_partner_id.category_id.ids), - ] - result = {'domain': { - 'type_selection_id': type_selection_domain, - }} - # Check wether domain results in no choice or wrong choice for - # type_selection_id: - warning = check_type_selection_domain(type_selection_domain) - if warning: - result['warning'] = warning - return result - - @api.model - def _correct_vals(self, vals): - """Fill left and right partner from this and other partner.""" - vals = vals.copy() - if 'this_partner_id' in vals: - vals['left_partner_id'] = vals['this_partner_id'] - del vals['this_partner_id'] - if 'other_partner_id' in vals: - vals['right_partner_id'] = vals['other_partner_id'] - del vals['other_partner_id'] - if 'type_selection_id' not in vals: - return vals - selection = self.type_selection_id.browse(vals['type_selection_id']) - type_id = selection.type_id.id - is_inverse = selection.is_inverse - vals['type_id'] = type_id - del vals['type_selection_id'] - # Need to switch right and left partner if we are in reverse id: - if 'left_partner_id' in vals or 'right_partner_id' in vals: - if is_inverse: - left_partner_id = False - right_partner_id = False - if 'left_partner_id' in vals: - right_partner_id = vals['left_partner_id'] - del vals['left_partner_id'] - if 'right_partner_id' in vals: - left_partner_id = vals['right_partner_id'] - del vals['right_partner_id'] - if left_partner_id: - vals['left_partner_id'] = left_partner_id - if right_partner_id: - vals['right_partner_id'] = right_partner_id - return vals - - def check_type_selection_domain(type_selection_domain): - """Check wether type_selection_domain results in empty selection - for type_selection_id, or wrong selection if already selected. - """ - warning = {} - if not type_selection_domain: - return warning - if self.type_selection_id: - test_domain = ( - [('id', '=', self.type_selection_id.id)] + - type_selection_domain + warning['title'] = _('Error!') + warning['message'] = _( + 'Relation type incompatible with selected partner(s).' ) - else: - test_domain = type_selection_domain - type_model = self.env['res.partner.relation.type.selection'] - types_found = type_model.search(test_domain, limit=1) - if not types_found: - if self.type_selection_id: - message = _( - 'Relation type incompatible with selected partner(s).' - ) - else: - message = _( - 'No relation type available for selected partners.' - ) - warning = {'title': _('Error!'), 'message': message} return warning type_selection_domain = [] @@ -362,8 +252,7 @@ CREATE OR REPLACE VIEW %(table)s AS '|', ('contact_type_this', '=', False), ('contact_type_this', '=', - self.this_partner_id.get_partner_type() - ), + self.this_partner_id.get_partner_type()), '|', ('partner_category_this', '=', False), ('partner_category_this', 'in', diff --git a/partner_relations/models/res_partner_relation_type.py b/partner_relations/models/res_partner_relation_type.py index 7ea1f94cc..7a8b324c6 100644 --- a/partner_relations/models/res_partner_relation_type.py +++ b/partner_relations/models/res_partner_relation_type.py @@ -4,6 +4,7 @@ """Define the type of relations that can exist between partners.""" from openerp import _, api, fields, models from openerp.exceptions import ValidationError +from openerp.osv.expression import AND, OR HANDLE_INVALID_ONCHANGE = [ @@ -98,6 +99,30 @@ class ResPartnerRelationType(models.Model): def check_existing(self, vals): """Check wether records exist that do not fit new criteria.""" relation_model = self.env['res.partner.relation'] + + def get_type_condition(vals, side): + """Add if needed check for contact type.""" + fieldname1 = 'contact_type_%s' % side + fieldname2 = '%s_partner_id.is_company' % side + contact_type = fieldname1 in vals and vals[fieldname1] or False + if contact_type == 'c': + # Records that are not companies are invalid: + return [(fieldname2, '=', False)] + if contact_type == 'p': + # Records that are companies are invalid: + return [(fieldname2, '=', True)] + return [] + + def get_category_condition(vals, side): + """Add if needed check for partner category.""" + fieldname1 = 'partner_category_%s' % side + fieldname2 = '%s_partner_id.category_id' % side + category_id = fieldname1 in vals and vals[fieldname1] or False + if category_id: + # Records that do not have the specified category are invalid: + return [(fieldname2, 'not in', [category_id])] + return [] + for rec in self: handling = ( 'handle_invalid_onchange' in vals and @@ -106,60 +131,22 @@ class ResPartnerRelationType(models.Model): ) if handling == 'ignore': continue + invalid_conditions = [] + for side in ['left', 'right']: + invalid_conditions = OR([ + invalid_conditions, + get_type_condition(vals, side), + ]) + invalid_conditions = OR([ + invalid_conditions, + get_category_condition(vals, side), + ]) + if not invalid_conditions: + return # only look at relations for this type - invalid_domain = [ - ('type_id', '=', rec.id), - ] - contact_type_left = ( - 'contact_type_left' in vals and vals['contact_type_left'] or - False - ) - if contact_type_left == 'c': - # Valid records are companies: - invalid_domain.append( - ('left_partner_id.is_company', '=', False) - ) - if contact_type_left == 'p': - # Valid records are persons: - invalid_domain.append( - ('left_partner_id.is_company', '=', True) - ) - contact_type_right = ( - 'contact_type_right' in vals and vals['contact_type_right'] or - False - ) - if contact_type_right == 'c': - # Valid records are companies: - invalid_domain.append( - ('right_partner_id.is_company', '=', False) - ) - if contact_type_right == 'p': - # Valid records are persons: - invalid_domain.append( - ('right_partner_id.is_company', '=', True) - ) - partner_category_left = ( - 'partner_category_left' in vals and - vals['partner_category_left'] or - False - ) - if partner_category_left: - # records that do not have the specified category are invalid: - invalid_domain.append( - ('left_partner_id.category_id', 'not in', - partner_category_left) - ) - partner_category_right = ( - 'partner_category_right' in vals and - vals['partner_category_right'] or - False - ) - if partner_category_right: - # records that do not have the specified category are invalid: - invalid_domain.append( - ('right_partner_id.category_id', 'not in', - partner_category_right) - ) + invalid_domain = AND([ + [('type_id', '=', rec.id)], invalid_conditions + ]) invalid_relations = relation_model.with_context( active_test=False ).search(invalid_domain) diff --git a/partner_relations/tests/__init__.py b/partner_relations/tests/__init__.py index 0c45f0703..272f6d49f 100644 --- a/partner_relations/tests/__init__.py +++ b/partner_relations/tests/__init__.py @@ -1 +1,7 @@ -from . import test_partner_relations +# -*- coding: utf-8 -*- +# Copyright 2016 Therp BV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from . import test_partner_relation_common +from . import test_partner_relation +from . import test_partner_relation_all +from . import test_partner_search diff --git a/partner_relations/tests/test_partner_relations.py b/partner_relations/tests/test_partner_relation.py similarity index 51% rename from partner_relations/tests/test_partner_relations.py rename to partner_relations/tests/test_partner_relation.py index 041a6d526..b3ba3bd7b 100644 --- a/partner_relations/tests/test_partner_relations.py +++ b/partner_relations/tests/test_partner_relation.py @@ -1,97 +1,26 @@ # -*- coding: utf-8 -*- -# Copyright 2015 Camptocamp SA # Copyright 2016 Therp BV # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from datetime import date +from dateutil.relativedelta import relativedelta + from openerp import fields -from openerp.tests import common from openerp.exceptions import ValidationError +from .test_partner_relation_common import TestPartnerRelationCommon -class TestPartnerRelation(common.TransactionCase): - def setUp(self): - super(TestPartnerRelation, self).setUp() +class TestPartnerRelation(TestPartnerRelationCommon): - self.partner_model = self.env['res.partner'] - self.category_model = self.env['res.partner.category'] - self.type_model = self.env['res.partner.relation.type'] - self.selection_model = self.env['res.partner.relation.type.selection'] - self.relation_model = self.env['res.partner.relation'] - self.relation_all_model = self.env['res.partner.relation.all'] - self.partner_01_person = self.partner_model.create({ - 'name': 'Test User 1', - 'is_company': False, - 'ref': 'PR01', - }) - self.partner_02_company = self.partner_model.create({ - 'name': 'Test Company', - 'is_company': True, - 'ref': 'PR02', - }) - self.type_company2person = self.type_model.create({ - 'name': 'mixed', - 'name_inverse': 'mixed_inverse', - 'contact_type_left': 'c', - 'contact_type_right': 'p', - }) - # Create partners with specific categories: - self.category_01_ngo = self.category_model.create({ - 'name': 'NGO', - }) - self.partner_03_ngo = self.partner_model.create({ - 'name': 'Test NGO', - 'is_company': True, - 'ref': 'PR03', - 'category_id': [(4, self.category_01_ngo.id)], - }) - self.category_02_volunteer = self.category_model.create({ - 'name': 'Volunteer', - }) - self.partner_04_volunteer = self.partner_model.create({ - 'name': 'Test Volunteer', - 'is_company': False, - 'ref': 'PR04', - 'category_id': [(4, self.category_02_volunteer.id)], - }) - # Determine the two records in res.partner.type.selection that came - # into existance by creating one res.partner.relation.type: - selection_types = self.selection_model.search([ - ('type_id', '=', self.type_company2person.id), - ]) - for st in selection_types: - if st.is_inverse: - self.selection_person2company = st - else: - self.selection_company2person = st - assert self.selection_person2company, ( - "Failed to create person to company selection in setup." - ) - assert self.selection_company2person, ( - "Failed to create company to person selection in setup." - ) - # Create realion type between NGO and volunteer, and then lookup - # resulting type_selection_id's: - self.type_ngo2volunteer = self.type_model.create({ - 'name': 'NGO has volunteer', - 'name_inverse': 'volunteer works for NGO', - 'contact_type_left': 'c', - 'contact_type_right': 'p', - 'partner_category_left': self.category_01_ngo.id, - 'partner_category_right': self.category_02_volunteer.id, - }) - selection_types = self.selection_model.search([ - ('type_id', '=', self.type_ngo2volunteer.id), - ]) - for st in selection_types: - if st.is_inverse: - self.selection_volunteer2ngo = st - else: - self.selection_ngo2volunteer = st - assert self.selection_volunteer2ngo, ( - "Failed to create volunteer to NGO selection in setup." + def test_selection_name_search(self): + """Test wether we can find type selection on reverse name.""" + selection_types = self.selection_model.name_search( + name=self.selection_person2company.name ) - assert self.selection_ngo2volunteer, ( - "Failed to create NGO to volunteer selection in setup." + self.assertTrue(selection_types) + self.assertTrue( + (self.selection_person2company.id, + self.selection_person2company.name) in selection_types ) def test_self_allowed(self): @@ -103,11 +32,13 @@ class TestPartnerRelation(common.TransactionCase): 'contact_type_right': 'p', 'allow_self': True }) - self.relation_model.create({ + self.assertTrue(type_allow) + reflexive_relation = self.relation_model.create({ 'type_id': type_allow.id, 'left_partner_id': self.partner_01_person.id, 'right_partner_id': self.partner_01_person.id, }) + self.assertTrue(reflexive_relation) def test_self_disallowed(self): """Test creating relation to same partner when disallowed. @@ -122,6 +53,7 @@ class TestPartnerRelation(common.TransactionCase): 'contact_type_right': 'p', 'allow_self': False }) + self.assertTrue(type_disallow) with self.assertRaises(ValidationError): self.relation_model.create({ 'type_id': type_disallow.id, @@ -142,6 +74,7 @@ class TestPartnerRelation(common.TransactionCase): 'contact_type_left': 'p', 'contact_type_right': 'p', }) + self.assertTrue(type_default) with self.assertRaises(ValidationError): self.relation_model.create({ 'type_id': type_default.id, @@ -162,90 +95,6 @@ class TestPartnerRelation(common.TransactionCase): 'right_partner_id': self.partner_02_company.id, }) - def test_searching(self): - """Test searching on relations. - - Interaction with the relations should always be through - res.partner.relation.all. - """ - relation = self.relation_all_model.create({ - 'type_selection_id': self.selection_company2person.id, - 'this_partner_id': self.partner_02_company.id, - 'other_partner_id': self.partner_01_person.id, - }) - partners = self.partner_model.search([ - ('search_relation_type_id', '=', relation.type_selection_id.id) - ]) - self.assertTrue(self.partner_02_company in partners) - partners = self.partner_model.search([ - ('search_relation_type_id', '!=', relation.type_selection_id.id) - ]) - self.assertTrue(self.partner_01_person in partners) - partners = self.partner_model.search([ - ('search_relation_type_id', '=', self.type_company2person.name) - ]) - self.assertTrue(self.partner_01_person in partners) - self.assertTrue(self.partner_02_company in partners) - partners = self.partner_model.search([ - ('search_relation_type_id', '=', 'unknown relation') - ]) - self.assertFalse(partners) - partners = self.partner_model.search([ - ('search_relation_partner_id', '=', self.partner_02_company.id), - ]) - self.assertTrue(self.partner_01_person in partners) - partners = self.partner_model.search([ - ('search_relation_date', '=', fields.Date.today()), - ]) - self.assertTrue(self.partner_01_person in partners) - self.assertTrue(self.partner_02_company in partners) - - def test_relation_all(self): - """Test interactions through res.partner.relation.all.""" - # Check wether we can create connection from company to person, - # taking the particular company from the active records: - relation_all_record = self.relation_all_model.with_context( - active_id=self.partner_02_company.id, - active_ids=self.partner_02_company.ids, - ).create({ - 'other_partner_id': self.partner_01_person.id, - 'type_selection_id': self.selection_company2person.id, - }) - # Check wether display name is what we should expect: - self.assertEqual( - relation_all_record.display_name, '%s %s %s' % ( - self.partner_02_company.name, - self.selection_company2person.name, - self.partner_01_person.name, - ) - ) - # Check wether the inverse record is present and looks like expected: - inverse_relation = self.relation_all_model.search([ - ('this_partner_id', '=', self.partner_01_person.id), - ('other_partner_id', '=', self.partner_02_company.id), - ]) - self.assertEqual(len(inverse_relation), 1) - self.assertEqual( - inverse_relation.type_selection_id.name, - self.selection_person2company.name - ) - # Check wether on_change_type_selection works as expected: - domain = relation_all_record.onchange_type_selection_id()['domain'] - self.assertTrue( - ('is_company', '=', False) in domain['other_partner_id'] - ) - domain = relation_all_record.onchange_partner_id()['domain'] - self.assertTrue( - ('contact_type_this', '=', 'c') in domain['type_selection_id'] - ) - relation_all_record.write({ - 'type_id': self.type_company2person.id, - }) - # Check wether underlying record is removed when record is removed: - relation = relation_all_record.relation_id - relation_all_record.unlink() - self.assertFalse(relation.exists()) - def test_symmetric(self): """Test creating symmetric relation.""" # Start out with non symmetric relation: @@ -323,41 +172,18 @@ class TestPartnerRelation(common.TransactionCase): 'left_partner_id': self.partner_03_ngo.id, 'right_partner_id': self.partner_01_person.id, }) - # Creating a relation with a type referring to a certain category - # should only allow partners for that category. - relation_all_record = self.relation_all_model.create({ - 'this_partner_id': self.partner_03_ngo.id, - 'type_selection_id': self.selection_ngo2volunteer.id, - 'other_partner_id': self.partner_04_volunteer.id, - }) - # Check wether on_change_type_selection works as expected: - domain = relation_all_record.onchange_type_selection_id()['domain'] - self.assertTrue( - ('category_id', 'in', [self.category_01_ngo.id]) in - domain['this_partner_id'] - ) - self.assertTrue( - ('category_id', 'in', [self.category_02_volunteer.id]) in - domain['other_partner_id'] - ) def test_relation_type_change(self): """Test change in relation type conditions.""" # First create a relation type having no particular conditions. - type_school2student = self.type_model.create({ - 'name': 'school has student', - 'name_inverse': 'studies at school', - }) - selection_types = self.selection_model.search([ - ('type_id', '=', type_school2student.id), - ]) - for st in selection_types: - if st.is_inverse: - student2school = st - else: - school2student = st - self.assertTrue(school2student) - self.assertTrue(student2school) + (type_school2student, + school2student, + school2student_inverse) = ( + self._create_relation_type_selection({ + 'name': 'school has student', + 'name_inverse': 'studies at school', + }) + ) # Second create relations based on those conditions. partner_school = self.partner_model.create({ 'name': 'Test School', @@ -375,20 +201,20 @@ class TestPartnerRelation(common.TransactionCase): 'ref': 'LS', }) relation_school2bart = self.relation_all_model.create({ - 'type_selection_id': school2student.id, 'this_partner_id': partner_school.id, + 'type_selection_id': school2student.id, 'other_partner_id': partner_bart.id, }) self.assertTrue(relation_school2bart) relation_school2lisa = self.relation_all_model.create({ - 'type_selection_id': school2student.id, 'this_partner_id': partner_school.id, + 'type_selection_id': school2student.id, 'other_partner_id': partner_lisa.id, }) self.assertTrue(relation_school2lisa) relation_bart2lisa = self.relation_all_model.create({ - 'type_selection_id': school2student.id, 'this_partner_id': partner_bart.id, + 'type_selection_id': school2student.id, 'other_partner_id': partner_lisa.id, }) self.assertTrue(relation_bart2lisa) @@ -421,6 +247,22 @@ class TestPartnerRelation(common.TransactionCase): partner_lisa.write({ 'category_id': [(4, category_student.id)], }) + # Future student to be deleted by end action: + partner_homer = self.partner_model.create({ + 'name': 'Homer Simpson', + 'is_company': False, + 'ref': 'HS', + 'category_id': [(4, category_student.id)], + }) + relation_lisa2homer = self.relation_all_model.create({ + 'this_partner_id': partner_lisa.id, + 'type_selection_id': school2student.id, + 'other_partner_id': partner_homer.id, + 'date_start': fields.Date.to_string( + date.today() + relativedelta(months=+6) + ), + }) + self.assertTrue(relation_lisa2homer) type_school2student.write({ 'handle_invalid_onchange': 'end', 'contact_type_left': 'c', @@ -429,8 +271,10 @@ class TestPartnerRelation(common.TransactionCase): relation_bart2lisa.date_end, fields.Date.today() ) + self.assertFalse(relation_lisa2homer.exists()) type_school2student.write({ 'handle_invalid_onchange': 'delete', 'contact_type_left': 'c', + 'contact_type_right': 'p', }) self.assertFalse(relation_bart2lisa.exists()) diff --git a/partner_relations/tests/test_partner_relation_all.py b/partner_relations/tests/test_partner_relation_all.py new file mode 100644 index 000000000..e2c48f46a --- /dev/null +++ b/partner_relations/tests/test_partner_relation_all.py @@ -0,0 +1,241 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 Therp BV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from openerp.exceptions import ValidationError + +from .test_partner_relation_common import TestPartnerRelationCommon + + +class TestPartnerRelation(TestPartnerRelationCommon): + + def setUp(self): + super(TestPartnerRelation, self).setUp() + + # Create a new relation type which will not have valid relations: + category_nobody = self.category_model.create({ + 'name': 'Nobody', + }) + (self.type_nobody, + self.selection_nobody, + self.selection_nobody_inverse) = ( + self._create_relation_type_selection({ + 'name': 'has relation with nobody', + 'name_inverse': 'nobody has relation with', + 'contact_type_left': 'c', + 'contact_type_right': 'p', + 'partner_category_left': category_nobody.id, + 'partner_category_right': category_nobody.id, + }) + ) + + def _get_empty_relation(self): + """Get empty relation record for onchange tests.""" + # Need English, because we will compare text + return self.relation_all_model.with_context(lang='en_US').new({}) + + def test_create_with_active_id(self): + """Test creation with this_partner_id from active_id.""" + # Check wether we can create connection from company to person, + # taking the particular company from the active records: + relation = self.relation_all_model.with_context( + active_id=self.partner_02_company.id, + active_ids=self.partner_02_company.ids, + ).create({ + 'other_partner_id': self.partner_01_person.id, + 'type_selection_id': self.selection_company2person.id, + }) + self.assertTrue(relation) + self.assertEqual(relation.this_partner_id, self.partner_02_company) + # Partner should have one relation now: + self.assertEqual(self.partner_01_person.relation_count, 1) + + def test_display_name(self): + """Test display name""" + relation = self._create_company2person_relation() + self.assertEqual( + relation.display_name, '%s %s %s' % ( + relation.this_partner_id.name, + relation.type_selection_id.name, + relation.other_partner_id.name, + ) + ) + + def test__regular_write(self): + """Test write with valid data.""" + relation = self._create_company2person_relation() + relation.write({ + 'date_start': '2014-09-01', + }) + relation.invalidate_cache(ids=relation.ids) + self.assertEqual(relation.date_start, '2014-09-01') + + def test_write_incompatible_dates(self): + """Test write with date_end before date_start.""" + relation = self._create_company2person_relation() + with self.assertRaises(ValidationError): + relation.write({ + 'date_start': '2016-09-01', + 'date_end': '2016-08-01', + }) + + def test_validate_overlapping_01(self): + """Test create overlapping with no start / end dates.""" + relation = self._create_company2person_relation() + with self.assertRaises(ValidationError): + # New relation with no start / end should give error + self.relation_all_model.create({ + 'this_partner_id': relation.this_partner_id.id, + 'type_selection_id': relation.type_selection_id.id, + 'other_partner_id': relation.other_partner_id.id, + }) + + def test_validate_overlapping_02(self): + """Test create overlapping with start / end dates.""" + relation = self.relation_all_model.create({ + 'this_partner_id': self.partner_02_company.id, + 'type_selection_id': self.selection_company2person.id, + 'other_partner_id': self.partner_01_person.id, + 'date_start': '2015-09-01', + 'date_end': '2016-08-31', + }) + # New relation with overlapping start / end should give error + with self.assertRaises(ValidationError): + self.relation_all_model.create({ + 'this_partner_id': relation.this_partner_id.id, + 'type_selection_id': relation.type_selection_id.id, + 'other_partner_id': relation.other_partner_id.id, + 'date_start': '2016-08-01', + 'date_end': '2017-07-30', + }) + + def test_validate_overlapping_03(self): + """Test create not overlapping.""" + relation = self.relation_all_model.create({ + 'this_partner_id': self.partner_02_company.id, + 'type_selection_id': self.selection_company2person.id, + 'other_partner_id': self.partner_01_person.id, + 'date_start': '2015-09-01', + 'date_end': '2016-08-31', + }) + relation_another_record = self.relation_all_model.create({ + 'this_partner_id': relation.this_partner_id.id, + 'type_selection_id': relation.type_selection_id.id, + 'other_partner_id': relation.other_partner_id.id, + 'date_start': '2016-09-01', + 'date_end': '2017-08-31', + }) + self.assertTrue(relation_another_record) + + def test_inverse_record(self): + """Test creation of inverse record.""" + relation = self._create_company2person_relation() + inverse_relation = self.relation_all_model.search([ + ('this_partner_id', '=', relation.other_partner_id.id), + ('other_partner_id', '=', relation.this_partner_id.id), + ]) + self.assertEqual(len(inverse_relation), 1) + self.assertEqual( + inverse_relation.type_selection_id.name, + self.selection_person2company.name + ) + + def test_inverse_creation(self): + """Test creation of record through inverse selection.""" + relation = self.relation_all_model.create({ + 'this_partner_id': self.partner_01_person.id, + 'type_selection_id': self.selection_person2company.id, + 'other_partner_id': self.partner_02_company.id, + }) + # Check wether display name is what we should expect: + self.assertEqual( + relation.display_name, '%s %s %s' % ( + self.partner_01_person.name, + self.selection_person2company.name, + self.partner_02_company.name, + ) + ) + + def test_unlink(self): + """Unlinking derived relation should unlink base relation.""" + # Check wether underlying record is removed when record is removed: + relation = self._create_company2person_relation() + base_relation = relation.relation_id + relation.unlink() + self.assertFalse(base_relation.exists()) + + def test_on_change_type_selection(self): + """Test on_change_type_selection.""" + # 1. Test call with empty relation + relation_empty = self._get_empty_relation() + result = relation_empty.onchange_type_selection_id() + self.assertTrue('domain' in result) + self.assertFalse('warning' in result) + self.assertTrue('this_partner_id' in result['domain']) + self.assertFalse(result['domain']['this_partner_id']) + self.assertTrue('other_partner_id' in result['domain']) + self.assertFalse(result['domain']['other_partner_id']) + # 2. Test call with company 2 person relation + relation = self._create_company2person_relation() + domain = relation.onchange_type_selection_id()['domain'] + self.assertTrue( + ('is_company', '=', False) in domain['other_partner_id'] + ) + # 3. Test with relation needing categories. + relation_ngo_volunteer = self.relation_all_model.create({ + 'this_partner_id': self.partner_03_ngo.id, + 'type_selection_id': self.selection_ngo2volunteer.id, + 'other_partner_id': self.partner_04_volunteer.id, + }) + domain = relation_ngo_volunteer.onchange_type_selection_id()['domain'] + self.assertTrue( + ('category_id', 'in', [self.category_01_ngo.id]) in + domain['this_partner_id'] + ) + self.assertTrue( + ('category_id', 'in', [self.category_02_volunteer.id]) in + domain['other_partner_id'] + ) + # 4. Test with invalid or impossible combinations + relation_nobody = self._get_empty_relation() + with self.env.do_in_draft(): + relation_nobody.type_selection_id = self.selection_nobody + warning = relation_nobody.onchange_type_selection_id()['warning'] + self.assertTrue('message' in warning) + self.assertTrue('No this partner available' in warning['message']) + with self.env.do_in_draft(): + relation_nobody.this_partner_id = self.partner_02_company + warning = relation_nobody.onchange_type_selection_id()['warning'] + self.assertTrue('message' in warning) + self.assertTrue('incompatible' in warning['message']) + # Allow left partner and check message for other partner: + self.type_nobody.write({ + 'partner_category_left': False, + }) + self.selection_nobody.invalidate_cache(ids=self.selection_nobody.ids) + warning = relation_nobody.onchange_type_selection_id()['warning'] + self.assertTrue('message' in warning) + self.assertTrue('No other partner available' in warning['message']) + + def test_on_change_partner_id(self): + """Test on_change_partner_id.""" + # 1. Test call with empty relation + relation_empty = self._get_empty_relation() + result = relation_empty.onchange_partner_id() + self.assertTrue('domain' in result) + self.assertFalse('warning' in result) + self.assertTrue('type_selection_id' in result['domain']) + self.assertFalse(result['domain']['type_selection_id']) + # 2. Test call with company 2 person relation + relation = self._create_company2person_relation() + domain = relation.onchange_partner_id()['domain'] + self.assertTrue( + ('contact_type_this', '=', 'c') in domain['type_selection_id'] + ) + # 3. Test with invalid or impossible combinations + relation_nobody = self._get_empty_relation() + with self.env.do_in_draft(): + relation_nobody.this_partner_id = self.partner_02_company + relation_nobody.type_selection_id = self.selection_nobody + warning = relation_nobody.onchange_partner_id()['warning'] + self.assertTrue('message' in warning) + self.assertTrue('incompatible' in warning['message']) diff --git a/partner_relations/tests/test_partner_relation_common.py b/partner_relations/tests/test_partner_relation_common.py new file mode 100644 index 000000000..94249c6c7 --- /dev/null +++ b/partner_relations/tests/test_partner_relation_common.py @@ -0,0 +1,113 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 Therp BV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from openerp.tests import common + + +class TestPartnerRelationCommon(common.TransactionCase): + + def setUp(self): + super(TestPartnerRelationCommon, self).setUp() + + self.partner_model = self.env['res.partner'] + self.category_model = self.env['res.partner.category'] + self.type_model = self.env['res.partner.relation.type'] + self.selection_model = self.env['res.partner.relation.type.selection'] + self.relation_model = self.env['res.partner.relation'] + self.relation_all_model = self.env['res.partner.relation.all'] + self.partner_01_person = self.partner_model.create({ + 'name': 'Test User 1', + 'is_company': False, + 'ref': 'PR01', + }) + self.partner_02_company = self.partner_model.create({ + 'name': 'Test Company', + 'is_company': True, + 'ref': 'PR02', + }) + # Create partners with specific categories: + self.category_01_ngo = self.category_model.create({ + 'name': 'NGO', + }) + self.partner_03_ngo = self.partner_model.create({ + 'name': 'Test NGO', + 'is_company': True, + 'ref': 'PR03', + 'category_id': [(4, self.category_01_ngo.id)], + }) + self.category_02_volunteer = self.category_model.create({ + 'name': 'Volunteer', + }) + self.partner_04_volunteer = self.partner_model.create({ + 'name': 'Test Volunteer', + 'is_company': False, + 'ref': 'PR04', + 'category_id': [(4, self.category_02_volunteer.id)], + }) + # Create a new relation type withouth categories: + (self.type_company2person, + self.selection_company2person, + self.selection_person2company) = ( + self._create_relation_type_selection({ + 'name': 'mixed', + 'name_inverse': 'mixed_inverse', + 'contact_type_left': 'c', + 'contact_type_right': 'p', + }) + ) + # Create a new relation type with categories: + (self.type_ngo2volunteer, + self.selection_ngo2volunteer, + self.selection_volunteer2ngo) = ( + self._create_relation_type_selection({ + 'name': 'NGO has volunteer', + 'name_inverse': 'volunteer works for NGO', + 'contact_type_left': 'c', + 'contact_type_right': 'p', + 'partner_category_left': self.category_01_ngo.id, + 'partner_category_right': self.category_02_volunteer.id, + }) + ) + + def _create_relation_type_selection(self, vals): + """Create relation type and return this with selection types.""" + assert 'name' in vals, ( + "Name missing in vals to create relation type. Vals: %s." + % vals + ) + assert 'name' in vals, ( + "Name_inverse missing in vals to create relation type. Vals: %s." + % vals + ) + new_type = self.type_model.create(vals) + self.assertTrue( + new_type, + msg="No relation type created with vals %s." % vals + ) + selection_types = self.selection_model.search([ + ('type_id', '=', new_type.id), + ]) + for st in selection_types: + if st.is_inverse: + inverse_type_selection = st + else: + type_selection = st + self.assertTrue( + inverse_type_selection, + msg="Failed to find inverse type selection based on" + " relation type created with vals %s." % vals + ) + self.assertTrue( + type_selection, + msg="Failed to find type selection based on" + " relation type created with vals %s." % vals + ) + return (new_type, type_selection, inverse_type_selection) + + def _create_company2person_relation(self): + """Utility function to get a relation from company 2 partner.""" + return self.relation_all_model.create({ + 'type_selection_id': self.selection_company2person.id, + 'this_partner_id': self.partner_02_company.id, + 'other_partner_id': self.partner_01_person.id, + }) diff --git a/partner_relations/tests/test_partner_search.py b/partner_relations/tests/test_partner_search.py new file mode 100644 index 000000000..153b35b78 --- /dev/null +++ b/partner_relations/tests/test_partner_search.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- +# Copyright 2015 Camptocamp SA +# Copyright 2016 Therp BV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from openerp import fields +from openerp.exceptions import ValidationError + +from .test_partner_relation_common import TestPartnerRelationCommon + + +class TestPartnerSearch(TestPartnerRelationCommon): + + def test_search_relation_type(self): + """Test searching on relation type.""" + relation = self._create_company2person_relation() + partners = self.partner_model.search([ + ('search_relation_type_id', '=', relation.type_selection_id.id) + ]) + self.assertTrue(self.partner_02_company in partners) + partners = self.partner_model.search([ + ('search_relation_type_id', '!=', relation.type_selection_id.id) + ]) + self.assertTrue(self.partner_01_person in partners) + partners = self.partner_model.search([ + ('search_relation_type_id', '=', self.type_company2person.name) + ]) + self.assertTrue(self.partner_01_person in partners) + self.assertTrue(self.partner_02_company in partners) + partners = self.partner_model.search([ + ('search_relation_type_id', '=', 'unknown relation') + ]) + self.assertFalse(partners) + # Check error with invalid search operator: + with self.assertRaises(ValidationError): + partners = self.partner_model.search([ + ('search_relation_type_id', 'child_of', 'some parent') + ]) + + def test_search_relation_partner(self): + """Test searching on related partner.""" + self._create_company2person_relation() + partners = self.partner_model.search([ + ('search_relation_partner_id', '=', self.partner_02_company.id), + ]) + self.assertTrue(self.partner_01_person in partners) + + def test_search_relation_date(self): + """Test searching on relations valid on a certain date.""" + self._create_company2person_relation() + partners = self.partner_model.search([ + ('search_relation_date', '=', fields.Date.today()), + ]) + self.assertTrue(self.partner_01_person in partners) + self.assertTrue(self.partner_02_company in partners) + + def test_search_any_partner(self): + """Test searching for partner left or right.""" + self._create_company2person_relation() + both_relations = self.relation_all_model.search([ + ('any_partner_id', '=', self.partner_02_company.id), + ]) + self.assertEqual(len(both_relations), 2) + + def test_search_partner_category(self): + """Test searching for partners related to partners having category.""" + relation_ngo_volunteer = self.relation_all_model.create({ + 'this_partner_id': self.partner_03_ngo.id, + 'type_selection_id': self.selection_ngo2volunteer.id, + 'other_partner_id': self.partner_04_volunteer.id, + }) + self.assertTrue(relation_ngo_volunteer) + partners = self.partner_model.search([ + ('search_relation_partner_category_id', '=', + self.category_02_volunteer.id) + ]) + self.assertTrue(self.partner_03_ngo in partners)