Browse Source

[IMP] partner_multi_relations_tabs. Clean code principles.

- Separate model (db) partially from user interface;
- Use demo data for performance, cleaner tests, and front end testing;
- Make sure tabs dependent on partner_category_id work.
12.0
Ronald Portier 6 years ago
committed by Jan Verbeek
parent
commit
e3b62719b6
  1. 4
      partner_multi_relation_tabs/__init__.py
  2. 7
      partner_multi_relation_tabs/__manifest__.py
  3. 13
      partner_multi_relation_tabs/demo/res_partner_category_demo.xml
  4. 28
      partner_multi_relation_tabs/demo/res_partner_demo.xml
  5. 11
      partner_multi_relation_tabs/demo/res_partner_relation_demo.xml
  6. 30
      partner_multi_relation_tabs/demo/res_partner_relation_type_demo.xml
  7. 24
      partner_multi_relation_tabs/demo/res_partner_tab_demo.xml
  8. 195
      partner_multi_relation_tabs/models/res_partner.py
  9. 11
      partner_multi_relation_tabs/models/res_partner_relation_all.py
  10. 2
      partner_multi_relation_tabs/models/res_partner_relation_type.py
  11. 24
      partner_multi_relation_tabs/models/res_partner_tab.py
  12. 4
      partner_multi_relation_tabs/tablib/__init__.py
  13. 84
      partner_multi_relation_tabs/tablib/tab.py
  14. 5
      partner_multi_relation_tabs/tests/__init__.py
  15. 47
      partner_multi_relation_tabs/tests/common.py
  16. 214
      partner_multi_relation_tabs/tests/test_partner_tabs.py
  17. 31
      partner_multi_relation_tabs/tests/test_tab.py

4
partner_multi_relation_tabs/__init__.py

@ -1,4 +1,4 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright 2014-2017 Therp BV <https://therp.nl>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from . import tablib
from . import models from . import models

7
partner_multi_relation_tabs/__manifest__.py

@ -12,6 +12,13 @@
'web_tree_many2one_clickable', 'web_tree_many2one_clickable',
'partner_multi_relation', 'partner_multi_relation',
], ],
"demo": [
"demo/res_partner_category_demo.xml",
"demo/res_partner_tab_demo.xml",
"demo/res_partner_demo.xml",
"demo/res_partner_relation_type_demo.xml",
"demo/res_partner_relation_demo.xml",
],
"data": [ "data": [
"views/res_partner_tab.xml", "views/res_partner_tab.xml",
"views/res_partner_relation_type.xml", "views/res_partner_relation_type.xml",

13
partner_multi_relation_tabs/demo/res_partner_category_demo.xml

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<!-- Categories -->
<record id="category_government" model="res.partner.category">
<field name="name">Government</field>
</record>
<record id="category_functionary" model="res.partner.category">
<field name="name">Functionary</field>
</record>
</odoo>

28
partner_multi_relation_tabs/demo/res_partner_demo.xml

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<!-- Partners -->
<record id="partner_big_company" model="res.partner">
<field name="name">Big company</field>
<field name="is_company">1</field>
<field name="customer" eval="1"/>
<field name="ref">BIG</field>
</record>
<record id="partner_important_person" model="res.partner">
<field
name="category_id"
eval="[(6,0,[ref('category_functionary')])]"
/>
<field name="name">Bart Simpson</field>
<field name="is_company">0</field>
<field name="ref">BS</field>
</record>
<record id="partner_common_person" model="res.partner">
<field name="name">Homer Simpson</field>
<field name="is_company">0</field>
<field name="ref">HS</field>
</record>
</odoo>

11
partner_multi_relation_tabs/demo/res_partner_relation_demo.xml

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<!-- Relations -->
<record id="relation_company_ceo" model="res.partner.relation">
<field name="left_partner_id" ref="partner_big_company" />
<field name="right_partner_id" ref="partner_important_person" />
<field name="type_id" ref="relation_type_company_has_ceo" />
</record>
</odoo>

30
partner_multi_relation_tabs/demo/res_partner_relation_type_demo.xml

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<!-- Company has CEO: organisation to functionary person -->
<record
id="relation_type_company_has_ceo"
model="res.partner.relation.type">
<field name="name">has ceo</field>
<field name="name_inverse">is ceo of</field>
<field name="contact_type_left">c</field>
<field name="tab_left_id" ref="tab_board" />
<field name="contact_type_right">p</field>
<field name="partner_category_right" ref="category_functionary"/>
</record>
<!-- Committee has a chairperson -->
<record
id="relation_type_committee_has_chairperson"
model="res.partner.relation.type">
<field name="name">has chairperson</field>
<field name="name_inverse">is chairperson of</field>
<field name="contact_type_left">c</field>
<field name="tab_left_id" ref="tab_committee" />
<field name="contact_type_right">p</field>
<field name="partner_category_left" ref="category_government"/>
<field name="partner_category_right" ref="category_functionary"/>
<field name="tab_right_id" ref="tab_positions" />
</record>
</odoo>

24
partner_multi_relation_tabs/demo/res_partner_tab_demo.xml

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id="tab_committee" model="res.partner.tab">
<field name="code">committee</field>
<field name="name">Government committee</field>
<field name="contact_type">c</field>
<field name="partner_category_id" ref="category_government" />
</record>
<record id="tab_board" model="res.partner.tab">
<field name="code">board</field>
<field name="name">Company executive board</field>
<field name="contact_type">c</field>
</record>
<record id="tab_positions" model="res.partner.tab">
<field name="code">positions</field>
<field name="name">Positions held</field>
<field name="contact_type">p</field>
<field name="partner_category_id" ref="category_functionary" />
</record>
</odoo>

195
partner_multi_relation_tabs/models/res_partner.py

@ -1,142 +1,27 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright 2014-2018 Therp BV <https://therp.nl> # Copyright 2014-2018 Therp BV <https://therp.nl>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
# pylint: disable=no-member
import logging import logging
from lxml import etree from lxml import etree
from odoo.osv.orm import transfer_modifiers_to_node
from odoo.osv import expression
from odoo import _, api, fields, models
from odoo import api, fields, models
_logger = logging.getLogger(__name__)
NAME_PREFIX = 'relation_ids_tab'
_logger = logging.getLogger(__name__) # pylint: disable=invalid-name
class ResPartner(models.Model): class ResPartner(models.Model):
_inherit = 'res.partner' _inherit = 'res.partner'
def _get_tab_fieldname(self, tab):
"""Create fieldname for tab."""
return '%s_%s' % (NAME_PREFIX, tab.id)
@api.model
def _add_tab_field(self, tab):
fieldname = self._get_tab_fieldname(tab)
_logger.info(_(
"Adding field %s to res.partner model." % fieldname))
field = fields.One2many(
comodel_name='res.partner.relation.all',
inverse_name='this_partner_id',
domain=[('tab_id', '=', tab.id)],
string=tab.name)
self._add_field(fieldname, field)
@api.model
def _update_tab_field(self, tab):
fieldname = self._get_tab_fieldname(tab)
@api.multi
def browse(self, arg=None, prefetch=None):
for tab in self._get_tabs():
fieldname = tab.get_fieldname()
if fieldname not in self._fields: if fieldname not in self._fields:
return self._add_tab_field(tab)
_logger.info(_(
"Updating field %s in res.partner model." % fieldname))
self._fields[fieldname].string = tab.name
@api.model
def _delete_tab_field(self, fieldname):
_logger.info(_(
"deleting field %s from res.partner model." % fieldname))
self._pop_field(fieldname)
@api.model
def _update_tab_fields(self):
"""Create a field for each tab that might be shown for a partner."""
deprecated_tab_fields = [
name for name in self._fields
if name.startswith(NAME_PREFIX)]
tab_model = self.env['res.partner.tab']
for tab in tab_model.search([]): # get all tabs
fieldname = self._get_tab_fieldname(tab)
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 _register_hook(self):
self._update_tab_fields()
def _create_tab_page(self, fieldname, tab):
"""Create an xml node containing the tab page to be added view."""
# pylint: disable=no-member
tab_page = etree.Element('page')
invisible = [('id', '=', False)] # Partner not created yet
if tab.partner_ids:
invisible = expression.OR([
invisible,
[('id', 'not in', tab.partner_ids.ids)]])
else:
if tab.contact_type:
invisible = expression.OR([
invisible,
[('is_company', '=', tab.contact_type != 'c')]])
if tab.partner_category_id:
invisible = expression.OR([
invisible,
[('category_id', '!=', tab.partner_category_id.id)]])
attrs = {'invisible': invisible}
tab_page.set('string', tab.name)
tab_page.set('attrs', repr(attrs))
transfer_modifiers_to_node(attrs, tab_page)
field = etree.Element(
'field',
name=fieldname,
context='{'
'"default_this_partner_id": id,'
'"default_tab_id": %d,'
'"active_test": False}' % tab.id)
tree = etree.Element('tree', editable='bottom')
# Now add fields for the editable tree view in the tab:
type_field = etree.Element(
'field',
name='type_selection_id',
widget='many2one_clickable')
type_field.set('domain', repr([('tab_id', '=', tab.id)]))
type_field.set('options', repr({'no_create': True}))
tree.append(type_field)
other_partner_field = etree.Element(
'field',
name='other_partner_id',
widget='many2one_clickable')
other_partner_field.set('options', repr({'no_create': True}))
tree.append(other_partner_field)
tree.append(etree.Element('field', name='date_start'))
tree.append(etree.Element('field', name='date_end'))
field.append(tree)
tab_page.append(field)
return tab_page
def _add_tab_pages(self, view):
"""Adds the relevant tabs to the partner's formview."""
# pylint: disable=no-member
last_page_nodes = view.xpath('//page[last()]')
if not last_page_nodes:
# Nothing to do if form contains no pages/tabs.
return []
extra_fields = []
if not view.xpath('//field[@name="id"]'):
view.append(
etree.Element('field', name='id', invisible='True'))
extra_fields.append('id')
last_page = last_page_nodes[0]
tab_model = self.env['res.partner.tab']
for tab in tab_model.search([]): # get all tabs
fieldname = self._get_tab_fieldname(tab)
self._update_tab_field(tab)
extra_fields.append(fieldname)
tab_page = self._create_tab_page(fieldname, tab)
last_page.addnext(tab_page)
last_page = tab_page # Keep ordering of tabs
return extra_fields
# Check this for performance reasons.
self.add_field(tab)
return super(ResPartner, self).browse(arg=arg, prefetch=prefetch)
@api.model @api.model
def fields_view_get( def fields_view_get(
@ -148,7 +33,7 @@ class ResPartner(models.Model):
submenu=submenu) submenu=submenu)
if view_type != 'form' or self.env.context.get('check_view_ids'): if view_type != 'form' or self.env.context.get('check_view_ids'):
return result return result
view = etree.fromstring(result['arch']) # pylint: disable=no-member
view = etree.fromstring(result['arch'])
extra_fields = self._add_tab_pages(view) extra_fields = self._add_tab_pages(view)
view_model = self.env['ir.ui.view'] view_model = self.env['ir.ui.view']
result['arch'], original_fields = view_model.postprocess_and_fields( result['arch'], original_fields = view_model.postprocess_and_fields(
@ -156,3 +41,61 @@ class ResPartner(models.Model):
for fieldname in extra_fields: for fieldname in extra_fields:
result['fields'][fieldname] = original_fields[fieldname] result['fields'][fieldname] = original_fields[fieldname]
return result return result
def _add_tab_pages(self, view):
"""Adds the relevant tabs to the partner's formview."""
# pylint: disable=no-member
def add_invisible_extra_field(view, extra_fields, fieldname):
"""Add invisible field to view."""
view.append(
etree.Element('field', name=fieldname, invisible='True'))
extra_fields.append(fieldname)
last_page_nodes = view.xpath('//page[last()]')
if not last_page_nodes:
# Nothing to do if form contains no pages/tabs.
return []
extra_fields = []
if not view.xpath('//field[@name="id"]'):
add_invisible_extra_field(view, extra_fields, 'id')
last_page = last_page_nodes[0]
for tab in self._get_tabs(): # get all tabs
self.add_field(tab)
add_invisible_extra_field(
view, extra_fields, tab.get_visible_fieldname())
extra_fields.append(tab.get_fieldname())
tab_page = tab.create_page()
last_page.addnext(tab_page)
last_page = tab_page # Keep ordering of tabs
return extra_fields
@api.depends('is_company', 'category_id')
def _compute_tabs_visibility(self):
"""Compute for all tabs wether they should be visible."""
for tab in self._get_tabs(): # get all tabs
for this in self:
this[tab.get_visible_fieldname()] = \
tab.compute_visibility(this)
def _get_tabs(self):
tab_model = self.env['res.partner.tab']
return tab_model.get_tabs()
def add_field(self, tab):
"""Add tab field to model.
Will replace existing field if already present.
"""
# Visible field determines wether first field will be visible.
# This is because domains on many2many no longer work in 9.0
# and above.
visible_field = fields.Boolean(compute='_compute_tabs_visibility')
self._add_field(tab.get_visible_fieldname(), visible_field)
if visible_field not in self._field_computed:
self._field_computed[visible_field] = [visible_field]
tab_field = fields.One2many(
comodel_name='res.partner.relation.all',
inverse_name='this_partner_id',
domain=[('tab_id', '=', tab.tab_record.id)],
string=tab.tab_record.name)
self._add_field(tab.get_fieldname(), tab_field)

11
partner_multi_relation_tabs/models/res_partner_relation_all.py

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright 2014-2017 Therp BV <http://therp.nl>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from openerp import api, fields, models
# Copyright 2014-2018 Therp BV <https://therp.nl>.
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from odoo import api, fields, models
class ResPartnerRelationAll(models.AbstractModel): class ResPartnerRelationAll(models.AbstractModel):
@ -16,6 +16,7 @@ class ResPartnerRelationAll(models.AbstractModel):
def _get_additional_view_fields(self): def _get_additional_view_fields(self):
"""Add tab_id to view fields.""" """Add tab_id to view fields."""
# pylint: disable=no-member
return ','.join([ return ','.join([
super(ResPartnerRelationAll, self)._get_additional_view_fields(), super(ResPartnerRelationAll, self)._get_additional_view_fields(),
"CASE" "CASE"
@ -26,6 +27,7 @@ class ResPartnerRelationAll(models.AbstractModel):
def _get_additional_tables(self): def _get_additional_tables(self):
"""Add res_partner_tab table to view.""" """Add res_partner_tab table to view."""
# pylint: disable=no-member
return ' '.join([ return ' '.join([
super(ResPartnerRelationAll, self)._get_additional_tables(), super(ResPartnerRelationAll, self)._get_additional_tables(),
"LEFT OUTER JOIN res_partner_tab lefttab" "LEFT OUTER JOIN res_partner_tab lefttab"
@ -40,9 +42,10 @@ class ResPartnerRelationAll(models.AbstractModel):
def onchange_partner_id(self): def onchange_partner_id(self):
"""Add tab if needed to type_selection_id domain. """Add tab if needed to type_selection_id domain.
This method makes sure then when a relation is added to a tab,
This method makes sure that when a relation is added to a tab,
it is with a relation type meant to be placed on that tab. it is with a relation type meant to be placed on that tab.
""" """
# pylint: disable=no-member
result = super(ResPartnerRelationAll, self).onchange_partner_id() result = super(ResPartnerRelationAll, self).onchange_partner_id()
if 'default_tab_id' in self.env.context: if 'default_tab_id' in self.env.context:
if 'domain' not in result: if 'domain' not in result:

2
partner_multi_relation_tabs/models/res_partner_relation_type.py

@ -1,11 +1,13 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright 2014-2017 Therp BV <https://therp.nl> # Copyright 2014-2017 Therp BV <https://therp.nl>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
# pylint: disable=no-self-use
from odoo import _, api, fields, models from odoo import _, api, fields, models
from odoo.exceptions import ValidationError from odoo.exceptions import ValidationError
class ResPartnerRelationType(models.Model): class ResPartnerRelationType(models.Model):
# pylint: disable=too-few-public-methods
_inherit = 'res.partner.relation.type' _inherit = 'res.partner.relation.type'
tab_left_id = fields.Many2one( tab_left_id = fields.Many2one(

24
partner_multi_relation_tabs/models/res_partner_tab.py

@ -1,9 +1,11 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright 2017-2018 Therp BV <https://therp.nl>. # Copyright 2017-2018 Therp BV <https://therp.nl>.
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from odoo import _, api, fields, models from odoo import _, api, fields, models
from odoo.exceptions import ValidationError from odoo.exceptions import ValidationError
from ..tablib import Tab
class ResPartnerTab(models.Model): class ResPartnerTab(models.Model):
"""Model that defines relation types that might exist between partners""" """Model that defines relation types that might exist between partners"""
@ -48,13 +50,6 @@ class ResPartnerTab(models.Model):
raise ValidationError(_( raise ValidationError(_(
"You can not both specify partner_ids and other criteria.")) "You can not both specify partner_ids and other criteria."))
@api.model
def create(self, vals):
new_tab = super(ResPartnerTab, self).create(vals)
partner_model = self.env['res.partner']
partner_model._add_tab_field(new_tab)
return new_tab
@api.multi @api.multi
def update_types(self, vals=None): def update_types(self, vals=None):
"""Update types on write or unlink. """Update types on write or unlink.
@ -86,17 +81,16 @@ class ResPartnerTab(models.Model):
vals.get('partner_category_id', False): vals.get('partner_category_id', False):
self.update_types(vals) self.update_types(vals)
result = super(ResPartnerTab, self).write(vals) result = super(ResPartnerTab, self).write(vals)
partner_model = self.env['res.partner']
for this in self:
partner_model._update_tab_field(this)
return result return result
@api.multi @api.multi
def unlink(self): def unlink(self):
"""Unlink should first remove references.""" """Unlink should first remove references."""
self.update_types() self.update_types()
partner_model = self.env['res.partner']
for this in self:
fieldname = partner_model._get_tab_fieldname(this)
partner_model._delete_tab_field(fieldname)
return super(ResPartnerTab, self).unlink() return super(ResPartnerTab, self).unlink()
@api.model
def get_tabs(self):
"""Convert information on tabs in database to array of objects."""
tabs = [Tab(tab_record) for tab_record in self.search([])]
return tabs

4
partner_multi_relation_tabs/tablib/__init__.py

@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Copyright 2018 Therp BV <https://therp.nl>.
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from .tab import Tab

84
partner_multi_relation_tabs/tablib/tab.py

@ -0,0 +1,84 @@
# -*- coding: utf-8 -*-
# Copyright 2014-2018 Therp BV <https://therp.nl>.
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
import logging
from lxml import etree
from odoo import _
from odoo.osv.orm import transfer_modifiers_to_node
_logger = logging.getLogger(__name__) # pylint: disable=invalid-name
NAME_PREFIX = 'relation_ids_tab'
class Tab(object):
"""Encapsulate the information on a tab in the database."""
def __init__(self, tab_record):
"""Create tab from tab_record.
In this version tab_record can be assumed to be a partner.relation.tab.
"""
self.tab_record = tab_record
self.name = tab_record.code
def get_fieldname(self):
return '%s_%s' % (NAME_PREFIX, self.tab_record.id)
def get_visible_fieldname(self):
return '%s_visible' % self.get_fieldname()
def create_page(self):
tab_page = etree.Element('page')
self._set_page_attrs(tab_page)
field = etree.Element(
'field',
name=self.get_fieldname(),
context='{'
'"default_this_partner_id": id,'
'"default_tab_id": %d,'
'"active_test": False}' % self.tab_record.id)
tab_page.append(field)
tree = etree.Element('tree', editable='bottom')
field.append(tree)
# Now add fields for the editable tree view in the tab.
type_field = etree.Element(
'field',
name='type_selection_id',
widget='many2one_clickable')
type_field.set('domain', repr([('tab_id', '=', self.tab_record.id)]))
type_field.set('options', repr({'no_create': True}))
tree.append(type_field)
other_partner_field = etree.Element(
'field',
string=_('Partner'),
name='other_partner_id',
widget='many2one_clickable')
other_partner_field.set('options', repr({'no_create': True}))
tree.append(other_partner_field)
tree.append(etree.Element('field', name='date_start'))
tree.append(etree.Element('field', name='date_end'))
return tab_page
def _set_page_attrs(self, tab_page):
tab_page.set('string', self.tab_record.name)
attrs = {'invisible': [(self.get_visible_fieldname(), '=', False)]}
tab_page.set('attrs', repr(attrs))
transfer_modifiers_to_node(attrs, tab_page)
def compute_visibility(self, partner):
"""Compute visibility, dependent on partner and conditions."""
tab = self.tab_record
if tab.partner_ids:
return partner in tab.partner_ids
if tab.contact_type:
is_company_tab = tab.contact_type == 'c'
if partner.is_company != is_company_tab:
return False
if tab.partner_category_id:
if tab.partner_category_id not in partner.category_id:
return False
return True

5
partner_multi_relation_tabs/tests/__init__.py

@ -1,4 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright 2014-2017 Therp BV <https://therp.nl>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from . import common
from . import test_partner_tabs from . import test_partner_tabs
from . import test_tab

47
partner_multi_relation_tabs/tests/common.py

@ -0,0 +1,47 @@
# -*- coding: utf-8 -*-
# Copyright 2014-2018 Therp BV <https://therp.nl>.
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from odoo.tests import common
class TestCommon(common.SingleTransactionCase):
# pylint: disable=too-many-instance-attributes
post_install = True
def setUp(self):
"""Create common objects for tab tests."""
# pylint: disable=invalid-name
super(TestCommon, self).setUp()
self.tab_model = self.env['res.partner.tab']
self.type_model = self.env['res.partner.relation.type']
self.partner_model = self.env['res.partner']
self.relation_model = self.env['res.partner.relation']
# Categories.
self.category_government = self.env.ref(
'partner_multi_relation_tabs.category_government')
self.category_functionary = self.env.ref(
'partner_multi_relation_tabs.category_functionary')
# Tabs.
self.tab_committee = self.env.ref(
'partner_multi_relation_tabs.tab_committee')
self.tab_board = self.env.ref(
'partner_multi_relation_tabs.tab_board')
self.tab_positions = self.env.ref(
'partner_multi_relation_tabs.tab_positions')
# Types.
self.type_chairperson = self.env.ref(
'partner_multi_relation_tabs'
'.relation_type_committee_has_chairperson')
self.type_ceo = self.env.ref(
'partner_multi_relation_tabs'
'.relation_type_company_has_ceo')
# Partners.
self.partner_big_company = self.env.ref(
'partner_multi_relation_tabs.partner_big_company')
self.partner_important_person = self.env.ref(
'partner_multi_relation_tabs.partner_important_person')
self.partner_common_person = self.env.ref(
'partner_multi_relation_tabs.partner_common_person')
# Relations.
self.relation_company_ceo = self.env.ref(
'partner_multi_relation_tabs.relation_company_ceo')

214
partner_multi_relation_tabs/tests/test_partner_tabs.py

@ -1,42 +1,31 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright 2014-2017 Therp BV <https://therp.nl>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
# Copyright 2014-2018 Therp BV <https://therp.nl>.
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from lxml import etree from lxml import etree
from odoo.exceptions import ValidationError from odoo.exceptions import ValidationError
from odoo.tests import common
from . import common
from ..tablib import Tab
class FakeTab():
def __init__(self, id, name):
self.id = id
self.name = name
class TestPartnerTabs(common.SingleTransactionCase):
class TestPartnerTabs(common.TestCommon):
post_install = True post_install = True
def test_create_tab(self): def test_create_tab(self):
tab_model = self.env['res.partner.tab']
partner_model = self.env['res.partner']
new_tab = tab_model.create({
'code': 'executive',
'name': 'Executive members',
'contact_type': 'c'})
self.assertTrue(bool(new_tab))
# There should now be a field in res_partner for the new tab.
fieldname = partner_model._get_tab_fieldname(new_tab)
self.assertTrue(fieldname in partner_model._fields)
# The form view for partner should now also contain the tab,
# if the view contains tabs in the first place.
self.assertTrue(bool(self.tab_board))
tab_obj = Tab(self.tab_board)
# fields_view_get should force the creation of the new tabs.
view_partner_form = self.env.ref('base.view_partner_form') view_partner_form = self.env.ref('base.view_partner_form')
view = partner_model.with_context().fields_view_get(
view = self.partner_model.with_context().fields_view_get(
view_id=view_partner_form.id, view_type='form') view_id=view_partner_form.id, view_type='form')
# The form view for partner should now also contain field 'id'.
tree = etree.fromstring(view['arch']) tree = etree.fromstring(view['arch'])
field = tree.xpath('//field[@name="id"]') field = tree.xpath('//field[@name="id"]')
self.assertTrue(field, 'Id field does not exist.') self.assertTrue(field, 'Id field does not exist.')
# There should now be a field in res_partner for the new tab.
fieldname = tab_obj.get_fieldname()
self.assertTrue(fieldname in self.partner_model._fields)
# And we should have a field for the tab: # And we should have a field for the tab:
field = tree.xpath('//field[@name="%s"]' % fieldname) field = tree.xpath('//field[@name="%s"]' % fieldname)
self.assertTrue( self.assertTrue(
@ -44,7 +33,8 @@ class TestPartnerTabs(common.SingleTransactionCase):
'Tab field %s does not exist in %s.' % 'Tab field %s does not exist in %s.' %
(fieldname, etree.tostring(tree))) (fieldname, etree.tostring(tree)))
# There should be no effect on the tree view: # There should be no effect on the tree view:
view = partner_model.with_context().fields_view_get(view_type='tree')
view = self. partner_model.with_context().fields_view_get(
view_type='tree')
tree = etree.fromstring(view['arch']) tree = etree.fromstring(view['arch'])
field = tree.xpath('//field[@name="%s"]' % fieldname) field = tree.xpath('//field[@name="%s"]' % fieldname)
self.assertFalse( self.assertFalse(
@ -53,165 +43,83 @@ class TestPartnerTabs(common.SingleTransactionCase):
(fieldname, etree.tostring(tree))) (fieldname, etree.tostring(tree)))
def test_tab_modifications(self): def test_tab_modifications(self):
category_model = self.env['res.partner.category']
tab_model = self.env['res.partner.tab']
type_model = self.env['res.partner.relation.type']
category_government = category_model.create({'name': 'Government'})
executive_tab = tab_model.create({
tab_executive = self.tab_model.create({
'code': 'executive', 'code': 'executive',
'name': 'Executive members'}) 'name': 'Executive members'})
self.assertTrue(bool(executive_tab))
type_has_chairperson = type_model.create({
self.assertTrue(bool(tab_executive))
type_chairperson = self.type_model.create({
'name': 'has chairperson', 'name': 'has chairperson',
'name_inverse': 'is chairperson for', 'name_inverse': 'is chairperson for',
'contact_type_left': 'p', # This emulates a user mistake.
'contact_type_right': 'p', 'contact_type_right': 'p',
'tab_left_id': executive_tab.id})
self.assertTrue(bool(type_has_chairperson))
'tab_left_id': tab_executive.id})
self.assertTrue(bool(type_chairperson))
# If we change tab now to be only valid on company partners # If we change tab now to be only valid on company partners
# the tab_left_id field should be cleared from the type: # the tab_left_id field should be cleared from the type:
executive_tab.write({
'contact_type': 'c',
'partner_category_id': category_government.id})
self.assertFalse(type_has_chairperson.tab_left_id.id)
tab_executive.write({'contact_type': 'c'})
self.assertFalse(type_chairperson.tab_left_id.id)
# Trying to set the tab back on type should be impossible: # Trying to set the tab back on type should be impossible:
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
type_has_chairperson.write({'tab_left_id': executive_tab.id})
# We should be able to change tab, if also changing contact type
# and category:
type_has_chairperson.write({
'partner_category_left': category_government.id,
type_chairperson.write({'tab_left_id': tab_executive.id})
# We should be able to change tab, if also changing contact type.
type_chairperson.write({
'contact_type_left': 'c', 'contact_type_left': 'c',
'tab_left_id': executive_tab.id})
'tab_left_id': tab_executive.id})
self.assertEqual( self.assertEqual(
type_has_chairperson.tab_left_id.id,
executive_tab.id)
# Unlinking the tab should reset the tab name on relations:
executive_tab.unlink()
type_chairperson.tab_left_id.id,
tab_executive.id)
# Unlinking the tab should reset the tab_left_id on relation type.
tab_executive.unlink()
self.assertEqual( self.assertEqual(
type_has_chairperson.tab_left_id.id,
type_chairperson.tab_left_id.id,
False) False)
def test_relation_type_modifications(self):
category_model = self.env['res.partner.category']
tab_model = self.env['res.partner.tab']
type_model = self.env['res.partner.relation.type']
category_government = category_model.create({'name': 'Government'})
category_positions = category_model.create({'name': 'Positions'})
executive_tab = tab_model.create({
'code': 'executive',
'name': 'Executive members',
'contact_type': 'c',
'partner_category_id': category_government.id})
self.assertTrue(bool(executive_tab))
positions_tab = tab_model.create({
'code': 'positions',
'name': 'Positions held',
'contact_type': 'p',
'partner_category_id': category_positions.id})
self.assertTrue(bool(executive_tab))
type_has_chairperson = type_model.create({
'name': 'has chairperson',
'name_inverse': 'is chairperson for',
'partner_category_left': category_government.id,
'contact_type_left': 'c',
'tab_left_id': executive_tab.id,
'partner_category_right': category_positions.id,
'contact_type_right': 'p',
'tab_right_id': positions_tab.id})
self.assertTrue(bool(type_has_chairperson))
def test_type_modifications(self):
self.assertTrue(bool(self.tab_board))
self.assertTrue(bool(self.tab_positions))
self.assertTrue(bool(self.type_chairperson))
# Trying to clear either category should raise ValidationError: # Trying to clear either category should raise ValidationError:
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
type_has_chairperson.write({'partner_category_left': False})
self.type_chairperson.write({'partner_category_left': False})
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
type_has_chairperson.write({'partner_category_right': False})
self.type_chairperson.write({'partner_category_right': False})
# Trying to clear either contact type should raise ValidationError: # Trying to clear either contact type should raise ValidationError:
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
type_has_chairperson.write({'contact_type_left': False})
self.type_chairperson.write({'contact_type_left': False})
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
type_has_chairperson.write({'contact_type_right': False})
self.type_chairperson.write({'contact_type_right': False})
def test_relations(self): def test_relations(self):
"""Test relations shown on tab.""" """Test relations shown on tab."""
tab_model = self.env['res.partner.tab']
type_model = self.env['res.partner.relation.type']
partner_model = self.env['res.partner']
relation_model = self.env['res.partner.relation']
relation_all_model = self.env['res.partner.relation.all'] relation_all_model = self.env['res.partner.relation.all']
executive_tab = tab_model.create({
'code': 'executive',
'name': 'Executive members'})
self.assertTrue(bool(executive_tab))
type_has_chairperson = type_model.create({
'name': 'has chairperson',
'name_inverse': 'is chairperson for',
'contact_type_right': 'p',
'tab_left_id': executive_tab.id})
self.assertTrue(bool(type_has_chairperson))
big_company = partner_model.create({
'name': 'Big company',
'is_company': True,
'ref': 'BIG'})
self.assertTrue(bool(big_company))
important_person = partner_model.create({
'name': 'Bart Simpson',
'is_company': False,
'ref': 'BS'})
self.assertTrue(bool(important_person))
relation_company_chair = relation_model.create({
'left_partner_id': big_company.id,
'type_id': type_has_chairperson.id,
'right_partner_id': important_person.id})
self.assertTrue(bool(relation_company_chair))
self.assertTrue(bool(self.tab_board))
self.assertTrue(bool(self.type_ceo))
self.assertTrue(bool(self.partner_big_company))
self.assertTrue(bool(self.partner_important_person))
self.assertTrue(bool(self.relation_company_ceo))
# Now we should be able to find the relation with the tab_id: # Now we should be able to find the relation with the tab_id:
relation_all_company_chair = relation_all_model.search([
('tab_id', '=', executive_tab.id)], limit=1)
self.assertTrue(bool(relation_all_company_chair))
self.assertEqual(
relation_company_chair.left_partner_id.id,
relation_all_company_chair.this_partner_id.id)
board_partners = relation_all_model.search([
('tab_id', '=', self.tab_board.id)])
self.assertTrue(bool(board_partners))
self.assertIn(
self.partner_big_company,
[relation.this_partner_id for relation in board_partners])
# We should find the company on the partner through tab field: # We should find the company on the partner through tab field:
fieldname = partner_model._get_tab_fieldname(executive_tab)
self.assertTrue(fieldname in partner_model._fields)
executive_partners = big_company[fieldname]
self.assertEqual(len(executive_partners), 1)
tab_obj = Tab(self.tab_board)
fieldname = tab_obj.get_fieldname()
self.assertTrue(fieldname in self.partner_model._fields)
board_partners = self.partner_big_company[fieldname]
self.assertEqual(len(board_partners), 1)
self.assertEqual( self.assertEqual(
executive_partners.other_partner_id.id,
important_person.id)
board_partners.other_partner_id.id,
self.partner_important_person.id)
# When adding a new relation on a tab, type must be for tab. # When adding a new relation on a tab, type must be for tab.
onchange_result = executive_partners.with_context(
default_tab_id=executive_tab.id
onchange_result = board_partners.with_context(
default_tab_id=self.tab_board.id
).onchange_partner_id() ).onchange_partner_id()
self.assertTrue(onchange_result) self.assertTrue(onchange_result)
self.assertIn('domain', onchange_result) self.assertIn('domain', onchange_result)
self.assertIn('type_selection_id', onchange_result['domain']) self.assertIn('type_selection_id', onchange_result['domain'])
self.assertEqual( self.assertEqual(
onchange_result['domain']['type_selection_id'][-1], onchange_result['domain']['type_selection_id'][-1],
('tab_id', '=', executive_tab.id))
def test_update_tabs(self):
"""Test the function that will create tabs during module loading."""
tab_model = self.env['res.partner.tab']
partner_model = self.env['res.partner']
executive_tab = tab_model.create({
'code': 'executive',
'name': 'Executive members'})
self.assertTrue(bool(executive_tab))
tabfield_executive_name = partner_model._get_tab_fieldname(
executive_tab)
# Create some fake tab fields (should be removed).
tab_123 = FakeTab(123, 'First tab')
tab_456 = FakeTab(456, 'Second tab')
# Add "tab fields"
partner_model._add_tab_field(tab_123)
tabfield_123_name = partner_model._get_tab_fieldname(tab_123)
self.assertEqual(
partner_model._fields[tabfield_123_name].string, tab_123.name)
partner_model._add_tab_field(tab_456)
tabfield_456_name = partner_model._get_tab_fieldname(tab_456)
self.assertEqual(
partner_model._fields[tabfield_456_name].string, tab_456.name)
# Now call hook method
partner_model._register_hook()
self.assertFalse(tabfield_123_name in partner_model._fields)
self.assertFalse(tabfield_456_name in partner_model._fields)
self.assertTrue(tabfield_executive_name in partner_model._fields)
('tab_id', '=', self.tab_board.id))

31
partner_multi_relation_tabs/tests/test_tab.py

@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
# Copyright 2018 Therp BV <https://therp.nl>.
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
# pylint: disable=protected-access
from . import common
from ..tablib import Tab
class TestTab(common.TestCommon):
def test_create_page(self):
self.assertTrue(bool(self.tab_board))
tab_obj = Tab(self.tab_board)
page = tab_obj.create_page()
# And we should have a field for (amongst others) selection_type_id.
field = page.xpath('//field[@name="type_selection_id"]')
self.assertTrue(field, 'Field selection_type_id not in page.')
def test_visibility(self):
"""Tab positions should be shown for functionaries, but not others."""
self.assertTrue(bool(self.tab_positions))
self.assertTrue(bool(self.partner_important_person))
self.assertTrue(bool(self.partner_common_person))
tab_obj = Tab(self.tab_positions)
self.assertTrue(
tab_obj.compute_visibility(self.partner_important_person),
'Board tab should be visible for functionary.')
self.assertFalse(
tab_obj.compute_visibility(self.partner_common_person),
'Board tab should not be visible for non-functionary.')
Loading…
Cancel
Save