diff --git a/partner_multi_relation_tabs/README.rst b/partner_multi_relation_tabs/README.rst new file mode 100644 index 000000000..b74ef9700 --- /dev/null +++ b/partner_multi_relation_tabs/README.rst @@ -0,0 +1,63 @@ +.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg + :target: http://www.gnu.org/licenses/agpl + :alt: License: AGPL-3 + +======================== +Partner Relations in tab +======================== + +This module adds the possibility to show certain partner relations in its own +tab instead of the list of all relations. This can be useful if certain +relation types are regularly used and should be overseeable at a glace. + + +Usage +===== + +To use this module, you need to: + +#. Go to ... + +.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas + :alt: Try me on Runbot + :target: https://runbot.odoo-community.org/runbot/partner-contact/10.0 + + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues +`_. In case of trouble, please +check there if your issue has already been reported. If you spotted it first, +help us smash it by providing detailed and welcomed feedback. + +Credits +======= + + +Contributors +------------ + +* Holger Brunn +* Alexandre Fayolle +* Stéphane Bidoul +* Ronald Portier +* George Daramouskas + +Do not contact contributors directly about support or help with technical issues. + + +Maintainer +---------- + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +This module is maintained by the OCA. + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +To contribute to this module, please visit https://odoo-community.org. diff --git a/partner_multi_relation_tabs/__init__.py b/partner_multi_relation_tabs/__init__.py new file mode 100644 index 000000000..39faa769c --- /dev/null +++ b/partner_multi_relation_tabs/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Copyright 2014-2017 Therp BV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from . import models diff --git a/partner_multi_relation_tabs/__manifest__.py b/partner_multi_relation_tabs/__manifest__.py new file mode 100644 index 000000000..101dca590 --- /dev/null +++ b/partner_multi_relation_tabs/__manifest__.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Copyright 2014-2018 Therp BV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +{ + "name": "Show partner relations in own tab", + "version": "10.0.1.0.0", + "author": "Therp BV,Odoo Community Association (OCA)", + "license": "AGPL-3", + "complexity": "normal", + "category": "Customer Relationship Management", + "depends": [ + 'web_tree_many2one_clickable', + 'partner_multi_relation', + ], + "data": [ + "views/res_partner_tab.xml", + "views/res_partner_relation_type.xml", + "views/res_partner_relation_all.xml", + 'views/menu.xml', + 'security/ir.model.access.csv', + ], + "auto_install": False, + "installable": True, + "application": False, +} diff --git a/partner_multi_relation_tabs/i18n/nl.po b/partner_multi_relation_tabs/i18n/nl.po new file mode 100644 index 000000000..2eaebbe17 --- /dev/null +++ b/partner_multi_relation_tabs/i18n/nl.po @@ -0,0 +1,182 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * partner_multi_relation_tabs +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-11-01 09:32+0000\n" +"PO-Revision-Date: 2017-11-01 09:32+0000\n" +"Last-Translator: Ronald Portier , 2017\n" +"Language-Team: Dutch (https://www.transifex.com/oca/teams/23907/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" + +#. module: partner_multi_relation_tabs +#: code:addons/partner_multi_relation_tabs/models/res_partner.py:26 +#, python-format +msgid "Adding field %s to res.partner nodel." +msgstr "Veld %s wordt toegevoegd aan res.partner model." + +#. module: partner_multi_relation_tabs +#: model:ir.model,name:partner_multi_relation_tabs.model_res_partner_relation_all +msgid "All (non-inverse + inverse) relations between partners" +msgstr "Alle connecties (van beide kanten) tussen relaties" + +#. module: partner_multi_relation_tabs +#: model:ir.model,name:partner_multi_relation_tabs.model_res_partner_relation_type_selection +msgid "All relation types" +msgstr "Alle connectie types" + +#. module: partner_multi_relation_tabs +#: model:ir.model.fields,field_description:partner_multi_relation_tabs.field_res_partner_tab_code +msgid "Code" +msgstr "Code" + +#. module: partner_multi_relation_tabs +#: code:addons/partner_multi_relation_tabs/models/res_partner_relation_type.py:32 +#, python-format +msgid "Contact type left not compatible with left tab" +msgstr "Linker type contact is niet verenigbaar met linker tab" + +#. module: partner_multi_relation_tabs +#: code:addons/partner_multi_relation_tabs/models/res_partner_relation_type.py:52 +#, python-format +msgid "Contact type right not compatible with right tab" +msgstr "Rechter type contact is niet verenigbaar met rechter tab" + +#. module: partner_multi_relation_tabs +#: model:ir.model.fields,field_description:partner_multi_relation_tabs.field_res_partner_tab_create_uid +msgid "Created by" +msgstr "Aangemaakt door" + +#. module: partner_multi_relation_tabs +#: model:ir.model.fields,field_description:partner_multi_relation_tabs.field_res_partner_tab_create_date +msgid "Created on" +msgstr "Aangemaakt op" + +#. module: partner_multi_relation_tabs +#: model:ir.model.fields,field_description:partner_multi_relation_tabs.field_res_partner_tab_display_name +msgid "Display Name" +msgstr "Naam weergave" + +#. module: partner_multi_relation_tabs +#: model:ir.model.fields,field_description:partner_multi_relation_tabs.field_res_partner_tab_id +msgid "ID" +msgstr "ID" + +#. module: partner_multi_relation_tabs +#: model:ir.model.fields,help:partner_multi_relation_tabs.field_res_partner_tab_code +msgid "Language independent code for tab" +msgstr "Taal onafhankelijke code voor tab" + +#. module: partner_multi_relation_tabs +#: model:ir.model.fields,field_description:partner_multi_relation_tabs.field_res_partner_tab___last_update +msgid "Last Modified on" +msgstr "Laatst gewijzigd op" + +#. module: partner_multi_relation_tabs +#: model:ir.model.fields,field_description:partner_multi_relation_tabs.field_res_partner_tab_write_uid +msgid "Last Updated by" +msgstr "Laatst gewijzigd door" + +#. module: partner_multi_relation_tabs +#: model:ir.model.fields,field_description:partner_multi_relation_tabs.field_res_partner_tab_write_date +msgid "Last Updated on" +msgstr "Laatst gewijzigd op" + +#. module: partner_multi_relation_tabs +#: model:ir.model.fields,field_description:partner_multi_relation_tabs.field_res_partner_tab_name +msgid "Name" +msgstr "Naam" + +#. module: partner_multi_relation_tabs +#: model:ir.model,name:partner_multi_relation_tabs.model_res_partner +msgid "Partner" +msgstr "Relatie" + +#. module: partner_multi_relation_tabs +#: model:ir.actions.act_window,name:partner_multi_relation_tabs.action_res_partner_tab +#: model:ir.ui.menu,name:partner_multi_relation_tabs.menu_res_partner_tab +msgid "Relation Tabs" +msgstr "Connectie tabs" + +#. module: partner_multi_relation_tabs +#: model:ir.model,name:partner_multi_relation_tabs.model_res_partner_relation_type +msgid "Partner Relation Type" +msgstr "Type connectie" + +#. module: partner_multi_relation_tabs +#: code:addons/partner_multi_relation_tabs/models/res_partner_relation_type.py:37 +#, python-format +msgid "Partner category left not compatible with left tab" +msgstr "Categorie van linker partner is niet verenigbaar met linker tab" + +#. module: partner_multi_relation_tabs +#: code:addons/partner_multi_relation_tabs/models/res_partner_relation_type.py:57 +#, python-format +msgid "Partner category right not compatible with right tab" +msgstr "Categorie van rechter partner is niet verenigbaar met rechter tab" + +#. module: partner_multi_relation_tabs +#: model:ir.model.fields,field_description:partner_multi_relation_tabs.field_res_partner_relation_all_tab_id +#: model:ir.model.fields,field_description:partner_multi_relation_tabs.field_res_partner_relation_type_selection_tab_id +msgid "Show relation on tab" +msgstr "Toon connectie op tab" + +#. module: partner_multi_relation_tabs +#: model:ir.model.fields,field_description:partner_multi_relation_tabs.field_res_partner_relation_type_tab_right_id +msgid "Tab for inverse relation" +msgstr "Tab voor de omgekeerde relatie" + +#. module: partner_multi_relation_tabs +#: model:ir.model.fields,field_description:partner_multi_relation_tabs.field_res_partner_relation_type_tab_left_id +msgid "Tab for this relation" +msgstr "Tab voor deze relatie" + +#. module: partner_multi_relation_tabs +#: model:ir.model.fields,help:partner_multi_relation_tabs.field_res_partner_relation_type_tab_right_id +msgid "Tab in which inverse relations will be visible on partner." +msgstr "Tab waar de omgekeerde connecties zichtbaar zullen zijn op de relatie." + +#. module: partner_multi_relation_tabs +#: model:ir.model.fields,help:partner_multi_relation_tabs.field_res_partner_relation_type_tab_left_id +msgid "Tab in which these relations will be visible on partner." +msgstr "Tab waarin deze connecties zichtbaar zullen zijn op de relaties." + +#. module: partner_multi_relation_tabs +#: model:ir.model,name:partner_multi_relation_tabs.model_res_partner_tab +msgid "Tabs to add to partner" +msgstr "Tab die aan de relatie wordt toegevoegd" + +#. module: partner_multi_relation_tabs +#: code:addons/partner_multi_relation_tabs/models/res_partner.py:39 +#, python-format +msgid "Updating field %s in res.partner nodel." +msgstr "Veld %s op res.partner model wordt bijgewerkt." + +#. module: partner_multi_relation_tabs +#: model:ir.model.fields,field_description:partner_multi_relation_tabs.field_res_partner_tab_partner_category_id +msgid "Valid for partner category" +msgstr "Geldig voor relatiecategorie" + +#. module: partner_multi_relation_tabs +#: model:ir.model.fields,field_description:partner_multi_relation_tabs.field_res_partner_tab_contact_type +msgid "Valid for partner type" +msgstr "Geldig voor type relatie" + +#. module: partner_multi_relation_tabs +#: model:ir.model.fields,help:partner_multi_relation_tabs.field_res_partner_tab_name +msgid "Will provide title for tab in user language" +msgstr "Wordt gebruikt voor titel van tabblad in de taal van de gebruiker" + +#. module: partner_multi_relation_tabs +#: code:addons/partner_multi_relation_tabs/models/res_partner.py:45 +#, python-format +msgid "deleting field %s from res.partner nodel." +msgstr "verwijderen veld % van res.partner model." + diff --git a/partner_multi_relation_tabs/i18n/partner_multi_relation_tabs.pot b/partner_multi_relation_tabs/i18n/partner_multi_relation_tabs.pot new file mode 100644 index 000000000..00d44a960 --- /dev/null +++ b/partner_multi_relation_tabs/i18n/partner_multi_relation_tabs.pot @@ -0,0 +1,181 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * partner_multi_relation_tabs +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-11-01 09:32+0000\n" +"PO-Revision-Date: 2017-11-01 09:32+0000\n" +"Last-Translator: <>\n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: partner_multi_relation_tabs +#: code:addons/partner_multi_relation_tabs/models/res_partner.py:26 +#, python-format +msgid "Adding field %s to res.partner nodel." +msgstr "" + +#. module: partner_multi_relation_tabs +#: model:ir.model,name:partner_multi_relation_tabs.model_res_partner_relation_all +msgid "All (non-inverse + inverse) relations between partners" +msgstr "" + +#. module: partner_multi_relation_tabs +#: model:ir.model,name:partner_multi_relation_tabs.model_res_partner_relation_type_selection +msgid "All relation types" +msgstr "" + +#. module: partner_multi_relation_tabs +#: model:ir.model.fields,field_description:partner_multi_relation_tabs.field_res_partner_tab_code +msgid "Code" +msgstr "" + +#. module: partner_multi_relation_tabs +#: code:addons/partner_multi_relation_tabs/models/res_partner_relation_type.py:32 +#, python-format +msgid "Contact type left not compatible with left tab" +msgstr "" + +#. module: partner_multi_relation_tabs +#: code:addons/partner_multi_relation_tabs/models/res_partner_relation_type.py:52 +#, python-format +msgid "Contact type right not compatible with right tab" +msgstr "" + +#. module: partner_multi_relation_tabs +#: model:ir.model.fields,field_description:partner_multi_relation_tabs.field_res_partner_tab_create_uid +msgid "Created by" +msgstr "" + +#. module: partner_multi_relation_tabs +#: model:ir.model.fields,field_description:partner_multi_relation_tabs.field_res_partner_tab_create_date +msgid "Created on" +msgstr "" + +#. module: partner_multi_relation_tabs +#: model:ir.model.fields,field_description:partner_multi_relation_tabs.field_res_partner_tab_display_name +msgid "Display Name" +msgstr "" + +#. module: partner_multi_relation_tabs +#: model:ir.model.fields,field_description:partner_multi_relation_tabs.field_res_partner_tab_id +msgid "ID" +msgstr "" + +#. module: partner_multi_relation_tabs +#: model:ir.model.fields,help:partner_multi_relation_tabs.field_res_partner_tab_code +msgid "Language independent code for tab" +msgstr "" + +#. module: partner_multi_relation_tabs +#: model:ir.model.fields,field_description:partner_multi_relation_tabs.field_res_partner_tab___last_update +msgid "Last Modified on" +msgstr "" + +#. module: partner_multi_relation_tabs +#: model:ir.model.fields,field_description:partner_multi_relation_tabs.field_res_partner_tab_write_uid +msgid "Last Updated by" +msgstr "" + +#. module: partner_multi_relation_tabs +#: model:ir.model.fields,field_description:partner_multi_relation_tabs.field_res_partner_tab_write_date +msgid "Last Updated on" +msgstr "" + +#. module: partner_multi_relation_tabs +#: model:ir.model.fields,field_description:partner_multi_relation_tabs.field_res_partner_tab_name +msgid "Name" +msgstr "" + +#. module: partner_multi_relation_tabs +#: model:ir.model,name:partner_multi_relation_tabs.model_res_partner +msgid "Partner" +msgstr "" + +#. module: partner_multi_relation_tabs +#: model:ir.actions.act_window,name:partner_multi_relation_tabs.action_res_partner_tab +#: model:ir.ui.menu,name:partner_multi_relation_tabs.menu_res_partner_tab +msgid "Relation Tabs" +msgstr "" + +#. module: partner_multi_relation_tabs +#: model:ir.model,name:partner_multi_relation_tabs.model_res_partner_relation_type +msgid "Partner Relation Type" +msgstr "" + +#. module: partner_multi_relation_tabs +#: code:addons/partner_multi_relation_tabs/models/res_partner_relation_type.py:37 +#, python-format +msgid "Partner category left not compatible with left tab" +msgstr "" + +#. module: partner_multi_relation_tabs +#: code:addons/partner_multi_relation_tabs/models/res_partner_relation_type.py:57 +#, python-format +msgid "Partner category right not compatible with right tab" +msgstr "" + +#. module: partner_multi_relation_tabs +#: model:ir.model.fields,field_description:partner_multi_relation_tabs.field_res_partner_relation_all_tab_id +#: model:ir.model.fields,field_description:partner_multi_relation_tabs.field_res_partner_relation_type_selection_tab_id +msgid "Show relation on tab" +msgstr "" + +#. module: partner_multi_relation_tabs +#: model:ir.model.fields,field_description:partner_multi_relation_tabs.field_res_partner_relation_type_tab_right_id +msgid "Tab for inverse relation" +msgstr "" + +#. module: partner_multi_relation_tabs +#: model:ir.model.fields,field_description:partner_multi_relation_tabs.field_res_partner_relation_type_tab_left_id +msgid "Tab for this relation" +msgstr "" + +#. module: partner_multi_relation_tabs +#: model:ir.model.fields,help:partner_multi_relation_tabs.field_res_partner_relation_type_tab_right_id +msgid "Tab in which inverse relations will be visible on partner." +msgstr "" + +#. module: partner_multi_relation_tabs +#: model:ir.model.fields,help:partner_multi_relation_tabs.field_res_partner_relation_type_tab_left_id +msgid "Tab in which these relations will be visible on partner." +msgstr "" + +#. module: partner_multi_relation_tabs +#: model:ir.model,name:partner_multi_relation_tabs.model_res_partner_tab +msgid "Tabs to add to partner" +msgstr "" + +#. module: partner_multi_relation_tabs +#: code:addons/partner_multi_relation_tabs/models/res_partner.py:39 +#, python-format +msgid "Updating field %s in res.partner nodel." +msgstr "" + +#. module: partner_multi_relation_tabs +#: model:ir.model.fields,field_description:partner_multi_relation_tabs.field_res_partner_tab_partner_category_id +msgid "Valid for partner category" +msgstr "" + +#. module: partner_multi_relation_tabs +#: model:ir.model.fields,field_description:partner_multi_relation_tabs.field_res_partner_tab_contact_type +msgid "Valid for partner type" +msgstr "" + +#. module: partner_multi_relation_tabs +#: model:ir.model.fields,help:partner_multi_relation_tabs.field_res_partner_tab_name +msgid "Will provide title for tab in user language" +msgstr "" + +#. module: partner_multi_relation_tabs +#: code:addons/partner_multi_relation_tabs/models/res_partner.py:45 +#, python-format +msgid "deleting field %s from res.partner nodel." +msgstr "" + diff --git a/partner_multi_relation_tabs/models/__init__.py b/partner_multi_relation_tabs/models/__init__.py new file mode 100644 index 000000000..7c93bed9a --- /dev/null +++ b/partner_multi_relation_tabs/models/__init__.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +# Copyright 2014-2017 Therp BV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from . import res_partner_tab +from . import res_partner_relation_type +from . import res_partner_relation_type_selection +from . import res_partner_relation_all +from . import res_partner diff --git a/partner_multi_relation_tabs/models/res_partner.py b/partner_multi_relation_tabs/models/res_partner.py new file mode 100644 index 000000000..f174b8ea8 --- /dev/null +++ b/partner_multi_relation_tabs/models/res_partner.py @@ -0,0 +1,158 @@ +# -*- coding: utf-8 -*- +# 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 odoo.osv.orm import transfer_modifiers_to_node +from odoo.osv import expression +from odoo import _, api, fields, models + + +_logger = logging.getLogger(__name__) +NAME_PREFIX = 'relation_ids_tab' + + +class ResPartner(models.Model): + _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) + 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 + + @api.model + def fields_view_get( + self, view_id=None, view_type='form', toolbar=False, + submenu=False): + """Override to add relation tabs to form.""" + result = super(ResPartner, self).fields_view_get( + view_id=view_id, view_type=view_type, toolbar=toolbar, + submenu=submenu) + if view_type != 'form' or self.env.context.get('check_view_ids'): + return result + view = etree.fromstring(result['arch']) # pylint: disable=no-member + extra_fields = self._add_tab_pages(view) + view_model = self.env['ir.ui.view'] + result['arch'], original_fields = view_model.postprocess_and_fields( + self._name, view, result['view_id']) + for fieldname in extra_fields: + result['fields'][fieldname] = original_fields[fieldname] + return result diff --git a/partner_multi_relation_tabs/models/res_partner_relation_all.py b/partner_multi_relation_tabs/models/res_partner_relation_all.py new file mode 100644 index 000000000..5430b633c --- /dev/null +++ b/partner_multi_relation_tabs/models/res_partner_relation_all.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +# Copyright 2014-2017 Therp BV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from openerp import api, fields, models + + +class ResPartnerRelationAll(models.AbstractModel): + """Abstract model to show each relation from two sides.""" + _inherit = 'res.partner.relation.all' + + tab_id = fields.Many2one( + comodel_name='res.partner.tab', + string='Show relation on tab', + readonly=True, + ) + + def _get_additional_view_fields(self): + """Add tab_id to view fields.""" + return ','.join([ + super(ResPartnerRelationAll, self)._get_additional_view_fields(), + "CASE" + " WHEN NOT bas.is_inverse" + " THEN lefttab.id" + " ELSE righttab.id" + " END as tab_id"]) + + def _get_additional_tables(self): + """Add res_partner_tab table to view.""" + return ' '.join([ + super(ResPartnerRelationAll, self)._get_additional_tables(), + "LEFT OUTER JOIN res_partner_tab lefttab" + " ON typ.tab_left_id = lefttab.id", + "LEFT OUTER JOIN res_partner_tab righttab" + " ON typ.tab_right_id = righttab.id"]) + + @api.onchange( + 'this_partner_id', + 'other_partner_id', + ) + def onchange_partner_id(self): + """Add tab if needed to type_selection_id domain. + + This method makes sure then when a relation is added to a tab, + it is with a relation type meant to be placed on that tab. + """ + result = super(ResPartnerRelationAll, self).onchange_partner_id() + if 'default_tab_id' in self.env.context: + if 'domain' not in result: + result['domain'] = {} + if 'type_selection_id' not in result['domain']: + result['domain']['type_selection_id'] = [] + selection_domain = result['domain']['type_selection_id'] + selection_domain.append( + ('tab_id', '=', self.env.context['default_tab_id'])) + return result diff --git a/partner_multi_relation_tabs/models/res_partner_relation_type.py b/partner_multi_relation_tabs/models/res_partner_relation_type.py new file mode 100644 index 000000000..59a95cc47 --- /dev/null +++ b/partner_multi_relation_tabs/models/res_partner_relation_type.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +# Copyright 2014-2017 Therp BV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + + +class ResPartnerRelationType(models.Model): + _inherit = 'res.partner.relation.type' + + tab_left_id = fields.Many2one( + comodel_name='res.partner.tab', + string='Tab for this relation', + help="Tab in which these relations will be visible on partner.") + tab_right_id = fields.Many2one( + comodel_name='res.partner.tab', + string='Tab for inverse relation', + help="Tab in which inverse relations will be visible on partner.") + + @api.multi + @api.constrains( + 'contact_type_left', + 'partner_category_left', + 'tab_left_id') + def _check_tab_left(self): + """Conditions for left partner should be consistent with tab.""" + for rec in self: + if not rec.tab_left_id: + continue + tab_contact_type = rec.tab_left_id.contact_type + if tab_contact_type and tab_contact_type != rec.contact_type_left: + raise ValidationError(_( + "Contact type left not compatible with left tab")) + tab_partner_category_id = rec.tab_left_id.partner_category_id + if tab_partner_category_id and \ + tab_partner_category_id != rec.partner_category_left: + raise ValidationError(_( + "Partner category left not compatible with left tab")) + + @api.multi + @api.constrains( + 'contact_type_right', + 'partner_category_right', + 'tab_right_id') + def _check_tab_right(self): + """Conditions for right partner should be consistent with tab.""" + for rec in self: + if not rec.tab_right_id: + continue + tab_contact_type = rec.tab_right_id.contact_type + if tab_contact_type and tab_contact_type != rec.contact_type_right: + raise ValidationError(_( + "Contact type right not compatible with right tab")) + tab_partner_category_id = rec.tab_right_id.partner_category_id + if tab_partner_category_id and \ + tab_partner_category_id != rec.partner_category_right: + raise ValidationError(_( + "Partner category right not compatible with right tab")) diff --git a/partner_multi_relation_tabs/models/res_partner_relation_type_selection.py b/partner_multi_relation_tabs/models/res_partner_relation_type_selection.py new file mode 100644 index 000000000..c05b4f928 --- /dev/null +++ b/partner_multi_relation_tabs/models/res_partner_relation_type_selection.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +# Copyright 2013-2017 Therp BV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from openerp import fields, models + + +class ResPartnerRelationTypeSelection(models.Model): + """Virtual relation types""" + _inherit = 'res.partner.relation.type.selection' + + tab_id = fields.Many2one( + comodel_name='res.partner.tab', + string='Show relation on tab', + readonly=True, + ) + + def _get_additional_view_fields(self): + """Add tab_id to fields in view.""" + return ','.join([ + super(ResPartnerRelationTypeSelection, self) + ._get_additional_view_fields(), + "CASE" + " WHEN NOT bas.is_inverse" + " THEN lefttab.id" + " ELSE righttab.id" + " END as tab_id"]) + + def _get_additional_tables(self): + """Add two links to res_partner_tab.""" + return ' '.join([ + super(ResPartnerRelationTypeSelection, self) + ._get_additional_tables(), + "LEFT OUTER JOIN res_partner_tab lefttab" + " ON typ.tab_left_id = lefttab.id", + "LEFT OUTER JOIN res_partner_tab righttab" + " ON typ.tab_right_id = righttab.id"]) diff --git a/partner_multi_relation_tabs/models/res_partner_tab.py b/partner_multi_relation_tabs/models/res_partner_tab.py new file mode 100644 index 000000000..dae1d9ba2 --- /dev/null +++ b/partner_multi_relation_tabs/models/res_partner_tab.py @@ -0,0 +1,102 @@ +# -*- coding: utf-8 -*- +# Copyright 2017-2018 Therp BV . +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + + +class ResPartnerTab(models.Model): + """Model that defines relation types that might exist between partners""" + _name = 'res.partner.tab' + _description = 'Tabs to add to partner' + _order = 'name' + + @api.model + def get_partner_types(self): + """Partner types are defined by model res.partner.relation.type.""" + # pylint: disable=no-self-use + rprt_model = self.env['res.partner.relation.type'] + return rprt_model.get_partner_types() + + code = fields.Char( + string='Code', + required=True, + help="Language independent code for tab") + name = fields.Char( + string='Name', + required=True, + translate=True, + help="Will provide title for tab in user language") + contact_type = fields.Selection( + selection='get_partner_types', + string='Valid for partner type') + partner_category_id = fields.Many2one( + comodel_name='res.partner.category', + string='Valid for partner category') + partner_ids = fields.Many2many( + comodel_name='res.partner', + string="Partners with this tab", + help="This tab will only show for certain partners.\n" + "Do not combine this with selection for contact type or" + " category.") + + @api.constrains('contact_type', 'partner_category_id', 'partner_ids') + def _check_partner_ids(self): + """If partner_ids filled, other domain fields should be empty.""" + if self.partner_ids and \ + (self.contact_type or self.partner_category_id): + raise ValidationError(_( + "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 + def update_types(self, vals=None): + """Update types on write or unlink. + + If we have no vals, assume unlink. + """ + if vals: + contact_type = vals.get('contact_type', False) + partner_category_id = vals.get('partner_category_id', False) + type_model = self.env['res.partner.relation.type'] + for this in self: + for tab_side in ('left', 'right'): + side_tab = 'tab_%s_id' % tab_side + tab_using = type_model.search([(side_tab, '=', this.id)]) + for relation_type in tab_using: + type_value = relation_type['contact_type_%s' % tab_side] + category_value = \ + relation_type['partner_category_%s' % tab_side] + if (not vals or + (contact_type and contact_type != type_value) or + (partner_category_id and + partner_category_id != category_value.id)): + relation_type.write({side_tab: False}) + + @api.multi + def write(self, vals): + """Remove tab from types no longer satifying criteria.""" + if vals.get('contact_type', False) or \ + vals.get('partner_category_id', False): + self.update_types(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 + + @api.multi + def unlink(self): + """Unlink should first remove references.""" + 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() diff --git a/partner_multi_relation_tabs/security/ir.model.access.csv b/partner_multi_relation_tabs/security/ir.model.access.csv new file mode 100644 index 000000000..7875667aa --- /dev/null +++ b/partner_multi_relation_tabs/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +read_res_partner_tab,read_res_partner_tab,model_res_partner_tab,,1,0,0,0 +crud_res_partner_tab,crud_res_partner_tab,model_res_partner_tab,base.group_partner_manager,1,1,1,1 diff --git a/partner_multi_relation_tabs/static/description/icon.png b/partner_multi_relation_tabs/static/description/icon.png new file mode 100644 index 000000000..b219b220e Binary files /dev/null and b/partner_multi_relation_tabs/static/description/icon.png differ diff --git a/partner_multi_relation_tabs/tests/__init__.py b/partner_multi_relation_tabs/tests/__init__.py new file mode 100644 index 000000000..c2550f63a --- /dev/null +++ b/partner_multi_relation_tabs/tests/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Copyright 2014-2017 Therp BV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from . import test_partner_tabs diff --git a/partner_multi_relation_tabs/tests/test_partner_tabs.py b/partner_multi_relation_tabs/tests/test_partner_tabs.py new file mode 100644 index 000000000..269596e7f --- /dev/null +++ b/partner_multi_relation_tabs/tests/test_partner_tabs.py @@ -0,0 +1,217 @@ +# -*- coding: utf-8 -*- +# Copyright 2014-2017 Therp BV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from lxml import etree + +from odoo.exceptions import ValidationError +from odoo.tests import common + + +class FakeTab(): + + def __init__(self, id, name): + self.id = id + self.name = name + + +class TestPartnerTabs(common.SingleTransactionCase): + + post_install = True + + 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. + view_partner_form = self.env.ref('base.view_partner_form') + view = partner_model.with_context().fields_view_get( + view_id=view_partner_form.id, 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.with_context().fields_view_get(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_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({ + '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)) + # If we change tab now to be only valid on company partners + # 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) + # Trying to set the tab back on type should be impossible: + 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, + 'contact_type_left': 'c', + 'tab_left_id': executive_tab.id}) + 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() + self.assertEqual( + type_has_chairperson.tab_left_id.id, + 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)) + # Trying to clear either category should raise ValidationError: + with self.assertRaises(ValidationError): + type_has_chairperson.write({'partner_category_left': False}) + with self.assertRaises(ValidationError): + type_has_chairperson.write({'partner_category_right': False}) + # Trying to clear either contact type should raise ValidationError: + with self.assertRaises(ValidationError): + type_has_chairperson.write({'contact_type_left': False}) + with self.assertRaises(ValidationError): + type_has_chairperson.write({'contact_type_right': False}) + + def test_relations(self): + """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'] + 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)) + # 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) + # 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) + self.assertEqual( + executive_partners.other_partner_id.id, + important_person.id) + # 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_partner_id() + self.assertTrue(onchange_result) + self.assertIn('domain', onchange_result) + self.assertIn('type_selection_id', onchange_result['domain']) + self.assertEqual( + 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) diff --git a/partner_multi_relation_tabs/views/menu.xml b/partner_multi_relation_tabs/views/menu.xml new file mode 100644 index 000000000..0859c9159 --- /dev/null +++ b/partner_multi_relation_tabs/views/menu.xml @@ -0,0 +1,18 @@ + + + + + + + + diff --git a/partner_multi_relation_tabs/views/res_partner_relation_all.xml b/partner_multi_relation_tabs/views/res_partner_relation_all.xml new file mode 100644 index 000000000..adce7fcaa --- /dev/null +++ b/partner_multi_relation_tabs/views/res_partner_relation_all.xml @@ -0,0 +1,17 @@ + + + + + res.partner.relation.all + + + + + + + + + diff --git a/partner_multi_relation_tabs/views/res_partner_relation_type.xml b/partner_multi_relation_tabs/views/res_partner_relation_type.xml new file mode 100644 index 000000000..ce8e2c6d1 --- /dev/null +++ b/partner_multi_relation_tabs/views/res_partner_relation_type.xml @@ -0,0 +1,20 @@ + + + + + res.partner.relation.type + + + + + + + + + + + + diff --git a/partner_multi_relation_tabs/views/res_partner_tab.xml b/partner_multi_relation_tabs/views/res_partner_tab.xml new file mode 100644 index 000000000..db1d1170e --- /dev/null +++ b/partner_multi_relation_tabs/views/res_partner_tab.xml @@ -0,0 +1,29 @@ + + + + + res.partner.tab + + + + + + + + + + res.partner.tab + +
+ + + + + + + +
+
+
+ +