From b29bbf1ff424b58c954055233f7bdd6fa6114546 Mon Sep 17 00:00:00 2001 From: Ronald Portier Date: Fri, 31 Aug 2018 18:50:52 +0200 Subject: [PATCH] [IMP] partner_relations_in_tab. Clean Code. --- partner_relations_in_tab/__init__.py | 21 +- partner_relations_in_tab/__openerp__.py | 4 +- partner_relations_in_tab/model/res_partner.py | 362 ++++++++++-------- .../model/res_partner_relation_type.py | 86 +---- partner_relations_in_tab/tests/__init__.py | 8 + .../tests/test_partner_tabs.py | 124 ++++++ 6 files changed, 356 insertions(+), 249 deletions(-) create mode 100644 partner_relations_in_tab/tests/__init__.py create mode 100644 partner_relations_in_tab/tests/test_partner_tabs.py diff --git a/partner_relations_in_tab/__init__.py b/partner_relations_in_tab/__init__.py index fcf92314d..4488d640a 100644 --- a/partner_relations_in_tab/__init__.py +++ b/partner_relations_in_tab/__init__.py @@ -1,21 +1,4 @@ # -*- 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 . -# -############################################################################## +# Copyright 2014-2018 Therp BV . +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). from . import model diff --git a/partner_relations_in_tab/__openerp__.py b/partner_relations_in_tab/__openerp__.py index c561be0a5..5dd497e91 100644 --- a/partner_relations_in_tab/__openerp__.py +++ b/partner_relations_in_tab/__openerp__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# © 2014-2017 Therp BV . -# License AGPL-3.0 or later . +# Copyright 2014-2018 Therp BV . +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). { "name": "Show partner relations in own tab", "version": "7.0.1.0.0", diff --git a/partner_relations_in_tab/model/res_partner.py b/partner_relations_in_tab/model/res_partner.py index ab7b3e57a..618fe0663 100644 --- a/partner_relations_in_tab/model/res_partner.py +++ b/partner_relations_in_tab/model/res_partner.py @@ -1,179 +1,215 @@ # -*- 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 . -# -############################################################################## +# Copyright 2014-2018 Therp BV . +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +import logging from lxml import etree + from openerp.osv.orm import Model, transfer_modifiers_to_node -from openerp.osv import expression +from openerp.osv import expression, fields from openerp.tools.translate import _ - - -class ResPartner(Model): - _inherit = 'res.partner' - - def _get_relation_ids_select(self, cr, uid, ids, field_name, arg, - context=None): - cr.execute( - '''select r.id, left_partner_id, right_partner_id - from res_partner_relation r - join res_partner_relation_type t - on r.type_id = t.id - where ((left_partner_id in %s and own_tab_left=False) - or (right_partner_id in %s and own_tab_right=False))''' + - ' order by ' + self.pool['res.partner.relation']._order, - (tuple(ids), tuple(ids)) - ) - return cr.fetchall() - - def _create_relation_type_tab( - self, cr, uid, rel_type, inverse, field_names, context=None): - '''Create an xml node containing the relation's tab to be added to the - view. Add the field(s) created on the form to field_names.''' - name = rel_type.name if not inverse else rel_type.name_inverse - contact_type = rel_type['contact_type_' + - ('left' if not inverse else 'right')] - partner_category = rel_type['partner_category_' + - ('left' if not inverse - else 'right')] - tab = etree.Element('page') - tab.set('string', name) - - invisible = [('id', '=', False)] - if contact_type: - invisible = expression.OR([ - invisible, - [('is_company', '=', contact_type != 'c')]]) - if partner_category: - invisible = expression.OR([ - invisible, - [('category_id', '!=', partner_category.id)]]) - attrs = { - 'invisible': invisible, - } - tab.set('attrs', repr(attrs)) - transfer_modifiers_to_node(attrs, tab) - - field_name = 'relation_ids_own_tab_%s_%s' % ( - rel_type.id, - 'left' if not inverse else 'right') - field_names.append(field_name) - this_partner_name = '%s_partner_id' % ( - 'left' if not inverse else 'right') - other_partner_name = '%s_partner_id' % ( - 'left' if inverse else 'right') - +from openerp import SUPERUSER_ID + + +_logger = logging.getLogger(__name__) # pylint: disable=invalid-name +NAME_PREFIX = 'relation_ids_tab' + + +class Tab(object): + + def __init__(self, source, side): + """Create tab from source. + + In this version source can be assumed to be a partner.relation.type. + """ + self.id = source.id + self.side = side + if side == 'left': + self.name = source.name + self.contact_type = source.contact_type_left + self.category_id = source.partner_category_left + self.other_contact_type = source.contact_type_right + self.other_category_id = source.partner_category_right + self.other_side = 'right' + else: + self.name = source.name_inverse + self.contact_type = source.contact_type_right + self.category_id = source.partner_category_right + self.other_contact_type = source.contact_type_left + self.other_category_id = source.partner_category_left + self.other_side = 'left' + + def get_fieldname(self): + return '%s_%s_%s' % (NAME_PREFIX, self.id, self.side) + + def get_domain(self): + return [('type_id', '=', self.id)] + + def create_page(self): + tab_page = etree.Element('page') + self._set_page_attrs(tab_page) field = etree.Element( 'field', - name=field_name, - context=('{"default_type_id": %s, "default_%s": id, ' - '"active_test": False}') % ( - rel_type.id, - this_partner_name)) - tab.append(field) + name=self.get_fieldname(), + context=( + '{"default_type_id": %s, "default_%s_partner_id": id, ' + '"active_test": False}') % (self.id, self.side)) + tab_page.append(field) tree = etree.Element('tree', editable='bottom') field.append(tree) - - onchange_type_values = self.pool['res.partner.relation']\ - .on_change_type_selection_id(cr, uid, None, - rel_type.id * 10 + - (1 if inverse else 0), - context=context) + tree.append(etree.Element( + 'field', name='%s_partner_id' % self.side, invisible='True')) tree.append(etree.Element( 'field', string=_('Partner'), - domain=repr( - onchange_type_values['domain']['partner_id_display']), + domain=repr(self._get_other_partner_domain()), widget='many2one_clickable', - name=other_partner_name)) - tree.append(etree.Element( - 'field', - name='date_start')) - tree.append(etree.Element( - 'field', - name='date_end')) - tree.append(etree.Element( - 'field', - name='active')) - tree.append(etree.Element('field', name='type_id', - invisible='True')) - tree.append(etree.Element('field', name=this_partner_name, - invisible='True')) - return tab - - def _add_relation_type_tab( - self, cr, uid, rel_type, inverse, field_names, relation_tab, - context=None): - '''add the xml node to the view''' - tab = self._create_relation_type_tab( - cr, uid, rel_type, inverse, field_names, context=context) - relation_tab.addnext(tab) - - def fields_view_get(self, cr, uid, view_id=None, view_type='form', - context=None, toolbar=False, submenu=False): - if context is None: - context = {} + name='%s_partner_id' % self.other_side)) + tree.append(etree.Element('field', name='date_start')) + tree.append(etree.Element('field', name='date_end')) + tree.append(etree.Element('field', name='active')) + tree.append(etree.Element('field', name='type_id', invisible='True')) + return tab_page + + def _get_other_partner_domain(self): + partner_domain = [] + if self.other_contact_type == 'c': + partner_domain.append(('is_company', '=', True)) + if self.other_contact_type == 'p': + partner_domain.append(('is_company', '=', False)) + if self.other_category_id: + partner_domain.append( + ('category_id', 'child_of', self.other_category_id)) + return partner_domain + + def _set_page_attrs(self, tab_page): + tab_page.set('string', self.name) + invisible = [('id', '=', False)] + if self.contact_type: + invisible = expression.OR([ + invisible, + [('is_company', '=', self.contact_type != 'c')]]) + if self.category_id: + invisible = expression.OR([ + invisible, + [('category_id', '!=', self.category_id)]]) + attrs = {'invisible': invisible} + tab_page.set('attrs', repr(attrs)) + transfer_modifiers_to_node(attrs, tab_page) + + +class ResPartner(Model): + _inherit = 'res.partner' + + def _make_tab(self, source, side): + return Tab(source, side) + + def _register_hook(self, cr): + """This function is automatically called by Odoo on all models.""" + self._update_tab_fields(cr) + + def _update_tab_fields(self, cr): + """Create a field for each tab that might be shown for a partner.""" + deprecated_tab_fields = [ + name for name in self._columns.copy() + if name.startswith(NAME_PREFIX)] + tabs = self._get_tabs(cr) + for tab in tabs: + fieldname = tab.get_fieldname() + if fieldname in self._columns: + self._update_tab_field(tab) + else: + self._add_tab_field(tab) + if fieldname in deprecated_tab_fields: + deprecated_tab_fields.remove(fieldname) # not deprecated + for fieldname in deprecated_tab_fields: + self._delete_tab_field(fieldname) + + def _get_tabs(self, cr): + tabs = [] + relation_type_model = self.pool['res.partner.relation.type'] + relation_type_domain = [ + '|', + ('own_tab_left', '=', True), + ('own_tab_right', '=', True)] + relation_type_ids = relation_type_model.search( + cr, SUPERUSER_ID, relation_type_domain) + for relation_type in relation_type_model.browse( + cr, SUPERUSER_ID, relation_type_ids): + if relation_type.own_tab_left: + new_tab = Tab(relation_type, 'left') + tabs.append(new_tab) + if relation_type.own_tab_right: + new_tab = Tab(relation_type, 'right') + tabs.append(new_tab) + return tabs + + def _add_tab_field(self, tab): + field = fields.one2many( + 'res.partner.relation', + '%s_partner_id' % tab.side, + string=tab.name, + domain=tab.get_domain()) + fieldname = tab.get_fieldname() + _logger.info(_( + "Adding field %s to res.partner model.") % fieldname) + self._columns[fieldname] = field + + def _update_tab_field(self, tab): + fieldname = tab.get_fieldname() + _logger.info(_( + "Updating field %s in res.partner model.") % fieldname) + self._columns[fieldname].string = tab.name + + def _delete_tab_field(self, fieldname): + _logger.info(_( + "Deleting field %s from res.partner model.") % fieldname) + del self._columns[fieldname] + + def fields_view_get( + self, cr, uid, view_id=None, view_type='form', context=None, + toolbar=False, submenu=False): + context = context or {} result = super(ResPartner, self).fields_view_get( cr, uid, view_id=view_id, view_type=view_type, context=context, toolbar=toolbar, submenu=submenu) - if view_type == 'form' and not context.get('check_view_ids'): - res_partner_relation_type = self.pool['res.partner.relation.type'] - own_tab_types = res_partner_relation_type.browse( - cr, uid, - res_partner_relation_type.search( - cr, uid, - [ - '|', - ('own_tab_left', '=', True), - ('own_tab_right', '=', True) - ], - context=context), - context=context) - view = etree.fromstring(result['arch']) - - relation_tab = view.xpath( - '//field[@name="relation_ids"]/ancestor::page') - if not relation_tab: - return result - relation_tab = relation_tab[0] - - field_names = [] - - if not view.xpath('//field[@name="id"]'): - view.append(etree.Element('field', name='id', - invisible='True')) - field_names.append('id') - - for rel_type in own_tab_types: - if rel_type.own_tab_left: - self._add_relation_type_tab( - cr, uid, rel_type, False, field_names, relation_tab, - context=context) - if rel_type.own_tab_right: - self._add_relation_type_tab( - cr, uid, rel_type, True, field_names, relation_tab, - context=context) - - result['arch'], fields = self\ - ._BaseModel__view_look_dom_arch( - cr, uid, view, result['view_id'], context=context) - - for field_name in field_names: - result['fields'][field_name] = fields[field_name] - + if view_type != 'form' or context.get('check_view_ids'): + return result + view = etree.fromstring(result['arch']) + extra_fields = self._add_tab_pages(cr, view) + result['arch'], view_fields = self._BaseModel__view_look_dom_arch( + cr, uid, view, result['view_id'], context=context) + for fieldname in extra_fields: + result['fields'][fieldname] = view_fields[fieldname] return result + + def _add_tab_pages(self, cr, view): + """Adds the relevant tabs to the partner's formview.""" + extra_fields = [] + if not view.xpath('//field[@name="id"]'): + view.append( + etree.Element('field', name='id', invisible='True')) + extra_fields.append('id') + element_last_page_hook = view.xpath('//page[last()]')[0] + for tab in self._get_tabs(cr): + fieldname = tab.get_fieldname() + extra_fields.append(fieldname) + tab_page = tab.create_page() + _logger.debug( + _("Adding %s tab %s with arch: %s"), + tab.side, tab.name, etree.tostring(tab_page)) + element_last_page_hook.addnext(tab_page) + return extra_fields + + def _get_relation_ids_select( + self, cr, uid, ids, fieldname, arg, context=None): + """Overide domain for other partner on default relations tab.""" + cr.execute( + """select r.id, left_partner_id, right_partner_id + from res_partner_relation r + join res_partner_relation_type t + on r.type_id = t.id + where ((left_partner_id in %s and own_tab_left=False) + or (right_partner_id in %s and own_tab_right=False))""" + + ' order by ' + self.pool['res.partner.relation']._order, + (tuple(ids), tuple(ids))) + return cr.fetchall() diff --git a/partner_relations_in_tab/model/res_partner_relation_type.py b/partner_relations_in_tab/model/res_partner_relation_type.py index 6a04d33f8..8ec85515f 100644 --- a/partner_relations_in_tab/model/res_partner_relation_type.py +++ b/partner_relations_in_tab/model/res_partner_relation_type.py @@ -1,26 +1,8 @@ # -*- 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 . -# -############################################################################## +# Copyright 2014-2018 Therp BV . +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). from openerp.osv.orm import Model from openerp.osv import fields -from openerp import SUPERUSER_ID class ResPartnerRelationType(Model): @@ -36,55 +18,29 @@ class ResPartnerRelationType(Model): 'own_tab_right': False, } - def _update_res_partner_fields(self, cr): - field_name_prefix = 'relation_ids_own_tab_' - field_name_format = field_name_prefix + '%s_%s' - res_partner = self.pool['res.partner'] - for field_name in res_partner._columns.copy(): - if field_name.startswith(field_name_prefix): - del res_partner._columns[field_name] - - def add_field(relation, inverse): - field = fields.one2many( - 'res.partner.relation', - '%s_partner_id' % ('left' if not inverse else 'right'), - string=relation['name' if not inverse else 'name_inverse'], - domain=[('type_id', '=', relation.id), - '|', - ('active', '=', True), - ('active', '=', False)]) - field_name = field_name_format % ( - relation.id, - 'left' if not inverse else 'right') - res_partner._columns[field_name] = field - - for relation in self.browse( - cr, SUPERUSER_ID, - self.search( - cr, SUPERUSER_ID, - [ - '|', - ('own_tab_left', '=', True), - ('own_tab_right', '=', True), - ])): - if relation.own_tab_left: - add_field(relation, False) - if relation.own_tab_right: - add_field(relation, True) - - def _register_hook(self, cr): - self._update_res_partner_fields(cr) - def create(self, cr, uid, vals, context=None): - result = super(ResPartnerRelationType, self).create( + relation_type_id = super(ResPartnerRelationType, self).create( cr, uid, vals, context=context) - if vals.get('own_tab_left') or vals.get('own_tab_right'): - self._update_res_partner_fields(cr) - return result + relation_type = self.browse(cr, uid, relation_type_id, context=context) + partner_model = self.pool['res.partner'] + if relation_type.own_tab_left: + tab = partner_model._make_tab(relation_type, 'left') + partner_model._add_tab_field(tab) + if relation_type.own_tab_right: + tab = partner_model._make_tab(relation_type, 'right') + partner_model._add_tab_field(tab) + return relation_type_id def write(self, cr, uid, ids, vals, context=None): result = super(ResPartnerRelationType, self).write( cr, uid, ids, vals, context=context) - if 'own_tab_left' in vals or 'own_tab_right' in vals: - self._update_res_partner_fields(cr) + partner_model = self.pool['res.partner'] + partner_model._update_tab_fields(cr) + return result + + def unlink(self, cr, uid, ids, context=None): + result = super(ResPartnerRelationType, self).unlink( + cr, uid, ids, context=context) + partner_model = self.pool['res.partner'] + partner_model._update_tab_fields(cr) return result diff --git a/partner_relations_in_tab/tests/__init__.py b/partner_relations_in_tab/tests/__init__.py new file mode 100644 index 000000000..fd28fe552 --- /dev/null +++ b/partner_relations_in_tab/tests/__init__.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +# Copyright 2017-2018 Therp BV . +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from . import test_partner_tabs + +checks = [ + test_partner_tabs, +] diff --git a/partner_relations_in_tab/tests/test_partner_tabs.py b/partner_relations_in_tab/tests/test_partner_tabs.py new file mode 100644 index 000000000..cd7c85f05 --- /dev/null +++ b/partner_relations_in_tab/tests/test_partner_tabs.py @@ -0,0 +1,124 @@ +# -*- coding: utf-8 -*- +# Copyright 2014-2018 Therp BV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from lxml import etree + +from openerp.tests import common + + +class TestPartnerTabs(common.SingleTransactionCase): + + post_install = True + + def _get_tab_fieldname(self, relation_type, side): + tab = self._get_tab(relation_type, side) + return tab.get_fieldname() + + def _get_tab(self, relation_type, side): + partner_model = self.registry('res.partner') + return partner_model._make_tab(relation_type, side) + + def test_create_tab(self): + cr, uid = self.cr, self.uid + type_model = self.registry('res.partner.relation.type') + partner_model = self.registry('res.partner') + type_has_chairperson_id = type_model.create( + cr, uid, { + 'name': 'has chairperson', + 'name_inverse': 'is chairperson for', + 'contact_type_left': 'c', + 'own_tab_left': True, + 'contact_type_right': 'p'}) + type_has_chairperson = type_model.browse( + cr, uid, type_has_chairperson_id) + self.assertTrue(bool(type_has_chairperson)) + # There should now be a field in res_partner for the new tab: + fieldname = self._get_tab_fieldname(type_has_chairperson, 'left') + self.assertTrue(fieldname in partner_model._columns) + # The form view for partner should now also contain the tab: + view = partner_model.fields_view_get(cr, uid, view_type='form') + tree = etree.fromstring(view['arch']) + field = tree.xpath('//field[@name="id"]') + self.assertTrue(field, 'Id field does not exist.') + # And we should have a field for the tab: + field = tree.xpath('//field[@name="%s"]' % fieldname) + self.assertTrue( + field, + 'Tab field %s does not exist in %s.' % + (fieldname, etree.tostring(tree))) + # There should be no effect on the tree view: + view = partner_model.fields_view_get(cr, uid, view_type='tree') + tree = etree.fromstring(view['arch']) + field = tree.xpath('//field[@name="%s"]' % fieldname) + self.assertFalse( + field, + 'Tab field %s should not exist in %s.' % + (fieldname, etree.tostring(tree))) + + def test_relations(self): + """Test relations shown on tab.""" + cr, uid = self.cr, self.uid + type_model = self.registry('res.partner.relation.type') + relation_model = self.registry('res.partner.relation') + partner_model = self.registry('res.partner') + type_has_chairperson_id = type_model.create( + cr, uid, { + 'name': 'has chairperson', + 'name_inverse': 'is chairperson for', + 'contact_type_left': 'c', + 'own_tab_left': True, + 'contact_type_right': 'p'}) + type_has_chairperson = type_model.browse( + cr, uid, type_has_chairperson_id) + self.assertTrue(bool(type_has_chairperson)) + big_company_id = partner_model.create( + cr, uid, { + 'name': 'Big company', + 'is_company': True, + 'ref': 'BIG'}) + big_company = partner_model.browse(cr, uid, big_company_id) + self.assertTrue(bool(big_company)) + important_person_id = partner_model.create( + cr, uid, { + 'name': 'Bart Simpson', + 'is_company': False, + 'ref': 'BS'}) + important_person = partner_model.browse(cr, uid, important_person_id) + self.assertTrue(bool(important_person)) + relation_company_chair_id = relation_model.create( + cr, uid, { + 'left_partner_id': big_company.id, + 'type_id': type_has_chairperson.id, + 'right_partner_id': important_person.id}) + relation_company_chair = relation_model.browse( + cr, uid, relation_company_chair_id) + self.assertTrue(bool(relation_company_chair)) + # There should now be a field in res_partner for the new tab: + fieldname = self._get_tab_fieldname(type_has_chairperson, 'left') + self.assertTrue(fieldname in partner_model._columns) + # We should find the chairperson of the company through the tab: + executive_partners = big_company[fieldname] + self.assertEqual(len(executive_partners), 1) + self.assertEqual( + executive_partners[0].right_partner_id.id, + important_person.id) + + def test_update_tabs(self): + """Test the function that will create tabs during module loading.""" + cr, uid = self.cr, self.uid + type_model = self.registry('res.partner.relation.type') + partner_model = self.registry('res.partner') + type_has_chairperson_id = type_model.create( + cr, uid, { + 'name': 'has chairperson', + 'name_inverse': 'is chairperson for', + 'contact_type_left': 'c', + 'own_tab_left': True, + 'contact_type_right': 'p'}) + type_has_chairperson = type_model.browse( + cr, uid, type_has_chairperson_id) + # Now call hook method + partner_model._register_hook(cr) + # There should now be a field in res_partner for the new tab: + fieldname = self._get_tab_fieldname(type_has_chairperson, 'left') + self.assertTrue(fieldname in partner_model._columns)