Browse Source

Merge pull request #301 from hbrunn/8.0-partner_relations-v8api

[RFR] v8 api, guidelines
pull/390/head
Pedro M. Baeza 8 years ago
committed by GitHub
parent
commit
e79f11591a
  1. 23
      partner_relations/__init__.py
  2. 33
      partner_relations/__openerp__.py
  3. 13
      partner_relations/data/demo.xml
  4. 2
      partner_relations/i18n/da.po
  5. 2
      partner_relations/i18n/de.po
  6. 2
      partner_relations/i18n/es.po
  7. 2
      partner_relations/i18n/fi.po
  8. 2
      partner_relations/i18n/fr.po
  9. 2
      partner_relations/i18n/it.po
  10. 401
      partner_relations/i18n/nl.po
  11. 297
      partner_relations/i18n/partner_relations.pot
  12. 2
      partner_relations/i18n/pt_BR.po
  13. 2
      partner_relations/i18n/sl.po
  14. 26
      partner_relations/model/__init__.py
  15. 339
      partner_relations/model/res_partner.py
  16. 376
      partner_relations/model/res_partner_relation.py
  17. 237
      partner_relations/model/res_partner_relation_all.py
  18. 70
      partner_relations/model/res_partner_relation_type.py
  19. 175
      partner_relations/model/res_partner_relation_type_selection.py
  20. 8
      partner_relations/models/__init__.py
  21. 172
      partner_relations/models/res_partner.py
  22. 156
      partner_relations/models/res_partner_relation.py
  23. 347
      partner_relations/models/res_partner_relation_all.py
  24. 176
      partner_relations/models/res_partner_relation_type.py
  25. 127
      partner_relations/models/res_partner_relation_type_selection.py
  26. 22
      partner_relations/test/test_allow.yml
  27. 8
      partner_relations/tests/__init__.py
  28. 280
      partner_relations/tests/test_partner_relation.py
  29. 241
      partner_relations/tests/test_partner_relation_all.py
  30. 113
      partner_relations/tests/test_partner_relation_common.py
  31. 81
      partner_relations/tests/test_partner_relations.py
  32. 76
      partner_relations/tests/test_partner_search.py
  33. 97
      partner_relations/view/res_partner_relation.xml
  34. 92
      partner_relations/view/res_partner_relation_all.xml
  35. 2
      partner_relations/views/menu.xml
  36. 34
      partner_relations/views/res_partner.xml
  37. 96
      partner_relations/views/res_partner_relation_all.xml
  38. 23
      partner_relations/views/res_partner_relation_type.xml

23
partner_relations/__init__.py

@ -1,22 +1,5 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# This module copyright (C) 2013 Therp BV (<http://therp.nl>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
from . import model
# © 2013-2016 Therp BV <http://therp.nl>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from . import models
from . import tests

33
partner_relations/__openerp__.py

@ -1,23 +1,6 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# This module copyright (C) 2013 Therp BV (<http://therp.nl>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
# © 2013-2016 Therp BV <http://therp.nl>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
{
"name": "Partner relations",
"version": "8.0.1.1.1",
@ -31,15 +14,11 @@
"demo": [
"data/demo.xml",
],
"test": [
"test/test_allow.yml",
],
"data": [
"view/res_partner_relation_all.xml",
'view/res_partner_relation.xml',
'view/res_partner.xml',
'view/res_partner_relation_type.xml',
'view/menu.xml',
"views/res_partner_relation_all.xml",
'views/res_partner.xml',
'views/res_partner_relation_type.xml',
'views/menu.xml',
'security/ir.model.access.csv',
],
"auto_install": False,

13
partner_relations/data/demo.xml

@ -2,20 +2,21 @@
<openerp>
<data>
<record id="rel_type_assistant" model="res.partner.relation.type">
<field name="name">Is assistant of</field>
<field name="name_inverse">Has assistant</field>
<field name="name">is assistant of</field>
<field name="name_inverse">has assistant</field>
<field name="contact_type_left">p</field>
<field name="contact_type_right">p</field>
</record>
<record id="rel_type_competitor" model="res.partner.relation.type">
<field name="name">Is competitor of</field>
<field name="name_inverse">Is competitor of</field>
<field name="name">is competitor of</field>
<field name="name_inverse">is competitor of</field>
<field name="contact_type_left">c</field>
<field name="contact_type_right">c</field>
<field name="is_symmetric" eval="True" />
</record>
<record id="rel_type_has_worked_for" model="res.partner.relation.type">
<field name="name">Has worked for</field>
<field name="name_inverse">Has former employee</field>
<field name="name">works for</field>
<field name="name_inverse">has employee</field>
<field name="contact_type_left">p</field>
<field name="contact_type_right">c</field>
</record>

2
partner_relations/i18n/da.po

@ -124,7 +124,7 @@ msgid "Has former employee"
msgstr ""
#. module: partner_relations
#: field:res.partner,search_relation_id:0
#: field:res.partner,search_relation_type_id:0
msgid "Has relation of type"
msgstr ""

2
partner_relations/i18n/de.po

@ -124,7 +124,7 @@ msgid "Has former employee"
msgstr "Hat ehemaligen Mitarbeiter"
#. module: partner_relations
#: field:res.partner,search_relation_id:0
#: field:res.partner,search_relation_type_id:0
msgid "Has relation of type"
msgstr "Hat Beziehungsart"

2
partner_relations/i18n/es.po

@ -125,7 +125,7 @@ msgid "Has former employee"
msgstr "Tiene ex-empleado"
#. module: partner_relations
#: field:res.partner,search_relation_id:0
#: field:res.partner,search_relation_type_id:0
msgid "Has relation of type"
msgstr "Tiene una relación de tipo"

2
partner_relations/i18n/fi.po

@ -123,7 +123,7 @@ msgid "Has former employee"
msgstr ""
#. module: partner_relations
#: field:res.partner,search_relation_id:0
#: field:res.partner,search_relation_type_id:0
msgid "Has relation of type"
msgstr ""

2
partner_relations/i18n/fr.po

@ -126,7 +126,7 @@ msgid "Has former employee"
msgstr ""
#. module: partner_relations
#: field:res.partner,search_relation_id:0
#: field:res.partner,search_relation_type_id:0
msgid "Has relation of type"
msgstr "A une relation de type"

2
partner_relations/i18n/it.po

@ -125,7 +125,7 @@ msgid "Has former employee"
msgstr ""
#. module: partner_relations
#: field:res.partner,search_relation_id:0
#: field:res.partner,search_relation_type_id:0
msgid "Has relation of type"
msgstr ""

401
partner_relations/i18n/nl.po

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

297
partner_relations/i18n/partner_relations.pot

@ -6,8 +6,8 @@ msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 8.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2015-04-15 16:09+0000\n"
"PO-Revision-Date: 2015-04-15 16:09+0000\n"
"POT-Creation-Date: 2016-09-02 12:39+0000\n"
"PO-Revision-Date: 2016-09-02 12:39+0000\n"
"Last-Translator: <>\n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
@ -16,16 +16,22 @@ msgstr ""
"Plural-Forms: \n"
#. module: partner_relations
#: model:ir.actions.act_window,help:partner_relations.action_res_partner_relation
#: code:addons/partner_relations/models/res_partner_relation_all.py:178
#, python-format
msgid "%s partner incompatible with relation type."
msgstr ""
#. module: partner_relations
#: model:ir.actions.act_window,help:partner_relations.action_res_partner_relation_all
msgid "<p class=\"oe_view_nocontent_create\">\n"
" Record and track your partners' relations. Relations may be linked to other partners with a type either directly or inversely.\n"
" Record and track your partners' relations. Relations may\n"
" be linked to other partners with a type either directly\n"
" or inversely.\n"
" </p>\n"
" "
msgstr ""
#. module: partner_relations
#: field:res.partner.relation,active:0
#: field:res.partner.relation.all,active:0
msgid "Active"
msgstr ""
@ -46,9 +52,10 @@ msgid "All relations with current partner"
msgstr ""
#. module: partner_relations
#: code:addons/partner_relations/model/res_partner_relation_type.py:64
#: code:addons/partner_relations/models/res_partner_relation_type.py:13
#: selection:res.partner.relation.type,handle_invalid_onchange:0
#, python-format
msgid "Company"
msgid "Allow existing relations that do not fit changed conditions"
msgstr ""
#. module: partner_relations
@ -63,14 +70,8 @@ msgstr ""
msgid "Created on"
msgstr ""
#. module: partner_relations
#: field:res.partner.relation.all,this_partner_id:0
msgid "Current Partner"
msgstr ""
#. module: partner_relations
#: field:res.partner.relation.type.selection,partner_category_this:0
#: field:res.partner.relation.type.selection,search_partner_category_this:0
msgid "Current record's category"
msgstr ""
@ -79,11 +80,40 @@ msgstr ""
msgid "Current record's partner type"
msgstr ""
#. module: partner_relations
#: code:addons/partner_relations/models/res_partner_relation_type.py:17
#: selection:res.partner.relation.type,handle_invalid_onchange:0
#, python-format
msgid "Delete relations that do not fit changed conditions"
msgstr ""
#. module: partner_relations
#: field:res.partner.relation,right_partner_id:0
msgid "Destination Partner"
msgstr ""
#. module: partner_relations
#: field:res.partner.relation,display_name:0
#: field:res.partner.relation.all,display_name:0
#: field:res.partner.relation.type,display_name:0
#: field:res.partner.relation.type.selection,display_name:0
msgid "Display Name"
msgstr ""
#. module: partner_relations
#: code:addons/partner_relations/models/res_partner_relation_type.py:11
#: selection:res.partner.relation.type,handle_invalid_onchange:0
#, python-format
msgid "Do not allow change that will result in invalid relations"
msgstr ""
#. module: partner_relations
#: code:addons/partner_relations/models/res_partner_relation_type.py:15
#: selection:res.partner.relation.type,handle_invalid_onchange:0
#, python-format
msgid "End relations per today, if they do not fit changed conditions"
msgstr ""
#. module: partner_relations
#: field:res.partner.relation,date_end:0
#: field:res.partner.relation.all,date_end:0
@ -91,13 +121,29 @@ msgid "Ending date"
msgstr ""
#. module: partner_relations
#: view:res.partner.relation:partner_relations.search_res_partner_relation
#: code:addons/partner_relations/models/res_partner_relation_all.py:184
#: code:addons/partner_relations/models/res_partner_relation_all.py:259
#, python-format
msgid "Error!"
msgstr ""
#. module: partner_relations
#: view:res.partner.relation.all:partner_relations.search_res_partner_relation_all
msgid "Group By"
msgstr ""
#. module: partner_relations
#: field:res.partner,search_relation_id:0
#: model:res.partner.relation.type,name_inverse:partner_relations.rel_type_assistant
msgid "Has assistant"
msgstr ""
#. module: partner_relations
#: model:res.partner.relation.type,name_inverse:partner_relations.rel_type_has_worked_for
msgid "Has former employee"
msgstr ""
#. module: partner_relations
#: field:res.partner,search_relation_type_id:0
msgid "Has relation of type"
msgstr ""
@ -111,6 +157,11 @@ msgstr ""
msgid "Has relation with a partner in category"
msgstr ""
#. module: partner_relations
#: model:res.partner.relation.type,name:partner_relations.rel_type_has_worked_for
msgid "Has worked for"
msgstr ""
#. module: partner_relations
#: field:res.partner.relation,id:0
#: field:res.partner.relation.all,id:0
@ -119,15 +170,48 @@ msgstr ""
msgid "ID"
msgstr ""
#. module: partner_relations
#: view:res.partner.relation.all:partner_relations.search_res_partner_relation_all
msgid "Include past records"
msgstr ""
#. module: partner_relations
#: field:res.partner.relation.type,handle_invalid_onchange:0
msgid "Invalid relation handling"
msgstr ""
#. module: partner_relations
#: field:res.partner.relation.type,name_inverse:0
msgid "Inverse name"
msgstr ""
#. module: partner_relations
#: selection:res.partner.relation.all,record_type:0
#: selection:res.partner.relation.type.selection,record_type:0
msgid "Inverse type"
#: help:res.partner.relation.type.selection,is_inverse:0
msgid "Inverse relations are from right to left partner."
msgstr ""
#. module: partner_relations
#: model:res.partner.relation.type,name:partner_relations.rel_type_assistant
msgid "Is assistant of"
msgstr ""
#. module: partner_relations
#: model:res.partner.relation.type,name:partner_relations.rel_type_competitor
#: model:res.partner.relation.type,name_inverse:partner_relations.rel_type_competitor
msgid "Is competitor of"
msgstr ""
#. module: partner_relations
#: field:res.partner.relation.type.selection,is_inverse:0
msgid "Is reverse type?"
msgstr ""
#. module: partner_relations
#: field:res.partner.relation,__last_update:0
#: field:res.partner.relation.all,__last_update:0
#: field:res.partner.relation.type,__last_update:0
#: field:res.partner.relation.type.selection,__last_update:0
msgid "Last Modified on"
msgstr ""
#. module: partner_relations
@ -143,18 +227,13 @@ msgid "Last Updated on"
msgstr ""
#. module: partner_relations
#: view:res.partner.relation:partner_relations.search_res_partner_relation
msgid "Left Partner"
msgstr ""
#. module: partner_relations
#: field:res.partner.relation,left_contact_type:0
msgid "Left Partner Type"
#: field:res.partner.relation.type,partner_category_left:0
msgid "Left partner category"
msgstr ""
#. module: partner_relations
#: field:res.partner.relation.type,partner_category_left:0
msgid "Left partner category"
#: selection:res.partner.relation.all,record_type:0
msgid "Left partner to right partner"
msgstr ""
#. module: partner_relations
@ -167,12 +246,41 @@ msgstr ""
msgid "Left side of relation"
msgstr ""
#. module: partner_relations
#: view:res.partner.relation.all:partner_relations.search_res_partner_relation_all
msgid "Left to right"
msgstr ""
#. module: partner_relations
#: field:res.partner.relation.type,name:0
#: field:res.partner.relation.type.selection,name:0
msgid "Name"
msgstr ""
#. module: partner_relations
#: code:addons/partner_relations/models/res_partner_relation_all.py:181
#, python-format
msgid "No %s partner available for relation type."
msgstr ""
#. module: partner_relations
#: code:addons/partner_relations/models/res_partner_relation_all.py:256
#, python-format
msgid "No relation type available for selected partners."
msgstr ""
#. module: partner_relations
#: view:res.partner.relation.all:partner_relations.search_res_partner_relation_all
#: field:res.partner.relation.all,this_partner_id:0
msgid "One Partner"
msgstr ""
#. module: partner_relations
#: code:addons/partner_relations/models/res_partner_relation_type.py:82
#, python-format
msgid "Organisation"
msgstr ""
#. module: partner_relations
#: view:res.partner.relation.all:partner_relations.search_res_partner_relation_all
#: field:res.partner.relation.all,other_partner_id:0
@ -191,22 +299,16 @@ msgstr ""
#. module: partner_relations
#: model:ir.model,name:partner_relations.model_res_partner
#: field:res.partner.relation,partner_id_display:0
#: field:res.partner.relation.all,any_partner_id:0
msgid "Partner"
msgstr ""
#. module: partner_relations
#: view:res.partner.relation:partner_relations.form_res_partner_relation
msgid "Partner Relation"
msgstr ""
#. module: partner_relations
#: model:ir.model,name:partner_relations.model_res_partner_relation_type
msgid "Partner Relation Type"
msgstr ""
#. module: partner_relations
#: view:res.partner.relation:partner_relations.tree_res_partner_relation
#: view:res.partner.relation.all:partner_relations.tree_res_partner_relation_all
msgid "Partner Relations"
msgstr ""
@ -217,45 +319,42 @@ msgstr ""
msgid "Partner Relations Types"
msgstr ""
#. module: partner_relations
#: field:res.partner.relation.all,contact_type:0
msgid "Partner Type"
msgstr ""
#. module: partner_relations
#: model:ir.model,name:partner_relations.model_res_partner_relation
#: view:res.partner.relation.all:partner_relations.form_res_partner_relation_all
#: view:res.partner.relation.type:partner_relations.form_res_partner_relation_type
#: view:res.partner.relation.type:partner_relations.tree_res_partner_relation_type
msgid "Partner relation"
msgstr ""
#. module: partner_relations
#: code:addons/partner_relations/model/res_partner_relation.py:291
#: code:addons/partner_relations/models/res_partner_relation.py:115
#, python-format
msgid "Partners cannot have a relation with themselves."
msgstr ""
#. module: partner_relations
#: code:addons/partner_relations/model/res_partner_relation_type.py:65
#: code:addons/partner_relations/models/res_partner_relation_type.py:83
#, python-format
msgid "Person"
msgstr ""
#. module: partner_relations
#: view:res.partner.relation.type:partner_relations.form_res_partner_relation_type
msgid "Properties"
msgstr ""
#. module: partner_relations
#: field:res.partner.relation.all,record_type:0
msgid "Record Type"
msgstr ""
#. module: partner_relations
#: field:res.partner.relation.type.selection,record_type:0
msgid "Record type"
#: help:res.partner.relation.all,active:0
msgid "Records with date_end in the past are inactive"
msgstr ""
#. module: partner_relations
#: code:addons/partner_relations/model/res_partner_relation.py:360
#, python-format
msgid "Related partners"
#: field:res.partner.relation.type,allow_self:0
#: field:res.partner.relation.type.selection,allow_self:0
msgid "Reflexive"
msgstr ""
#. module: partner_relations
@ -264,44 +363,46 @@ msgid "Relation"
msgstr ""
#. module: partner_relations
#: field:res.partner.relation.all,type_id:0
#: field:res.partner,relation_count:0
msgid "Relation Count"
msgstr ""
#. module: partner_relations
#: field:res.partner.relation.all,type_selection_id:0
msgid "Relation Type"
msgstr ""
#. module: partner_relations
#: code:addons/partner_relations/models/res_partner_relation_all.py:252
#, python-format
msgid "Relation type incompatible with selected partner(s)."
msgstr ""
#. module: partner_relations
#: field:res.partner,search_relation_date:0
msgid "Relation valid"
msgstr ""
#. module: partner_relations
#: model:ir.actions.act_window,name:partner_relations.action_res_partner_relation
#: model:ir.actions.act_window,name:partner_relations.action_res_partner_relation_all
#: model:ir.ui.menu,name:partner_relations.menu_res_partner_relation_sales
#: view:res.partner:partner_relations.view_partner_form
#: field:res.partner,relation_ids:0
msgid "Relations"
msgstr ""
#. module: partner_relations
#: view:res.partner.relation:partner_relations.search_res_partner_relation
#: view:res.partner.relation.all:partner_relations.search_res_partner_relation_all
msgid "Relationship Type"
msgstr ""
#. module: partner_relations
#: view:res.partner.relation:partner_relations.search_res_partner_relation
msgid "Right Partner"
msgstr ""
#. module: partner_relations
#: field:res.partner.relation,right_contact_type:0
msgid "Right Partner Type"
#: field:res.partner.relation.type,partner_category_right:0
msgid "Right partner category"
msgstr ""
#. module: partner_relations
#: field:res.partner.relation.type,partner_category_right:0
msgid "Right partner category"
#: selection:res.partner.relation.all,record_type:0
msgid "Right partner to left partner"
msgstr ""
#. module: partner_relations
@ -315,19 +416,18 @@ msgid "Right side of relation"
msgstr ""
#. module: partner_relations
#: view:res.partner.relation:partner_relations.search_res_partner_relation
#: view:res.partner.relation.all:partner_relations.search_res_partner_relation_all
msgid "Search Relations"
msgid "Right to left"
msgstr ""
#. module: partner_relations
#: model:ir.actions.act_window,name:partner_relations.action_show_partner_relations
msgid "Show partner's relations"
#: view:res.partner.relation.all:partner_relations.search_res_partner_relation_all
msgid "Search Relations"
msgstr ""
#. module: partner_relations
#: model:ir.actions.server,name:partner_relations.action_show_right_relation_partners
msgid "Show partners"
#: model:ir.actions.act_window,name:partner_relations.action_show_partner_relations
msgid "Show partner's relations"
msgstr ""
#. module: partner_relations
@ -342,35 +442,80 @@ msgid "Starting date"
msgstr ""
#. module: partner_relations
#: code:addons/partner_relations/model/res_partner_relation.py:278
#: field:res.partner.relation.type,is_symmetric:0
#: field:res.partner.relation.type.selection,is_symmetric:0
msgid "Symmetric"
msgstr ""
#. module: partner_relations
#: code:addons/partner_relations/models/res_partner_relation.py:101
#, python-format
msgid "The %s partner does not have category %s."
msgstr ""
#. module: partner_relations
#: code:addons/partner_relations/models/res_partner_relation.py:95
#, python-format
msgid "The %s partner is not applicable for this relation type."
msgstr ""
#. module: partner_relations
#: code:addons/partner_relations/model/res_partner_relation.py:245
#: code:addons/partner_relations/models/res_partner_relation.py:61
#, python-format
msgid "The starting date cannot be after the ending date."
msgstr ""
#. module: partner_relations
#: code:addons/partner_relations/model/res_partner_relation.py:319
#: code:addons/partner_relations/models/res_partner_relation_type.py:168
#, python-format
msgid "There are already relations not satisfying the conditions for partner type or category."
msgstr ""
#. module: partner_relations
#: code:addons/partner_relations/models/res_partner_relation.py:154
#, python-format
msgid "There is already a similar relation with overlapping dates"
msgstr ""
#. module: partner_relations
#: help:res.partner.relation.type,allow_self:0
msgid "This relation can be set up with the same partner left and right"
msgstr ""
#. module: partner_relations
#: help:res.partner.relation.type,is_symmetric:0
msgid "This relation is the same from right to left as from left to right"
msgstr ""
#. module: partner_relations
#: field:res.partner.relation,type_id:0
#: field:res.partner.relation,type_selection_id:0
#: selection:res.partner.relation.all,record_type:0
#: selection:res.partner.relation.type.selection,record_type:0
#: field:res.partner.relation.type.selection,type_id:0
msgid "Type"
msgstr ""
#. module: partner_relations
#: code:addons/partner_relations/model/res_partner.py:109
#: code:addons/partner_relations/models/res_partner.py:80
#: code:addons/partner_relations/models/res_partner.py:121
#, python-format
msgid "Unsupported search operator \"%s\""
msgstr ""
#. module: partner_relations
#: help:res.partner.relation.type,handle_invalid_onchange:0
msgid "When adding relations criteria like partner type and category are checked.\n"
"However when you change the criteria, there might be relations that do not fit the new criteria.\n"
"Specify how this situation should be handled."
msgstr ""
#. module: partner_relations
#: code:addons/partner_relations/models/res_partner_relation_all.py:221
#, python-format
msgid "other"
msgstr ""
#. module: partner_relations
#: code:addons/partner_relations/models/res_partner_relation_all.py:215
#, python-format
msgid "Unsupported search operand \"%s\""
msgid "this"
msgstr ""

2
partner_relations/i18n/pt_BR.po

@ -125,7 +125,7 @@ msgid "Has former employee"
msgstr "Tem ex-empregado"
#. module: partner_relations
#: field:res.partner,search_relation_id:0
#: field:res.partner,search_relation_type_id:0
msgid "Has relation of type"
msgstr "Tem relação do tipo"

2
partner_relations/i18n/sl.po

@ -124,7 +124,7 @@ msgid "Has former employee"
msgstr "Ima bivšega zaposlenega"
#. module: partner_relations
#: field:res.partner,search_relation_id:0
#: field:res.partner,search_relation_type_id:0
msgid "Has relation of type"
msgstr "Ima odnos tipa"

26
partner_relations/model/__init__.py

@ -1,26 +0,0 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# This module copyright (C) 2013 Therp BV (<http://therp.nl>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
from . import res_partner
from . import res_partner_relation
from . import res_partner_relation_type
from . import res_partner_relation_all
from . import res_partner_relation_type_selection

339
partner_relations/model/res_partner.py

@ -1,339 +0,0 @@
# -*- coding: utf-8 -*-
'''Extend res.partner model'''
##############################################################################
#
# OpenERP, Open Source Management Solution
# This module copyright (C) 2013 Therp BV (<http://therp.nl>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
import time
from openerp import osv, models, fields, exceptions, api
from openerp.osv.expression import is_leaf, AND, OR, FALSE_LEAF
from openerp.tools import DEFAULT_SERVER_DATE_FORMAT
from openerp.tools.translate import _
PADDING = 10
def get_partner_type(partner):
"""Get partner type for relation.
:param partner: a res.partner either a company or not
:return: 'c' for company or 'p' for person
:rtype: str
"""
return 'c' if partner.is_company else 'p'
class ResPartner(models.Model):
_inherit = 'res.partner'
relation_count = fields.Integer(
'Relation Count',
compute="_count_relations"
)
@api.one
@api.depends("relation_ids")
def _count_relations(self):
"""Count the number of relations this partner has for Smart Button
Don't count inactive relations.
"""
self.relation_count = len([r for r in self.relation_ids if r.active])
def _get_relation_ids_select(self, cr, uid, ids, field_name, arg,
context=None):
'''return the partners' relations as tuple
(id, left_partner_id, right_partner_id)'''
cr.execute(
'''select id, left_partner_id, right_partner_id
from res_partner_relation
where (left_partner_id in %s or right_partner_id in %s)''' +
' order by ' + self.pool['res.partner.relation']._order,
(tuple(ids), tuple(ids))
)
return cr.fetchall()
def _get_relation_ids(
self, cr, uid, ids, field_name, arg, context=None):
'''getter for relation_ids'''
if context is None:
context = {}
result = dict([(i, []) for i in ids])
# TODO: do a permission test on returned ids
for row in self._get_relation_ids_select(
cr, uid, ids, field_name, arg, context=context):
if row[1] in result:
result[row[1]].append(row[0])
if row[2] in result:
result[row[2]].append(row[0])
return result
def _set_relation_ids(
self, cr, uid, ids, dummy_name, field_value, dummy_arg,
context=None):
'''setter for relation_ids'''
if context is None:
context = {}
relation_obj = self.pool.get('res.partner.relation')
context2 = self.with_partner_relations_context(
cr, uid, ids, context=context).env.context
for value in field_value:
if value[0] == 0:
relation_obj.create(cr, uid, value[2], context=context2)
if value[0] == 1:
# if we write partner_id_display, we also need to pass
# type_selection_id in order to have this write end up on
# the correct field
if 'partner_id_display' in value[2] and 'type_selection_id'\
not in value[2]:
relation_data = relation_obj.read(
cr, uid, [value[1]], ['type_selection_id'],
context=context)[0]
value[2]['type_selection_id'] =\
relation_data['type_selection_id']
relation_obj.write(
cr, uid, value[1], value[2], context=context2)
if value[0] == 2:
relation_obj.unlink(cr, uid, value[1], context=context2)
def _search_relation_id(
self, cr, uid, dummy_obj, name, args, context=None):
result = []
for arg in args:
if isinstance(arg, tuple) and arg[0] == name:
if arg[1] not in ['=', '!=', 'like', 'not like', 'ilike',
'not ilike', 'in', 'not in']:
raise exceptions.ValidationError(
_('Unsupported search operand "%s"') % arg[1])
relation_type_selection_ids = []
relation_type_selection = self\
.pool['res.partner.relation.type.selection']
if arg[1] == '=' and isinstance(arg[2], (long, int)):
relation_type_selection_ids.append(arg[2])
elif arg[1] == '!=' and isinstance(arg[2], (long, int)):
type_id, is_inverse = (
relation_type_selection.browse(cr, uid, arg[2],
context=context)
.get_type_from_selection_id()
)
result = OR([
result,
[
('relation_all_ids.type_id', '!=', type_id),
]
])
continue
else:
relation_type_selection_ids = relation_type_selection\
.search(
cr, uid,
[
('type_id.name', arg[1], arg[2]),
('record_type', '=', 'a'),
],
context=context)
relation_type_selection_ids.extend(
relation_type_selection.search(
cr, uid,
[
('type_id.name_inverse', arg[1], arg[2]),
('record_type', '=', 'b'),
],
context=context))
if not relation_type_selection_ids:
result = AND([result, [FALSE_LEAF]])
for relation_type_selection_id in relation_type_selection_ids:
type_id, is_inverse = (
relation_type_selection.browse(
cr, uid, relation_type_selection_id,
context=context
).get_type_from_selection_id()
)
result = OR([
result,
[
'&',
('relation_all_ids.type_id', '=', type_id),
('relation_all_ids.record_type', '=',
'b' if is_inverse else 'a')
],
])
return result
def _search_relation_date(self, cr, uid, obj, name, args, context=None):
result = []
for arg in args:
if isinstance(arg, tuple) and arg[0] == name:
# TODO: handle {<,>}{,=}
if arg[1] != '=':
continue
result.extend([
'&',
'|',
('relation_all_ids.date_start', '=', False),
('relation_all_ids.date_start', '<=', arg[2]),
'|',
('relation_all_ids.date_end', '=', False),
('relation_all_ids.date_end', '>=', arg[2]),
])
return result
def _search_related_partner_id(
self, cr, uid, dummy_obj, name, args, context=None):
result = []
for arg in args:
if isinstance(arg, tuple) and arg[0] == name:
result.append(
(
'relation_all_ids.other_partner_id',
arg[1],
arg[2],
))
return result
def _search_related_partner_category_id(
self, cr, uid, dummy_obj, name, args, context=None):
result = []
for arg in args:
if isinstance(arg, tuple) and arg[0] == name:
result.append(
(
'relation_all_ids.other_partner_id.category_id',
arg[1],
arg[2],
))
return result
_columns = {
'relation_ids': osv.fields.function(
lambda self, *args, **kwargs: self._get_relation_ids(
*args, **kwargs),
fnct_inv=_set_relation_ids,
type='one2many', obj='res.partner.relation',
string='Relations',
selectable=False,
),
'relation_all_ids': osv.fields.one2many(
'res.partner.relation.all', 'this_partner_id',
string='All relations with current partner',
auto_join=True,
selectable=False,
),
'search_relation_id': osv.fields.function(
lambda self, cr, uid, ids, *args: dict([
(i, False) for i in ids]),
fnct_search=_search_relation_id,
string='Has relation of type',
type='many2one', obj='res.partner.relation.type.selection'
),
'search_relation_partner_id': osv.fields.function(
lambda self, cr, uid, ids, *args: dict([
(i, False) for i in ids]),
fnct_search=_search_related_partner_id,
string='Has relation with',
type='many2one', obj='res.partner'
),
'search_relation_date': osv.fields.function(
lambda self, cr, uid, ids, *args: dict([
(i, False) for i in ids]),
fnct_search=_search_relation_date,
string='Relation valid', type='date'
),
'search_relation_partner_category_id': osv.fields.function(
lambda self, cr, uid, ids, *args: dict([
(i, False) for i in ids]),
fnct_search=_search_related_partner_category_id,
string='Has relation with a partner in category',
type='many2one', obj='res.partner.category'
),
}
def copy_data(self, cr, uid, id, default=None, context=None):
if default is None:
default = {}
default.setdefault('relation_ids', [])
default.setdefault('relation_all_ids', [])
return super(ResPartner, self).copy_data(cr, uid, id, default=default,
context=context)
def search(self, cr, uid, args, offset=0, limit=None, order=None,
context=None, count=False):
if context is None:
context = {}
# inject searching for current relation date if we search for relation
# properties and no explicit date was given
date_args = []
for arg in args:
if is_leaf(arg) and arg[0].startswith('search_relation'):
if arg[0] == 'search_relation_date':
date_args = []
break
if not date_args:
date_args = [
('search_relation_date', '=', time.strftime(
DEFAULT_SERVER_DATE_FORMAT))]
# because of auto_join, we have to do the active test by hand
active_args = []
if context.get('active_test', True):
for arg in args:
if is_leaf(arg) and\
arg[0].startswith('search_relation'):
active_args = [('relation_all_ids.active', '=', True)]
break
return super(ResPartner, self).search(
cr, uid, args + date_args + active_args, offset=offset,
limit=limit, order=order, context=context, count=count)
@api.v7
def read(self, cr, user, ids, fields=None, context=None,
load='_classic_read'):
return super(ResPartner, self).read(
cr, user, ids, fields=fields, context=context, load=load)
@api.v8
def read(self, fields=None, load='_classic_read'):
return super(ResPartner, self.with_partner_relations_context())\
.read(fields=fields, load=load)
@api.multi
def write(self, vals):
return super(ResPartner, self.with_partner_relations_context())\
.write(vals)
@api.multi
def with_partner_relations_context(self):
context = dict(self.env.context)
if context.get('active_model', self._name) == self._name:
existing = self.exists()
context.setdefault(
'active_id', existing.ids[0] if existing.ids else None)
context.setdefault('active_ids', existing.ids)
context.setdefault('active_model', self._name)
return self.with_context(context)

376
partner_relations/model/res_partner_relation.py

@ -1,376 +0,0 @@
# -*- coding: utf-8 -*-
'''Define model res.partner.relation'''
##############################################################################
#
# OpenERP, Open Source Management Solution
# This module copyright (C) 2013 Therp BV (<http://therp.nl>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
from openerp import osv, models, fields, api, exceptions, _
from .res_partner import get_partner_type
class ResPartnerRelation(models.Model):
'''Model res.partner.relation is used to describe all links or relations
between partners in the database.
In many parts of the code we have to know whether the active partner is
the left partner, or the right partner. If the active partner is the
right partner we have to show the inverse name.
Because the active partner is crucial for the working of partner
relationships, we make sure on the res.partner model that the partner id
is set in the context where needed.
'''
_name = 'res.partner.relation'
_description = 'Partner relation'
_order = 'active desc, date_start desc, date_end desc'
def _search_any_partner_id(self, operator, value):
return [
'|',
('left_partner_id', operator, value),
('right_partner_id', operator, value),
]
def _get_computed_fields(
self, cr, uid, ids, field_names, arg, context=None):
'''Return a dictionary of dictionaries, with for every partner for
ids, the computed values.'''
def get_values(self, dummy_field_names, dummy_arg, context=None):
'''Get computed values for record'''
values = {}
on_right_partner = self._on_right_partner(self.right_partner_id.id)
# type_selection_id
values['type_selection_id'] = (
((self.type_id.id) * 10) + (on_right_partner and 1 or 0))
# partner_id_display
values['partner_id_display'] = (
self.left_partner_id.id
if on_right_partner
else self.right_partner_id.id
)
return values
return dict([
(i.id, get_values(i, field_names, arg, context=context))
for i in self.browse(cr, uid, ids, context=context)
])
_columns = {
'type_selection_id': osv.fields.function(
_get_computed_fields,
multi="computed_fields",
fnct_inv=lambda *args: None,
type='many2one', obj='res.partner.relation.type.selection',
string='Type',
),
'partner_id_display': osv.fields.function(
_get_computed_fields,
multi="computed_fields",
fnct_inv=lambda *args: None,
type='many2one', obj='res.partner',
string='Partner'
),
}
allow_self = fields.Boolean(related='type_id.allow_self')
left_contact_type = fields.Selection(
lambda s: s.env['res.partner.relation.type']._get_partner_types(),
'Left Partner Type',
compute='_get_partner_type_any',
store=True,
)
right_contact_type = fields.Selection(
lambda s: s.env['res.partner.relation.type']._get_partner_types(),
'Right Partner Type',
compute='_get_partner_type_any',
store=True,
)
any_partner_id = fields.Many2many(
'res.partner',
string='Partner',
compute='_get_partner_type_any',
search='_search_any_partner_id'
)
left_partner_id = fields.Many2one(
'res.partner',
string='Source Partner',
required=True,
auto_join=True,
ondelete='cascade',
)
right_partner_id = fields.Many2one(
'res.partner',
string='Destination Partner',
required=True,
auto_join=True,
ondelete='cascade',
)
type_id = fields.Many2one(
'res.partner.relation.type',
string='Type',
required=True,
auto_join=True,
)
date_start = fields.Date('Starting date')
date_end = fields.Date('Ending date')
active = fields.Boolean('Active', default=True)
@api.one
@api.depends('left_partner_id', 'right_partner_id')
def _get_partner_type_any(self):
self.left_contact_type = get_partner_type(self.left_partner_id)
self.right_contact_type = get_partner_type(self.right_partner_id)
self.any_partner_id = self.left_partner_id + self.right_partner_id
def _on_right_partner(self, cr, uid, right_partner_id, context=None):
'''Determine wether functions are called in a situation where the
active partner is the right partner. Default False!
'''
if (context and 'active_ids' in context and
right_partner_id in context.get('active_ids', [])):
return True
return False
def _correct_vals(self, vals):
"""Fill type and left and right partner id, according to whether
we have a normal relation type or an inverse relation type
"""
vals = vals.copy()
# If type_selection_id ends in 1, it is a reverse relation type
if 'type_selection_id' in vals:
prts_model = self.env['res.partner.relation.type.selection']
type_selection_id = vals['type_selection_id']
(type_id, is_reverse) = (
prts_model.browse(type_selection_id).
get_type_from_selection_id()
)
vals['type_id'] = type_id
if self._context.get('active_id'):
if is_reverse:
vals['right_partner_id'] = self._context['active_id']
else:
vals['left_partner_id'] = self._context['active_id']
if vals.get('partner_id_display'):
if is_reverse:
vals['left_partner_id'] = vals['partner_id_display']
else:
vals['right_partner_id'] = vals['partner_id_display']
if vals.get('other_partner_id'):
if is_reverse:
vals['left_partner_id'] = vals['other_partner_id']
else:
vals['right_partner_id'] = vals['other_partner_id']
del vals['other_partner_id']
if vals.get('contact_type'):
del vals['contact_type']
return vals
@api.multi
def write(self, vals):
"""Override write to correct values, before being stored."""
vals = self._correct_vals(vals)
return super(ResPartnerRelation, self).write(vals)
@api.model
def create(self, vals):
"""Override create to correct values, before being stored."""
vals = self._correct_vals(vals)
return super(ResPartnerRelation, self).create(vals)
def on_change_type_selection_id(
self, cr, uid, dummy_ids, type_selection_id, context=None):
'''Set domain on partner_id_display, when selection a relation type'''
result = {
'domain': {'partner_id_display': []},
'value': {'type_id': False}
}
if not type_selection_id:
return result
prts_model = self.pool['res.partner.relation.type.selection']
type_model = self.pool['res.partner.relation.type']
(type_id, is_reverse) = (
prts_model.get_type_from_selection_id(
cr, uid, type_selection_id)
)
result['value']['type_id'] = type_id
type_obj = type_model.browse(cr, uid, type_id, context=context)
partner_domain = []
check_contact_type = type_obj.contact_type_right
check_partner_category = (
type_obj.partner_category_right and
type_obj.partner_category_right.id
)
if is_reverse:
# partner_id_display is left partner
check_contact_type = type_obj.contact_type_left
check_partner_category = (
type_obj.partner_category_left and
type_obj.partner_category_left.id
)
if check_contact_type == 'c':
partner_domain.append(('is_company', '=', True))
if check_contact_type == 'p':
partner_domain.append(('is_company', '=', False))
if check_partner_category:
partner_domain.append(
('category_id', 'child_of', check_partner_category))
result['domain']['partner_id_display'] = partner_domain
return result
@api.one
@api.constrains('date_start', 'date_end')
def _check_dates(self):
"""End date should not be before start date, if not filled
:raises exceptions.Warning: When constraint is violated
"""
if (self.date_start and self.date_end and
self.date_start > self.date_end):
raise exceptions.Warning(
_('The starting date cannot be after the ending date.')
)
@api.one
@api.constrains('left_partner_id', 'type_id')
def _check_partner_type_left(self):
"""Check left partner for required company or person
:raises exceptions.Warning: When constraint is violated
"""
self._check_partner_type("left")
@api.one
@api.constrains('right_partner_id', 'type_id')
def _check_partner_type_right(self):
"""Check right partner for required company or person
:raises exceptions.Warning: When constraint is violated
"""
self._check_partner_type("right")
@api.one
def _check_partner_type(self, side):
"""Check partner to left or right for required company or person
:param str side: left or right
:raises exceptions.Warning: When constraint is violated
"""
assert side in ['left', 'right']
ptype = getattr(self.type_id, "contact_type_%s" % side)
company = getattr(self, '%s_partner_id' % side).is_company
if (ptype == 'c' and not company) or (ptype == 'p' and company):
raise exceptions.Warning(
_('The %s partner is not applicable for this relation type.') %
side
)
@api.one
@api.constrains('left_partner_id', 'right_partner_id')
def _check_not_with_self(self):
"""Not allowed to link partner to same partner
:raises exceptions.Warning: When constraint is violated
"""
if self.left_partner_id == self.right_partner_id:
if not self.allow_self:
raise exceptions.Warning(
_('Partners cannot have a relation with themselves.')
)
@api.one
@api.constrains('left_partner_id', 'right_partner_id', 'active')
def _check_relation_uniqueness(self):
"""Forbid multiple active relations of the same type between the same
partners
:raises exceptions.Warning: When constraint is violated
"""
if not self.active:
return
domain = [
('type_id', '=', self.type_id.id),
('active', '=', True),
('id', '!=', self.id),
('left_partner_id', '=', self.left_partner_id.id),
('right_partner_id', '=', self.right_partner_id.id),
]
if self.date_start:
domain += ['|', ('date_end', '=', False),
('date_end', '>=', self.date_start)]
if self.date_end:
domain += ['|', ('date_start', '=', False),
('date_start', '<=', self.date_end)]
if self.search(domain):
raise exceptions.Warning(
_('There is already a similar relation with overlapping dates')
)
def get_action_related_partners(self, cr, uid, ids, context=None):
'''return a window action showing a list of partners taking part in the
relations names by ids. Context key 'partner_relations_show_side'
determines if we show 'left' side, 'right' side or 'all' (default)
partners.
If active_model is res.partner.relation.all, left=this and
right=other'''
if context is None:
context = {}
field_names = {}
if context.get('active_model', self._name) == self._name:
field_names = {
'left': ['left'],
'right': ['right'],
'all': ['left', 'right']
}
elif context.get('active_model') == 'res.partner.relation.all':
field_names = {
'left': ['this'],
'right': ['other'],
'all': ['this', 'other']
}
else:
assert False, 'Unknown active_model!'
partner_ids = []
field_names = field_names[
context.get('partner_relations_show_side', 'all')]
field_names = ['%s_partner_id' % n for n in field_names]
for relation in self.pool[context.get('active_model')].read(
cr, uid, ids, context=context, load='_classic_write'):
for name in field_names:
partner_ids.append(relation[name])
return {
'name': _('Related partners'),
'type': 'ir.actions.act_window',
'res_model': 'res.partner',
'domain': [('id', 'in', partner_ids)],
'views': [(False, 'tree'), (False, 'form')],
'view_type': 'form'
}

237
partner_relations/model/res_partner_relation_all.py

@ -1,237 +0,0 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# This module copyright (C) 2014 Therp BV (<http://therp.nl>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
from openerp import models, fields, api
from openerp.tools import drop_view_if_exists
from .res_partner_relation_type_selection import \
ResPartnerRelationTypeSelection
from .res_partner import get_partner_type, PADDING
class ResPartnerRelationAll(models.AbstractModel):
_auto = False
_log_access = False
_name = 'res.partner.relation.all'
_overlays = 'res.partner.relation'
_description = 'All (non-inverse + inverse) relations between partners'
_additional_view_fields = []
'''append to this list if you added fields to res_partner_relation that
you need in this model and related fields are not adequate (ie for sorting)
You must use the same name as in res_partner_relation.
Don't overwrite this list in your declaration but append in _auto_init:
def _auto_init(self, cr, context=None):
self._additional_view_fields.append('my_field')
return super(ResPartnerRelationAll, self)._auto_init(
cr, context=context)
my_field = fields...
'''
this_partner_id = fields.Many2one(
'res.partner',
string='Current Partner',
required=True,
)
other_partner_id = fields.Many2one(
'res.partner',
string='Other Partner',
required=True,
)
type_id = fields.Many2one(
'res.partner.relation.type',
string='Relation Type',
required=True,
)
type_selection_id = fields.Many2one(
'res.partner.relation.type.selection',
string='Relation Type',
required=True,
)
relation_id = fields.Many2one(
'res.partner.relation',
'Relation',
readonly=True,
)
record_type = fields.Selection(
ResPartnerRelationTypeSelection._RECORD_TYPES,
'Record Type',
readonly=True,
)
contact_type = fields.Selection(
lambda s: s.env['res.partner.relation.type']._get_partner_types(),
'Partner Type',
default=lambda self: self._get_default_contact_type()
)
date_start = fields.Date('Starting date')
date_end = fields.Date('Ending date')
active = fields.Boolean('Active', default=True)
def _auto_init(self, cr, context=None):
drop_view_if_exists(cr, self._table)
additional_view_fields = ','.join(self._additional_view_fields)
additional_view_fields = (',' + additional_view_fields)\
if additional_view_fields else ''
cr.execute(
'''create or replace view %(table)s as
select
id * %(padding)d as id,
id as relation_id,
type_id,
cast('a' as char(1)) as record_type,
left_contact_type as contact_type,
left_partner_id as this_partner_id,
right_partner_id as other_partner_id,
date_start,
date_end,
active,
type_id * %(padding)d as type_selection_id
%(additional_view_fields)s
from %(underlying_table)s
union select
id * %(padding)d + 1,
id,
type_id,
cast('b' as char(1)),
right_contact_type,
right_partner_id,
left_partner_id,
date_start,
date_end,
active,
type_id * %(padding)d + 1
%(additional_view_fields)s
from %(underlying_table)s''' % {
'table': self._table,
'padding': PADDING,
'additional_view_fields': additional_view_fields,
'underlying_table': 'res_partner_relation',
}
)
return super(ResPartnerRelationAll, self)._auto_init(
cr, context=context)
def _get_underlying_object(self):
"""Get the record on which this record is overlaid"""
return self.env[self._overlays].browse(self.id / PADDING)
def _get_default_contact_type(self):
partner_id = self._context.get('default_this_partner_id')
if partner_id:
partner = self.env['res.partner'].browse(partner_id)
return get_partner_type(partner)
return False
@api.multi
def name_get(self):
return {
this.id: '%s %s %s' % (
this.this_partner_id.name,
this.type_selection_id.name_get()[0][1],
this.other_partner_id.name,
)
for this in self
}
@api.onchange('type_selection_id')
def onchange_type_selection_id(self):
"""Add domain on other_partner_id according to category_other and
contact_type_other"""
domain = []
if self.type_selection_id.contact_type_other:
domain.append(
('is_company', '=',
self.type_selection_id.contact_type_other == 'c'))
if self.type_selection_id.partner_category_other:
domain.append(
('category_id', 'in',
self.type_selection_id.partner_category_other.ids))
return {
'domain': {
'other_partner_id': domain,
}
}
@api.onchange('this_partner_id')
def onchange_this_partner_id(self):
if not self.this_partner_id:
return {'domain': {'type_selection_id': []}}
return {
'domain': {
'type_selection_id': [
'|',
('contact_type_this', '=', False),
('contact_type_this', '=',
'c' if self.this_partner_id else 'p'),
'|',
('partner_category_this', '=', False),
('partner_category_this', 'in',
self.this_partner_id.category_id.ids),
],
},
}
@api.one
def write(self, vals):
"""divert non-problematic writes to underlying table"""
underlying_objs = self._get_underlying_object()
vals = {
key: val
for key, val in vals.iteritems()
if not self._columns[key].readonly
}
vals['type_selection_id'] = vals.get(
'type_selection_id',
underlying_objs.type_selection_id.id
)
return underlying_objs.write(vals)
@api.model
def create(self, vals):
"""divert non-problematic creates to underlying table
Create a res.partner.relation but return the converted id
"""
vals = {
key: val
for key, val in vals.iteritems()
if not self._columns[key].readonly
}
vals['type_selection_id'] = vals.get(
'type_selection_id',
False,
)
res = self.env[self._overlays].create(vals)
return self.browse(res.id * PADDING)
@api.one
def unlink(self):
"""divert non-problematic creates to underlying table"""
return self._get_underlying_object().unlink()

70
partner_relations/model/res_partner_relation_type.py

@ -1,70 +0,0 @@
# -*- coding: utf-8 -*-
'''Define model res.partner.relation.type'''
##############################################################################
#
# OpenERP, Open Source Management Solution
# This module copyright (C) 2013 Therp BV (<http://therp.nl>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
from openerp import models, fields, api, _
class ResPartnerRelationType(models.Model):
"""Model that defines relation types that might exist between partners"""
_name = 'res.partner.relation.type'
_description = 'Partner Relation Type'
_order = 'name'
name = fields.Char(
'Name',
size=128,
required=True,
translate=True,
)
name_inverse = fields.Char(
'Inverse name',
size=128,
required=True,
translate=True,
)
contact_type_left = fields.Selection(
'_get_partner_types',
'Left partner type',
)
contact_type_right = fields.Selection(
'_get_partner_types',
'Right partner type',
)
partner_category_left = fields.Many2one(
'res.partner.category',
'Left partner category',
)
partner_category_right = fields.Many2one(
'res.partner.category',
'Right partner category',
)
allow_self = fields.Boolean(
'Allow both sides to be the same',
default=False,
)
@api.model
def _get_partner_types(self):
return [
('c', _('Company')),
('p', _('Person')),
]

175
partner_relations/model/res_partner_relation_type_selection.py

@ -1,175 +0,0 @@
# -*- coding: utf-8 -*-
'''
Created on 23 may 2014
@author: Ronald Portier, Therp
rportier@therp.nl
http://www.therp.nl
For the model defined here _auto is set to False to prevent creating a
database file. All i/o operations are overridden to use a sql SELECT that
takes data from res_partner_connection_type where each type is included in the
result set twice, so it appears that the connection type and the inverse
type are separate records..
The original function _auto_init is still called because this function
normally (if _auto == True) not only creates the db tables, but it also takes
care of registering all fields in ir_model_fields. This is needed to make
the field labels translatable.
example content for last lines of _statement:
select id, record_type,
customer_id, customer_name, customer_city, customer_zip, customer_street,
caller_id, caller_name, caller_phone, caller_fax, caller_email
from FULL_LIST as ResPartnerRelationTypeSelection where record_type = 'c'
ORDER BY ResPartnerRelationTypeSelection.customer_name asc,
ResPartnerRelationTypeSelection.caller_name asc;
'''
from openerp import api
from openerp.osv import fields
from openerp.osv import orm
from openerp.tools import drop_view_if_exists
from .res_partner_relation_type import ResPartnerRelationType
from .res_partner import PADDING
class ResPartnerRelationTypeSelection(orm.Model):
'''Virtual relation types'''
_RECORD_TYPES = [
('a', 'Type'),
('b', 'Inverse type'),
]
_auto = False # Do not try to create table in _auto_init(..)
_log_access = False
@api.multi
def get_type_from_selection_id(self):
"""Selection id ic computed from id of underlying type and the
kind of record. This function does the inverse computation to give
back the original type id, and about the record type."""
type_id = self.id / PADDING
is_reverse = (self.id % PADDING) > 0
return type_id, is_reverse
def _auto_init(self, cr, context=None):
drop_view_if_exists(cr, self._table)
# TODO: we lose field value's translations here.
# probably we need to patch ir_translation.get_source for that
# to get res_partner_relation_type's translations
cr.execute(
'''create or replace view %(table)s as
select
id * %(padding)d as id,
id as type_id,
cast('a' as char(1)) as record_type,
name as name,
contact_type_left as contact_type_this,
contact_type_right as contact_type_other,
partner_category_left as partner_category_this,
partner_category_right as partner_category_other
from %(underlying_table)s
union select
id * %(padding)d + 1,
id,
cast('b' as char(1)),
name_inverse,
contact_type_right,
contact_type_left,
partner_category_right,
partner_category_left
from %(underlying_table)s''' % {
'table': self._table,
'padding': PADDING,
'underlying_table': 'res_partner_relation_type',
})
return super(ResPartnerRelationTypeSelection, self)._auto_init(
cr, context=context)
def _search_partner_category_this(self, cr, uid, obj, field_name, args,
context=None):
category_ids = []
for arg in args:
if isinstance(arg, tuple) and arg[0] == field_name\
and (arg[1] == '=' or arg[1] == 'in'):
# TODO don't we have an api function to eval that?
for delta in arg[2]:
if delta[0] == 6:
category_ids.extend(delta[2])
if category_ids:
return [
'|',
('partner_category_this', '=', False),
('partner_category_this', 'in', category_ids),
]
else:
return [('partner_category_this', '=', False)]
_name = 'res.partner.relation.type.selection'
_description = 'All relation types'
_foreign_keys = []
_columns = {
'record_type': fields.selection(_RECORD_TYPES, 'Record type', size=16),
'type_id': fields.many2one(
'res.partner.relation.type', 'Type'),
'name': fields.char('Name', size=64),
'contact_type_this': fields.selection(
ResPartnerRelationType._get_partner_types.im_func,
'Current record\'s partner type'),
'contact_type_other': fields.selection(
ResPartnerRelationType._get_partner_types.im_func,
'Other record\'s partner type'),
'partner_category_this': fields.many2one(
'res.partner.category', 'Current record\'s category'),
'partner_category_other': fields.many2one(
'res.partner.category', 'Other record\'s category'),
# search field to handle many2many deltas from the client
'search_partner_category_this': fields.function(
lambda self, cr, uid, ids, context=None: dict(
[(i, False) for i in ids]),
fnct_search=_search_partner_category_this,
type='many2many', obj='res.partner.category',
string='Current record\'s category'),
}
_order = 'name asc'
def name_get(self, cr, uid, ids, context=None):
'translate name using translations from res.partner.relation.type'
result = super(ResPartnerRelationTypeSelection, self).name_get(
cr, uid, ids, context=context)
ir_translation = self.pool['ir.translation']
return [
(i, ir_translation._get_source(
cr, uid,
'res.partner.relation.type,name_inverse'
if self.get_type_from_selection_id(cr, uid, i)[1]
else 'res.partner.relation.type,name',
'model', context.get('lang'), name))
for i, name in result]
def name_search(self, cr, uid, name='', args=None, operator='ilike',
context=None, limit=100):
'search for translated names in res.partner.relation.type'
res_partner_relation_type = self.pool['res.partner.relation.type']
relation_ids = res_partner_relation_type.search(
cr, uid, [('name', operator, name)],
context=context)
inverse_relation_ids = res_partner_relation_type.search(
cr, uid, [('name_inverse', operator, name)],
context=context)
all_ids = self.search(
cr, uid,
[
('id', 'in',
map(lambda x: x * PADDING, relation_ids) +
map(lambda x: x * PADDING + 1, inverse_relation_ids)),
] + (args or []),
context=context, limit=limit)
return self.name_get(cr, uid, all_ids, context=context)

8
partner_relations/models/__init__.py

@ -0,0 +1,8 @@
# -*- coding: utf-8 -*-
# © 2013-2016 Therp BV <http://therp.nl>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from . import res_partner
from . import res_partner_relation
from . import res_partner_relation_type
from . import res_partner_relation_all
from . import res_partner_relation_type_selection

172
partner_relations/models/res_partner.py

@ -0,0 +1,172 @@
# -*- coding: utf-8 -*-
# © 2013-2016 Therp BV <http://therp.nl>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
"""Support connections between partners."""
import numbers
from openerp import _, api, exceptions, fields, models
from openerp.osv.expression import is_leaf, OR, FALSE_LEAF
class ResPartner(models.Model):
"""Extend partner with relations and allow to search for relations
in various ways.
"""
# pylint: disable=invalid-name
# pylint: disable=no-member
_inherit = 'res.partner'
relation_count = fields.Integer(
string='Relation Count',
compute="_compute_relation_count"
)
relation_all_ids = fields.One2many(
comodel_name='res.partner.relation.all',
inverse_name='this_partner_id',
string='All relations with current partner',
auto_join=True,
selectable=False,
copy=False,
)
search_relation_type_id = fields.Many2one(
comodel_name='res.partner.relation.type.selection',
compute=lambda self: None,
search='_search_relation_type_id',
string='Has relation of type',
)
search_relation_partner_id = fields.Many2one(
comodel_name='res.partner',
compute=lambda self: None,
search='_search_related_partner_id',
string='Has relation with',
)
search_relation_date = fields.Date(
compute=lambda self: None,
search='_search_relation_date',
string='Relation valid',
)
search_relation_partner_category_id = fields.Many2one(
comodel_name='res.partner.category',
compute=lambda self: None,
search='_search_related_partner_category_id',
string='Has relation with a partner in category',
)
@api.depends("relation_all_ids")
def _compute_relation_count(self):
"""Count the number of relations this partner has for Smart Button
Don't count inactive relations.
"""
for rec in self:
rec.relation_count = len(rec.relation_all_ids.filtered('active'))
@api.model
def _search_relation_type_id(self, operator, value):
"""Search partners based on their type of relations."""
result = []
SUPPORTED_OPERATORS = (
'=',
'!=',
'like',
'not like',
'ilike',
'not ilike',
'in',
'not in',
)
if operator not in SUPPORTED_OPERATORS:
raise exceptions.ValidationError(
_('Unsupported search operator "%s"') % operator)
type_selection_model = self.env['res.partner.relation.type.selection']
relation_type_selection = []
if operator == '=' and isinstance(value, numbers.Integral):
relation_type_selection += type_selection_model.browse(value)
elif operator == '!=' and isinstance(value, numbers.Integral):
relation_type_selection = type_selection_model.search([
('id', operator, value),
])
else:
relation_type_selection = type_selection_model.search([
'|',
('type_id.name', operator, value),
('type_id.name_inverse', operator, value),
])
if not relation_type_selection:
result = [FALSE_LEAF]
for relation_type in relation_type_selection:
result = OR([
result,
[
('relation_all_ids.type_selection_id.id', '=',
relation_type.id),
],
])
return result
@api.model
def _search_related_partner_id(self, operator, value):
"""Find partner based on relation with other partner."""
# pylint: disable=no-self-use
return [
('relation_all_ids.other_partner_id', operator, value),
]
@api.model
def _search_relation_date(self, operator, value):
"""Look only for relations valid at date of search."""
# pylint: disable=no-self-use
return [
'&',
'|',
('relation_all_ids.date_start', '=', False),
('relation_all_ids.date_start', '<=', value),
'|',
('relation_all_ids.date_end', '=', False),
('relation_all_ids.date_end', '>=', value),
]
@api.model
def _search_related_partner_category_id(self, operator, value):
"""Search for partner related to a partner with search category."""
# pylint: disable=no-self-use
return [
('relation_all_ids.other_partner_id.category_id', operator, value),
]
@api.model
def search(self, args, offset=0, limit=None, order=None, count=False):
"""Inject searching for current relation date if we search for
relation properties and no explicit date was given.
"""
# pylint: disable=arguments-differ
# pylint: disable=no-value-for-parameter
date_args = []
for arg in args:
if is_leaf(arg) and arg[0].startswith('search_relation'):
if arg[0] == 'search_relation_date':
date_args = []
break
if not date_args:
date_args = [
('search_relation_date', '=', fields.Date.today()),
]
# because of auto_join, we have to do the active test by hand
active_args = []
if self.env.context.get('active_test', True):
for arg in args:
if is_leaf(arg) and arg[0].startswith('search_relation'):
active_args = [('relation_all_ids.active', '=', True)]
break
return super(ResPartner, self).search(
args + date_args + active_args, offset=offset, limit=limit,
order=order, count=count)
@api.multi
def get_partner_type(self):
"""Get partner type for relation.
:return: 'c' for company or 'p' for person
:rtype: str
"""
self.ensure_one()
return 'c' if self.is_company else 'p'

156
partner_relations/models/res_partner_relation.py

@ -0,0 +1,156 @@
# -*- coding: utf-8 -*-
# © 2013-2016 Therp BV <http://therp.nl>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
"""Store relations (connections) between partners."""
from openerp import _, api, fields, models
from openerp.exceptions import ValidationError
class ResPartnerRelation(models.Model):
"""Model res.partner.relation is used to describe all links or relations
between partners in the database.
This model is actually only used to store the data. The model
res.partner.relation.all, based on a view that contains each record
two times, once for the normal relation, once for the inverse relation,
will be used to maintain the data.
"""
_name = 'res.partner.relation'
_description = 'Partner relation'
left_partner_id = fields.Many2one(
comodel_name='res.partner',
string='Source Partner',
required=True,
auto_join=True,
ondelete='cascade',
)
right_partner_id = fields.Many2one(
comodel_name='res.partner',
string='Destination Partner',
required=True,
auto_join=True,
ondelete='cascade',
)
type_id = fields.Many2one(
comodel_name='res.partner.relation.type',
string='Type',
required=True,
auto_join=True,
)
date_start = fields.Date('Starting date')
date_end = fields.Date('Ending date')
@api.model
def create(self, vals):
"""Override create to correct values, before being stored."""
context = self.env.context
if 'left_partner_id' not in vals and context.get('active_id'):
vals['left_partner_id'] = context.get('active_id')
return super(ResPartnerRelation, self).create(vals)
@api.one
@api.constrains('date_start', 'date_end')
def _check_dates(self):
"""End date should not be before start date, if not filled
:raises ValidationError: When constraint is violated
"""
if (self.date_start and self.date_end and
self.date_start > self.date_end):
raise ValidationError(
_('The starting date cannot be after the ending date.')
)
@api.one
@api.constrains('left_partner_id', 'type_id')
def _check_partner_left(self):
"""Check left partner for required company or person
:raises ValidationError: When constraint is violated
"""
self._check_partner("left")
@api.one
@api.constrains('right_partner_id', 'type_id')
def _check_partner_right(self):
"""Check right partner for required company or person
:raises ValidationError: When constraint is violated
"""
self._check_partner("right")
@api.one
def _check_partner(self, side):
"""Check partner for required company or person, and for category
:param str side: left or right
:raises ValidationError: When constraint is violated
"""
assert side in ['left', 'right']
ptype = getattr(self.type_id, "contact_type_%s" % side)
partner = getattr(self, '%s_partner_id' % side)
if ((ptype == 'c' and not partner.is_company) or
(ptype == 'p' and partner.is_company)):
raise ValidationError(
_('The %s partner is not applicable for this relation type.') %
side
)
category = getattr(self.type_id, "partner_category_%s" % side)
if category and category.id not in partner.category_id.ids:
raise ValidationError(
_('The %s partner does not have category %s.') %
(side, category.name)
)
@api.one
@api.constrains('left_partner_id', 'right_partner_id')
def _check_not_with_self(self):
"""Not allowed to link partner to same partner
:raises ValidationError: When constraint is violated
"""
if self.left_partner_id == self.right_partner_id:
if not (self.type_id and self.type_id.allow_self):
raise ValidationError(
_('Partners cannot have a relation with themselves.')
)
@api.one
@api.constrains(
'left_partner_id',
'type_id',
'right_partner_id',
'date_start',
'date_end',
)
def _check_relation_uniqueness(self):
"""Forbid multiple active relations of the same type between the same
partners
:raises ValidationError: When constraint is violated
"""
# pylint: disable=no-member
# pylint: disable=no-value-for-parameter
domain = [
('type_id', '=', self.type_id.id),
('id', '!=', self.id),
('left_partner_id', '=', self.left_partner_id.id),
('right_partner_id', '=', self.right_partner_id.id),
]
if self.date_start:
domain += [
'|',
('date_end', '=', False),
('date_end', '>=', self.date_start),
]
if self.date_end:
domain += [
'|',
('date_start', '=', False),
('date_start', '<=', self.date_end),
]
if self.search(domain):
raise ValidationError(
_('There is already a similar relation with overlapping dates')
)

347
partner_relations/models/res_partner_relation_all.py

@ -0,0 +1,347 @@
# -*- coding: utf-8 -*-
# © 2014-2016 Therp BV <http://therp.nl>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
"""Abstract model to show each relation from two sides."""
from psycopg2.extensions import AsIs
from openerp import _, api, fields, models
from openerp.tools import drop_view_if_exists
PADDING = 10
_RECORD_TYPES = [
('a', 'Left partner to right partner'),
('b', 'Right partner to left partner'),
]
class ResPartnerRelationAll(models.AbstractModel):
"""Abstract model to show each relation from two sides."""
_auto = False
_log_access = False
_name = 'res.partner.relation.all'
_description = 'All (non-inverse + inverse) relations between partners'
_order = (
'this_partner_id, type_selection_id,'
'date_end desc, date_start desc'
)
_overlays = 'res.partner.relation'
_additional_view_fields = []
"""append to this list if you added fields to res_partner_relation that
you need in this model and related fields are not adequate (ie for sorting)
You must use the same name as in res_partner_relation.
Don't overwrite this list in your declaration but append in _auto_init:
def _auto_init(self, cr, context=None):
self._additional_view_fields.append('my_field')
return super(ResPartnerRelationAll, self)._auto_init(
cr, context=context)
my_field = fields...
"""
this_partner_id = fields.Many2one(
comodel_name='res.partner',
string='One Partner',
required=True,
)
other_partner_id = fields.Many2one(
comodel_name='res.partner',
string='Other Partner',
required=True,
)
type_selection_id = fields.Many2one(
comodel_name='res.partner.relation.type.selection',
string='Relation Type',
required=True,
)
relation_id = fields.Many2one(
comodel_name='res.partner.relation',
string='Relation',
readonly=True,
)
record_type = fields.Selection(
selection=_RECORD_TYPES,
string='Record Type',
readonly=True,
)
date_start = fields.Date('Starting date')
date_end = fields.Date('Ending date')
active = fields.Boolean(
string='Active',
help="Records with date_end in the past are inactive",
)
any_partner_id = fields.Many2many(
comodel_name='res.partner',
string='Partner',
compute=lambda self: None,
search='_search_any_partner_id'
)
def _auto_init(self, cr, context=None):
drop_view_if_exists(cr, self._table)
additional_view_fields = ','.join(self._additional_view_fields)
additional_view_fields = (',' + additional_view_fields)\
if additional_view_fields else ''
cr.execute(
"""\
CREATE OR REPLACE VIEW %(table)s AS
SELECT
rel.id * %(padding)s AS id,
rel.id AS relation_id,
cast('a' AS CHAR(1)) AS record_type,
rel.left_partner_id AS this_partner_id,
rel.right_partner_id AS other_partner_id,
rel.date_start,
rel.date_end,
(rel.date_end IS NULL OR rel.date_end >= current_date) AS active,
rel.type_id * %(padding)s AS type_selection_id
%(additional_view_fields)s
FROM res_partner_relation rel
UNION SELECT
rel.id * %(padding)s + 1,
rel.id,
CAST('b' AS CHAR(1)),
rel.right_partner_id,
rel.left_partner_id,
rel.date_start,
rel.date_end,
rel.date_end IS NULL OR rel.date_end >= current_date,
CASE
WHEN typ.is_symmetric THEN rel.type_id * %(padding)s
ELSE rel.type_id * %(padding)s + 1
END
%(additional_view_fields)s
FROM res_partner_relation rel
JOIN res_partner_relation_type typ ON (rel.type_id = typ.id)
""",
{
'table': AsIs(self._table),
'padding': PADDING,
'additional_view_fields': AsIs(additional_view_fields),
}
)
return super(ResPartnerRelationAll, self)._auto_init(
cr, context=context
)
@api.model
def _search_any_partner_id(self, operator, value):
"""Search relation with partner, no matter on which side."""
# pylint: disable=no-self-use
return [
'|',
('this_partner_id', operator, value),
('other_partner_id', operator, value),
]
@api.multi
def name_get(self):
return {
this.id: '%s %s %s' % (
this.this_partner_id.name,
this.type_selection_id.display_name,
this.other_partner_id.name,
)
for this in self
}
@api.onchange('type_selection_id')
def onchange_type_selection_id(self):
"""Add domain on partners according to category and contact_type."""
def check_partner_domain(partner, partner_domain, side):
"""Check wether partner_domain results in empty selection
for partner, or wrong selection of partner already selected.
"""
warning = {}
if partner:
test_domain = [('id', '=', partner.id)] + partner_domain
else:
test_domain = partner_domain
partner_model = self.env['res.partner']
partners_found = partner_model.search(test_domain, limit=1)
if not partners_found:
warning['title'] = _('Error!')
if partner:
warning['message'] = (
_('%s partner incompatible with relation type.') %
side.title()
)
else:
warning['message'] = (
_('No %s partner available for relation type.') %
side
)
return warning
this_partner_domain = []
other_partner_domain = []
if self.type_selection_id.contact_type_this:
this_partner_domain.append((
'is_company', '=',
self.type_selection_id.contact_type_this == 'c'
))
if self.type_selection_id.partner_category_this:
this_partner_domain.append((
'category_id', 'in',
self.type_selection_id.partner_category_this.ids
))
if self.type_selection_id.contact_type_other:
other_partner_domain.append((
'is_company', '=',
self.type_selection_id.contact_type_other == 'c'
))
if self.type_selection_id.partner_category_other:
other_partner_domain.append((
'category_id', 'in',
self.type_selection_id.partner_category_other.ids
))
result = {'domain': {
'this_partner_id': this_partner_domain,
'other_partner_id': other_partner_domain,
}}
# Check wether domain results in no choice or wrong choice of partners:
warning = {}
if this_partner_domain:
warning = check_partner_domain(
self.this_partner_id, this_partner_domain, _('this')
)
if not warning and other_partner_domain:
warning = check_partner_domain(
self.other_partner_id, other_partner_domain, _('other')
)
if warning:
result['warning'] = warning
return result
@api.onchange(
'this_partner_id',
'other_partner_id',
)
def onchange_partner_id(self):
"""Set domain on type_selection_id based on partner(s) selected."""
def check_type_selection_domain(type_selection_domain):
"""If type_selection_id already selected, check wether it
is compatible with the computed type_selection_domain. An empty
selection can practically only occur in a practically empty
database, and will not lead to problems. Therefore not tested.
"""
warning = {}
if not (type_selection_domain and self.type_selection_id):
return warning
test_domain = (
[('id', '=', self.type_selection_id.id)] +
type_selection_domain
)
type_model = self.env['res.partner.relation.type.selection']
types_found = type_model.search(test_domain, limit=1)
if not types_found:
warning['title'] = _('Error!')
warning['message'] = _(
'Relation type incompatible with selected partner(s).'
)
return warning
type_selection_domain = []
if self.this_partner_id:
type_selection_domain += [
'|',
('contact_type_this', '=', False),
('contact_type_this', '=',
self.this_partner_id.get_partner_type()),
'|',
('partner_category_this', '=', False),
('partner_category_this', 'in',
self.this_partner_id.category_id.ids),
]
if self.other_partner_id:
type_selection_domain += [
'|',
('contact_type_other', '=', False),
('contact_type_other', '=',
self.other_partner_id.get_partner_type()),
'|',
('partner_category_other', '=', False),
('partner_category_other', 'in',
self.other_partner_id.category_id.ids),
]
result = {'domain': {
'type_selection_id': type_selection_domain,
}}
# Check wether domain results in no choice or wrong choice for
# type_selection_id:
warning = check_type_selection_domain(type_selection_domain)
if warning:
result['warning'] = warning
return result
@api.model
def _correct_vals(self, vals):
"""Fill left and right partner from this and other partner."""
vals = vals.copy()
if 'this_partner_id' in vals:
vals['left_partner_id'] = vals['this_partner_id']
del vals['this_partner_id']
if 'other_partner_id' in vals:
vals['right_partner_id'] = vals['other_partner_id']
del vals['other_partner_id']
if 'type_selection_id' not in vals:
return vals
selection = self.type_selection_id.browse(vals['type_selection_id'])
type_id = selection.type_id.id
is_inverse = selection.is_inverse
vals['type_id'] = type_id
del vals['type_selection_id']
# Need to switch right and left partner if we are in reverse id:
if 'left_partner_id' in vals or 'right_partner_id' in vals:
if is_inverse:
left_partner_id = False
right_partner_id = False
if 'left_partner_id' in vals:
right_partner_id = vals['left_partner_id']
del vals['left_partner_id']
if 'right_partner_id' in vals:
left_partner_id = vals['right_partner_id']
del vals['right_partner_id']
if left_partner_id:
vals['left_partner_id'] = left_partner_id
if right_partner_id:
vals['right_partner_id'] = right_partner_id
return vals
@api.multi
def write(self, vals):
"""divert non-problematic writes to underlying table"""
vals = self._correct_vals(vals)
for rec in self:
rec.relation_id.write(vals)
return True
@api.model
def create(self, vals):
"""Divert non-problematic creates to underlying table.
Create a res.partner.relation but return the converted id.
"""
is_inverse = False
if 'type_selection_id' in vals:
selection = self.type_selection_id.browse(
vals['type_selection_id']
)
is_inverse = selection.is_inverse
vals = self._correct_vals(vals)
res = self.relation_id.create(vals)
return_id = res.id * PADDING + (is_inverse and 1 or 0)
return self.browse(return_id)
@api.multi
def unlink(self):
"""divert non-problematic creates to underlying table"""
# pylint: disable=arguments-differ
for rec in self:
rec.relation_id.unlink()
return True

176
partner_relations/models/res_partner_relation_type.py

@ -0,0 +1,176 @@
# -*- coding: utf-8 -*-
# © 2013-2016 Therp BV <http://therp.nl>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
"""Define the type of relations that can exist between partners."""
from openerp import _, api, fields, models
from openerp.exceptions import ValidationError
from openerp.osv.expression import AND, OR
HANDLE_INVALID_ONCHANGE = [
('restrict',
_('Do not allow change that will result in invalid relations')),
('ignore',
_('Allow existing relations that do not fit changed conditions')),
('end',
_('End relations per today, if they do not fit changed conditions')),
('delete',
_('Delete relations that do not fit changed conditions')),
]
class ResPartnerRelationType(models.Model):
"""Model that defines relation types that might exist between partners"""
_name = 'res.partner.relation.type'
_description = 'Partner Relation Type'
_order = 'name'
name = fields.Char(
string='Name',
required=True,
translate=True,
)
name_inverse = fields.Char(
string='Inverse name',
required=True,
translate=True,
)
contact_type_left = fields.Selection(
selection='get_partner_types',
string='Left partner type',
)
contact_type_right = fields.Selection(
selection='get_partner_types',
string='Right partner type',
)
partner_category_left = fields.Many2one(
comodel_name='res.partner.category',
string='Left partner category',
)
partner_category_right = fields.Many2one(
comodel_name='res.partner.category',
string='Right partner category',
)
allow_self = fields.Boolean(
string='Reflexive',
help='This relation can be set up with the same partner left and '
'right',
default=False,
)
is_symmetric = fields.Boolean(
string='Symmetric',
old_name='symmetric',
help="This relation is the same from right to left as from left to"
" right",
default=False,
)
handle_invalid_onchange = fields.Selection(
selection=HANDLE_INVALID_ONCHANGE,
string='Invalid relation handling',
required=True,
default='restrict',
help="When adding relations criteria like partner type and category"
" are checked.\n"
"However when you change the criteria, there might be relations"
" that do not fit the new criteria.\n"
"Specify how this situation should be handled.",
)
@api.model
def get_partner_types(self):
"""A partner can be an organisation or an individual."""
# pylint: disable=no-self-use
return [
('c', _('Organisation')),
('p', _('Person')),
]
@api.onchange('is_symmetric')
def onchange_is_symmetric(self):
"""Set right side to left side if symmetric."""
if self.is_symmetric:
self.update({
'name_inverse': self.name,
'contact_type_right': self.contact_type_left,
'partner_category_right': self.partner_category_left,
})
@api.multi
def check_existing(self, vals):
"""Check wether records exist that do not fit new criteria."""
relation_model = self.env['res.partner.relation']
def get_type_condition(vals, side):
"""Add if needed check for contact type."""
fieldname1 = 'contact_type_%s' % side
fieldname2 = '%s_partner_id.is_company' % side
contact_type = fieldname1 in vals and vals[fieldname1] or False
if contact_type == 'c':
# Records that are not companies are invalid:
return [(fieldname2, '=', False)]
if contact_type == 'p':
# Records that are companies are invalid:
return [(fieldname2, '=', True)]
return []
def get_category_condition(vals, side):
"""Add if needed check for partner category."""
fieldname1 = 'partner_category_%s' % side
fieldname2 = '%s_partner_id.category_id' % side
category_id = fieldname1 in vals and vals[fieldname1] or False
if category_id:
# Records that do not have the specified category are invalid:
return [(fieldname2, 'not in', [category_id])]
return []
for rec in self:
handling = (
'handle_invalid_onchange' in vals and
vals['handle_invalid_onchange'] or
self.handle_invalid_onchange
)
if handling == 'ignore':
continue
invalid_conditions = []
for side in ['left', 'right']:
invalid_conditions = OR([
invalid_conditions,
get_type_condition(vals, side),
])
invalid_conditions = OR([
invalid_conditions,
get_category_condition(vals, side),
])
if not invalid_conditions:
return
# only look at relations for this type
invalid_domain = AND([
[('type_id', '=', rec.id)], invalid_conditions
])
invalid_relations = relation_model.with_context(
active_test=False
).search(invalid_domain)
if invalid_relations:
if handling == 'restrict':
raise ValidationError(
_('There are already relations not satisfying the'
' conditions for partner type or category.')
)
elif handling == 'delete':
invalid_relations.unlink()
else:
# Delete future records, end other ones, ignore relations
# already ended:
cutoff_date = fields.Date.today()
for relation in invalid_relations:
if relation.date_start >= cutoff_date:
relation.unlink()
elif (not relation.date_end or
relation.date_end > cutoff_date):
relation.write({'date_end': cutoff_date})
@api.multi
def write(self, vals):
"""Handle existing relations if conditions change."""
self.check_existing(vals)
return super(ResPartnerRelationType, self).write(vals)

127
partner_relations/models/res_partner_relation_type_selection.py

@ -0,0 +1,127 @@
# -*- coding: utf-8 -*-
# © 2014-2016 Therp BV <http://therp.nl>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
"""
For the model defined here _auto is set to False to prevent creating a
database file. The model is based on a SQL view based on
res_partner_relation_type where each type is included in the
result set twice, so it appears that the connection type and the inverse
type are separate records..
The original function _auto_init is still called because this function
normally (if _auto == True) not only creates the db tables, but it also takes
care of registering all fields in ir_model_fields. This is needed to make
the field labels translatable.
"""
from psycopg2.extensions import AsIs
from openerp import api, fields, models
from openerp.tools import drop_view_if_exists
from .res_partner_relation_type import ResPartnerRelationType
PADDING = 10
class ResPartnerRelationTypeSelection(models.Model):
"""Virtual relation types"""
_name = 'res.partner.relation.type.selection'
_description = 'All relation types'
_auto = False # Do not try to create table in _auto_init(..)
_foreign_keys = []
_log_access = False
_order = 'name asc'
type_id = fields.Many2one(
comodel_name='res.partner.relation.type',
string='Type',
)
name = fields.Char('Name')
contact_type_this = fields.Selection(
selection=ResPartnerRelationType.get_partner_types.im_func,
string='Current record\'s partner type',
)
is_inverse = fields.Boolean(
string="Is reverse type?",
help="Inverse relations are from right to left partner.",
)
contact_type_other = fields.Selection(
selection=ResPartnerRelationType.get_partner_types.im_func,
string='Other record\'s partner type',
)
partner_category_this = fields.Many2one(
comodel_name='res.partner.category',
string='Current record\'s category',
)
partner_category_other = fields.Many2one(
comodel_name='res.partner.category',
string='Other record\'s category',
)
allow_self = fields.Boolean(
string='Reflexive',
)
is_symmetric = fields.Boolean(
string='Symmetric',
)
def _auto_init(self, cr, context=None):
drop_view_if_exists(cr, self._table)
cr.execute(
"""CREATE OR REPLACE VIEW %(table)s AS
SELECT
id * %(padding)s AS id,
id AS type_id,
name AS name,
False AS is_inverse,
contact_type_left AS contact_type_this,
contact_type_right AS contact_type_other,
partner_category_left AS partner_category_this,
partner_category_right AS partner_category_other,
allow_self,
is_symmetric
FROM %(underlying_table)s
UNION SELECT
id * %(padding)s + 1,
id,
name_inverse,
True,
contact_type_right,
contact_type_left,
partner_category_right,
partner_category_left,
allow_self,
is_symmetric
FROM %(underlying_table)s
WHERE not is_symmetric
""",
{
'table': AsIs(self._table),
'padding': PADDING,
'underlying_table': AsIs('res_partner_relation_type'),
})
return super(ResPartnerRelationTypeSelection, self)._auto_init(
cr, context=context)
@api.multi
def name_get(self):
"""Get name or name_inverse from underlying model."""
return [
(this.id,
this.is_inverse and this.type_id.name_inverse or
this.type_id.display_name)
for this in self
]
@api.model
def name_search(self, name='', args=None, operator='ilike', limit=100):
"""Search for name or inverse name in underlying model."""
# pylint: disable=no-value-for-parameter
return self.search(
[
'|',
('type_id.name', operator, name),
('type_id.name_inverse', operator, name),
] + (args or []),
limit=limit
).name_get()

22
partner_relations/test/test_allow.yml

@ -1,22 +0,0 @@
-
I create a relation allowing the same partner at both ends.
-
!record {model: res.partner.relation.type, id: partner_relations.allow_self}:
name: 'Relation Allow'
name_inverse: 'Inverse Relation Allow'
contact_type_right: 'p'
contact_type_left: 'p'
allow_self: True
-
I create a partner U for testing purposes
-
!record {model: res.partner, id: partner_relations.test_U}:
name: 'unittests.U'
image: ''
-
I create relation instance U -- (allow) --> U
-
!record {model: res.partner.relation, id: partner_relations.test_allow}:
left_partner_id: partner_relations.test_U
right_partner_id: partner_relations.test_U
type_id: partner_relations.allow_self

8
partner_relations/tests/__init__.py

@ -1 +1,7 @@
from . import test_partner_relations
# -*- coding: utf-8 -*-
# Copyright 2016 Therp BV
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from . import test_partner_relation_common
from . import test_partner_relation
from . import test_partner_relation_all
from . import test_partner_search

280
partner_relations/tests/test_partner_relation.py

@ -0,0 +1,280 @@
# -*- coding: utf-8 -*-
# Copyright 2016 Therp BV
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from datetime import date
from dateutil.relativedelta import relativedelta
from openerp import fields
from openerp.exceptions import ValidationError
from .test_partner_relation_common import TestPartnerRelationCommon
class TestPartnerRelation(TestPartnerRelationCommon):
def test_selection_name_search(self):
"""Test wether we can find type selection on reverse name."""
selection_types = self.selection_model.name_search(
name=self.selection_person2company.name
)
self.assertTrue(selection_types)
self.assertTrue(
(self.selection_person2company.id,
self.selection_person2company.name) in selection_types
)
def test_self_allowed(self):
"""Test creation of relation to same partner when type allows."""
type_allow = self.type_model.create({
'name': 'allow',
'name_inverse': 'allow_inverse',
'contact_type_left': 'p',
'contact_type_right': 'p',
'allow_self': True
})
self.assertTrue(type_allow)
reflexive_relation = self.relation_model.create({
'type_id': type_allow.id,
'left_partner_id': self.partner_01_person.id,
'right_partner_id': self.partner_01_person.id,
})
self.assertTrue(reflexive_relation)
def test_self_disallowed(self):
"""Test creating relation to same partner when disallowed.
Attempt to create a relation of a partner to the same partner should
raise an error when the type of relation explicitly disallows this.
"""
type_disallow = self.type_model.create({
'name': 'disallow',
'name_inverse': 'disallow_inverse',
'contact_type_left': 'p',
'contact_type_right': 'p',
'allow_self': False
})
self.assertTrue(type_disallow)
with self.assertRaises(ValidationError):
self.relation_model.create({
'type_id': type_disallow.id,
'left_partner_id': self.partner_01_person.id,
'right_partner_id': self.partner_01_person.id,
})
def test_self_default(self):
"""Test default not to allow relation with same partner.
Attempt to create a relation of a partner to the same partner
raise an error when the type of relation does not explicitly allow
this.
"""
type_default = self.type_model.create({
'name': 'default',
'name_inverse': 'default_inverse',
'contact_type_left': 'p',
'contact_type_right': 'p',
})
self.assertTrue(type_default)
with self.assertRaises(ValidationError):
self.relation_model.create({
'type_id': type_default.id,
'left_partner_id': self.partner_01_person.id,
'right_partner_id': self.partner_01_person.id,
})
def test_self_mixed(self):
"""Test creation of relation with wrong types.
Trying to create a relation between partners with an inappropiate
type should raise an error.
"""
with self.assertRaises(ValidationError):
self.relation_model.create({
'type_id': self.type_company2person.id,
'left_partner_id': self.partner_01_person.id,
'right_partner_id': self.partner_02_company.id,
})
def test_symmetric(self):
"""Test creating symmetric relation."""
# Start out with non symmetric relation:
type_symmetric = self.type_model.create({
'name': 'not yet symmetric',
'name_inverse': 'the other side of not symmetric',
'is_symmetric': False,
'contact_type_left': False,
'contact_type_right': 'p',
})
# not yet symmetric relation should result in two records in
# selection:
selection_symmetric = self.selection_model.search([
('type_id', '=', type_symmetric.id),
])
self.assertEqual(len(selection_symmetric), 2)
# Now change to symmetric and test name and inverse name:
with self.env.do_in_draft():
type_symmetric.write(
vals={
'name': 'sym',
'is_symmetric': True,
}
)
with self.env.do_in_onchange():
type_symmetric.onchange_is_symmetric()
self.assertEqual(type_symmetric.is_symmetric, True)
self.assertEqual(
type_symmetric.name_inverse,
type_symmetric.name
)
self.assertEqual(
type_symmetric.contact_type_right,
type_symmetric.contact_type_left
)
# now update the database:
type_symmetric.write(
vals={
'name': type_symmetric.name,
'is_symmetric': type_symmetric.is_symmetric,
'name_inverse': type_symmetric.name_inverse,
'contact_type_right': type_symmetric.contact_type_right,
}
)
# symmetric relation should result in only one record in
# selection:
selection_symmetric = self.selection_model.search([
('type_id', '=', type_symmetric.id),
])
self.assertEqual(len(selection_symmetric), 1)
relation = self.relation_all_model.create({
'type_selection_id': selection_symmetric.id,
'this_partner_id': self.partner_02_company.id,
'other_partner_id': self.partner_01_person.id,
})
partners = self.partner_model.search([
('search_relation_type_id', '=', relation.type_selection_id.id)
])
self.assertTrue(self.partner_01_person in partners)
self.assertTrue(self.partner_02_company in partners)
def test_category_domain(self):
"""Test check on category in relations."""
# Check on left side:
with self.assertRaises(ValidationError):
self.relation_model.create({
'type_id': self.type_ngo2volunteer.id,
'left_partner_id': self.partner_02_company.id,
'right_partner_id': self.partner_04_volunteer.id,
})
# Check on right side:
with self.assertRaises(ValidationError):
self.relation_model.create({
'type_id': self.type_ngo2volunteer.id,
'left_partner_id': self.partner_03_ngo.id,
'right_partner_id': self.partner_01_person.id,
})
def test_relation_type_change(self):
"""Test change in relation type conditions."""
# First create a relation type having no particular conditions.
(type_school2student,
school2student,
school2student_inverse) = (
self._create_relation_type_selection({
'name': 'school has student',
'name_inverse': 'studies at school',
})
)
# Second create relations based on those conditions.
partner_school = self.partner_model.create({
'name': 'Test School',
'is_company': True,
'ref': 'TS',
})
partner_bart = self.partner_model.create({
'name': 'Bart Simpson',
'is_company': False,
'ref': 'BS',
})
partner_lisa = self.partner_model.create({
'name': 'Lisa Simpson',
'is_company': False,
'ref': 'LS',
})
relation_school2bart = self.relation_all_model.create({
'this_partner_id': partner_school.id,
'type_selection_id': school2student.id,
'other_partner_id': partner_bart.id,
})
self.assertTrue(relation_school2bart)
relation_school2lisa = self.relation_all_model.create({
'this_partner_id': partner_school.id,
'type_selection_id': school2student.id,
'other_partner_id': partner_lisa.id,
})
self.assertTrue(relation_school2lisa)
relation_bart2lisa = self.relation_all_model.create({
'this_partner_id': partner_bart.id,
'type_selection_id': school2student.id,
'other_partner_id': partner_lisa.id,
})
self.assertTrue(relation_bart2lisa)
# Third creata a category and make it a condition for the
# relation type.
# - Test restriction
# - Test ignore
category_student = self.category_model.create({
'name': 'Student',
})
with self.assertRaises(ValidationError):
type_school2student.write({
'partner_category_right': category_student.id,
})
self.assertFalse(type_school2student.partner_category_right.id)
type_school2student.write({
'handle_invalid_onchange': 'ignore',
'partner_category_right': category_student.id,
})
self.assertEqual(
type_school2student.partner_category_right.id,
category_student.id
)
# Fourth make company type a condition for left partner
# - Test ending
# - Test deletion
partner_bart.write({
'category_id': [(4, category_student.id)],
})
partner_lisa.write({
'category_id': [(4, category_student.id)],
})
# Future student to be deleted by end action:
partner_homer = self.partner_model.create({
'name': 'Homer Simpson',
'is_company': False,
'ref': 'HS',
'category_id': [(4, category_student.id)],
})
relation_lisa2homer = self.relation_all_model.create({
'this_partner_id': partner_lisa.id,
'type_selection_id': school2student.id,
'other_partner_id': partner_homer.id,
'date_start': fields.Date.to_string(
date.today() + relativedelta(months=+6)
),
})
self.assertTrue(relation_lisa2homer)
type_school2student.write({
'handle_invalid_onchange': 'end',
'contact_type_left': 'c',
})
self.assertEqual(
relation_bart2lisa.date_end,
fields.Date.today()
)
self.assertFalse(relation_lisa2homer.exists())
type_school2student.write({
'handle_invalid_onchange': 'delete',
'contact_type_left': 'c',
'contact_type_right': 'p',
})
self.assertFalse(relation_bart2lisa.exists())

241
partner_relations/tests/test_partner_relation_all.py

@ -0,0 +1,241 @@
# -*- coding: utf-8 -*-
# Copyright 2016 Therp BV
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from openerp.exceptions import ValidationError
from .test_partner_relation_common import TestPartnerRelationCommon
class TestPartnerRelation(TestPartnerRelationCommon):
def setUp(self):
super(TestPartnerRelation, self).setUp()
# Create a new relation type which will not have valid relations:
category_nobody = self.category_model.create({
'name': 'Nobody',
})
(self.type_nobody,
self.selection_nobody,
self.selection_nobody_inverse) = (
self._create_relation_type_selection({
'name': 'has relation with nobody',
'name_inverse': 'nobody has relation with',
'contact_type_left': 'c',
'contact_type_right': 'p',
'partner_category_left': category_nobody.id,
'partner_category_right': category_nobody.id,
})
)
def _get_empty_relation(self):
"""Get empty relation record for onchange tests."""
# Need English, because we will compare text
return self.relation_all_model.with_context(lang='en_US').new({})
def test_create_with_active_id(self):
"""Test creation with this_partner_id from active_id."""
# Check wether we can create connection from company to person,
# taking the particular company from the active records:
relation = self.relation_all_model.with_context(
active_id=self.partner_02_company.id,
active_ids=self.partner_02_company.ids,
).create({
'other_partner_id': self.partner_01_person.id,
'type_selection_id': self.selection_company2person.id,
})
self.assertTrue(relation)
self.assertEqual(relation.this_partner_id, self.partner_02_company)
# Partner should have one relation now:
self.assertEqual(self.partner_01_person.relation_count, 1)
def test_display_name(self):
"""Test display name"""
relation = self._create_company2person_relation()
self.assertEqual(
relation.display_name, '%s %s %s' % (
relation.this_partner_id.name,
relation.type_selection_id.name,
relation.other_partner_id.name,
)
)
def test__regular_write(self):
"""Test write with valid data."""
relation = self._create_company2person_relation()
relation.write({
'date_start': '2014-09-01',
})
relation.invalidate_cache(ids=relation.ids)
self.assertEqual(relation.date_start, '2014-09-01')
def test_write_incompatible_dates(self):
"""Test write with date_end before date_start."""
relation = self._create_company2person_relation()
with self.assertRaises(ValidationError):
relation.write({
'date_start': '2016-09-01',
'date_end': '2016-08-01',
})
def test_validate_overlapping_01(self):
"""Test create overlapping with no start / end dates."""
relation = self._create_company2person_relation()
with self.assertRaises(ValidationError):
# New relation with no start / end should give error
self.relation_all_model.create({
'this_partner_id': relation.this_partner_id.id,
'type_selection_id': relation.type_selection_id.id,
'other_partner_id': relation.other_partner_id.id,
})
def test_validate_overlapping_02(self):
"""Test create overlapping with start / end dates."""
relation = self.relation_all_model.create({
'this_partner_id': self.partner_02_company.id,
'type_selection_id': self.selection_company2person.id,
'other_partner_id': self.partner_01_person.id,
'date_start': '2015-09-01',
'date_end': '2016-08-31',
})
# New relation with overlapping start / end should give error
with self.assertRaises(ValidationError):
self.relation_all_model.create({
'this_partner_id': relation.this_partner_id.id,
'type_selection_id': relation.type_selection_id.id,
'other_partner_id': relation.other_partner_id.id,
'date_start': '2016-08-01',
'date_end': '2017-07-30',
})
def test_validate_overlapping_03(self):
"""Test create not overlapping."""
relation = self.relation_all_model.create({
'this_partner_id': self.partner_02_company.id,
'type_selection_id': self.selection_company2person.id,
'other_partner_id': self.partner_01_person.id,
'date_start': '2015-09-01',
'date_end': '2016-08-31',
})
relation_another_record = self.relation_all_model.create({
'this_partner_id': relation.this_partner_id.id,
'type_selection_id': relation.type_selection_id.id,
'other_partner_id': relation.other_partner_id.id,
'date_start': '2016-09-01',
'date_end': '2017-08-31',
})
self.assertTrue(relation_another_record)
def test_inverse_record(self):
"""Test creation of inverse record."""
relation = self._create_company2person_relation()
inverse_relation = self.relation_all_model.search([
('this_partner_id', '=', relation.other_partner_id.id),
('other_partner_id', '=', relation.this_partner_id.id),
])
self.assertEqual(len(inverse_relation), 1)
self.assertEqual(
inverse_relation.type_selection_id.name,
self.selection_person2company.name
)
def test_inverse_creation(self):
"""Test creation of record through inverse selection."""
relation = self.relation_all_model.create({
'this_partner_id': self.partner_01_person.id,
'type_selection_id': self.selection_person2company.id,
'other_partner_id': self.partner_02_company.id,
})
# Check wether display name is what we should expect:
self.assertEqual(
relation.display_name, '%s %s %s' % (
self.partner_01_person.name,
self.selection_person2company.name,
self.partner_02_company.name,
)
)
def test_unlink(self):
"""Unlinking derived relation should unlink base relation."""
# Check wether underlying record is removed when record is removed:
relation = self._create_company2person_relation()
base_relation = relation.relation_id
relation.unlink()
self.assertFalse(base_relation.exists())
def test_on_change_type_selection(self):
"""Test on_change_type_selection."""
# 1. Test call with empty relation
relation_empty = self._get_empty_relation()
result = relation_empty.onchange_type_selection_id()
self.assertTrue('domain' in result)
self.assertFalse('warning' in result)
self.assertTrue('this_partner_id' in result['domain'])
self.assertFalse(result['domain']['this_partner_id'])
self.assertTrue('other_partner_id' in result['domain'])
self.assertFalse(result['domain']['other_partner_id'])
# 2. Test call with company 2 person relation
relation = self._create_company2person_relation()
domain = relation.onchange_type_selection_id()['domain']
self.assertTrue(
('is_company', '=', False) in domain['other_partner_id']
)
# 3. Test with relation needing categories.
relation_ngo_volunteer = self.relation_all_model.create({
'this_partner_id': self.partner_03_ngo.id,
'type_selection_id': self.selection_ngo2volunteer.id,
'other_partner_id': self.partner_04_volunteer.id,
})
domain = relation_ngo_volunteer.onchange_type_selection_id()['domain']
self.assertTrue(
('category_id', 'in', [self.category_01_ngo.id]) in
domain['this_partner_id']
)
self.assertTrue(
('category_id', 'in', [self.category_02_volunteer.id]) in
domain['other_partner_id']
)
# 4. Test with invalid or impossible combinations
relation_nobody = self._get_empty_relation()
with self.env.do_in_draft():
relation_nobody.type_selection_id = self.selection_nobody
warning = relation_nobody.onchange_type_selection_id()['warning']
self.assertTrue('message' in warning)
self.assertTrue('No this partner available' in warning['message'])
with self.env.do_in_draft():
relation_nobody.this_partner_id = self.partner_02_company
warning = relation_nobody.onchange_type_selection_id()['warning']
self.assertTrue('message' in warning)
self.assertTrue('incompatible' in warning['message'])
# Allow left partner and check message for other partner:
self.type_nobody.write({
'partner_category_left': False,
})
self.selection_nobody.invalidate_cache(ids=self.selection_nobody.ids)
warning = relation_nobody.onchange_type_selection_id()['warning']
self.assertTrue('message' in warning)
self.assertTrue('No other partner available' in warning['message'])
def test_on_change_partner_id(self):
"""Test on_change_partner_id."""
# 1. Test call with empty relation
relation_empty = self._get_empty_relation()
result = relation_empty.onchange_partner_id()
self.assertTrue('domain' in result)
self.assertFalse('warning' in result)
self.assertTrue('type_selection_id' in result['domain'])
self.assertFalse(result['domain']['type_selection_id'])
# 2. Test call with company 2 person relation
relation = self._create_company2person_relation()
domain = relation.onchange_partner_id()['domain']
self.assertTrue(
('contact_type_this', '=', 'c') in domain['type_selection_id']
)
# 3. Test with invalid or impossible combinations
relation_nobody = self._get_empty_relation()
with self.env.do_in_draft():
relation_nobody.this_partner_id = self.partner_02_company
relation_nobody.type_selection_id = self.selection_nobody
warning = relation_nobody.onchange_partner_id()['warning']
self.assertTrue('message' in warning)
self.assertTrue('incompatible' in warning['message'])

113
partner_relations/tests/test_partner_relation_common.py

@ -0,0 +1,113 @@
# -*- coding: utf-8 -*-
# Copyright 2016 Therp BV
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from openerp.tests import common
class TestPartnerRelationCommon(common.TransactionCase):
def setUp(self):
super(TestPartnerRelationCommon, self).setUp()
self.partner_model = self.env['res.partner']
self.category_model = self.env['res.partner.category']
self.type_model = self.env['res.partner.relation.type']
self.selection_model = self.env['res.partner.relation.type.selection']
self.relation_model = self.env['res.partner.relation']
self.relation_all_model = self.env['res.partner.relation.all']
self.partner_01_person = self.partner_model.create({
'name': 'Test User 1',
'is_company': False,
'ref': 'PR01',
})
self.partner_02_company = self.partner_model.create({
'name': 'Test Company',
'is_company': True,
'ref': 'PR02',
})
# Create partners with specific categories:
self.category_01_ngo = self.category_model.create({
'name': 'NGO',
})
self.partner_03_ngo = self.partner_model.create({
'name': 'Test NGO',
'is_company': True,
'ref': 'PR03',
'category_id': [(4, self.category_01_ngo.id)],
})
self.category_02_volunteer = self.category_model.create({
'name': 'Volunteer',
})
self.partner_04_volunteer = self.partner_model.create({
'name': 'Test Volunteer',
'is_company': False,
'ref': 'PR04',
'category_id': [(4, self.category_02_volunteer.id)],
})
# Create a new relation type withouth categories:
(self.type_company2person,
self.selection_company2person,
self.selection_person2company) = (
self._create_relation_type_selection({
'name': 'mixed',
'name_inverse': 'mixed_inverse',
'contact_type_left': 'c',
'contact_type_right': 'p',
})
)
# Create a new relation type with categories:
(self.type_ngo2volunteer,
self.selection_ngo2volunteer,
self.selection_volunteer2ngo) = (
self._create_relation_type_selection({
'name': 'NGO has volunteer',
'name_inverse': 'volunteer works for NGO',
'contact_type_left': 'c',
'contact_type_right': 'p',
'partner_category_left': self.category_01_ngo.id,
'partner_category_right': self.category_02_volunteer.id,
})
)
def _create_relation_type_selection(self, vals):
"""Create relation type and return this with selection types."""
assert 'name' in vals, (
"Name missing in vals to create relation type. Vals: %s."
% vals
)
assert 'name' in vals, (
"Name_inverse missing in vals to create relation type. Vals: %s."
% vals
)
new_type = self.type_model.create(vals)
self.assertTrue(
new_type,
msg="No relation type created with vals %s." % vals
)
selection_types = self.selection_model.search([
('type_id', '=', new_type.id),
])
for st in selection_types:
if st.is_inverse:
inverse_type_selection = st
else:
type_selection = st
self.assertTrue(
inverse_type_selection,
msg="Failed to find inverse type selection based on"
" relation type created with vals %s." % vals
)
self.assertTrue(
type_selection,
msg="Failed to find type selection based on"
" relation type created with vals %s." % vals
)
return (new_type, type_selection, inverse_type_selection)
def _create_company2person_relation(self):
"""Utility function to get a relation from company 2 partner."""
return self.relation_all_model.create({
'type_selection_id': self.selection_company2person.id,
'this_partner_id': self.partner_02_company.id,
'other_partner_id': self.partner_01_person.id,
})

81
partner_relations/tests/test_partner_relations.py

@ -1,81 +0,0 @@
# -*- coding: utf-8 -*-
# Author: Charbel Jacquin
# Copyright 2015 Camptocamp SA
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from openerp.tests import common
from openerp.exceptions import ValidationError
class TestPartnerRelation(common.TransactionCase):
def setUp(self):
super(TestPartnerRelation, self).setUp()
self.partner_model = self.env['res.partner']
self.relation_type_model = self.env['res.partner.relation.type']
self.relation_model = self.env['res.partner.relation']
self.partner_1 = self.partner_model.create({
'name': 'Test User 1',
'is_company': False
})
self.partner_2 = self.partner_model.create({
'name': 'Test User 2',
'is_company': False
})
self.relation_allow = self.relation_type_model.create({
'name': 'allow',
'name_inverse': 'allow_inverse',
'contact_type_left': 'p',
'contact_type_right': 'p',
'allow_self': True
})
self.relation_disallow = self.relation_type_model.create({
'name': 'disallow',
'name_inverse': 'disallow_inverse',
'contact_type_left': 'p',
'contact_type_right': 'p',
'allow_self': False
})
self.relation_default = self.relation_type_model.create({
'name': 'default',
'name_inverse': 'default_inverse',
'contact_type_left': 'p',
'contact_type_right': 'p',
})
def test_self_allowed(self):
self.relation_model.create({'type_id': self.relation_allow.id,
'left_partner_id': self.partner_1.id,
'right_partner_id': self.partner_1.id})
def test_self_disallowed(self):
with self.assertRaises(ValidationError):
self.relation_model.create({'type_id': self.relation_disallow.id,
'left_partner_id': self.partner_1.id,
'right_partner_id': self.partner_1.id})
def test_self_default(self):
with self.assertRaises(ValidationError):
self.relation_model.create({'type_id': self.relation_default.id,
'left_partner_id': self.partner_1.id,
'right_partner_id': self.partner_1.id})

76
partner_relations/tests/test_partner_search.py

@ -0,0 +1,76 @@
# -*- coding: utf-8 -*-
# Copyright 2015 Camptocamp SA
# Copyright 2016 Therp BV
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from openerp import fields
from openerp.exceptions import ValidationError
from .test_partner_relation_common import TestPartnerRelationCommon
class TestPartnerSearch(TestPartnerRelationCommon):
def test_search_relation_type(self):
"""Test searching on relation type."""
relation = self._create_company2person_relation()
partners = self.partner_model.search([
('search_relation_type_id', '=', relation.type_selection_id.id)
])
self.assertTrue(self.partner_02_company in partners)
partners = self.partner_model.search([
('search_relation_type_id', '!=', relation.type_selection_id.id)
])
self.assertTrue(self.partner_01_person in partners)
partners = self.partner_model.search([
('search_relation_type_id', '=', self.type_company2person.name)
])
self.assertTrue(self.partner_01_person in partners)
self.assertTrue(self.partner_02_company in partners)
partners = self.partner_model.search([
('search_relation_type_id', '=', 'unknown relation')
])
self.assertFalse(partners)
# Check error with invalid search operator:
with self.assertRaises(ValidationError):
partners = self.partner_model.search([
('search_relation_type_id', 'child_of', 'some parent')
])
def test_search_relation_partner(self):
"""Test searching on related partner."""
self._create_company2person_relation()
partners = self.partner_model.search([
('search_relation_partner_id', '=', self.partner_02_company.id),
])
self.assertTrue(self.partner_01_person in partners)
def test_search_relation_date(self):
"""Test searching on relations valid on a certain date."""
self._create_company2person_relation()
partners = self.partner_model.search([
('search_relation_date', '=', fields.Date.today()),
])
self.assertTrue(self.partner_01_person in partners)
self.assertTrue(self.partner_02_company in partners)
def test_search_any_partner(self):
"""Test searching for partner left or right."""
self._create_company2person_relation()
both_relations = self.relation_all_model.search([
('any_partner_id', '=', self.partner_02_company.id),
])
self.assertEqual(len(both_relations), 2)
def test_search_partner_category(self):
"""Test searching for partners related to partners having category."""
relation_ngo_volunteer = self.relation_all_model.create({
'this_partner_id': self.partner_03_ngo.id,
'type_selection_id': self.selection_ngo2volunteer.id,
'other_partner_id': self.partner_04_volunteer.id,
})
self.assertTrue(relation_ngo_volunteer)
partners = self.partner_model.search([
('search_relation_partner_category_id', '=',
self.category_02_volunteer.id)
])
self.assertTrue(self.partner_03_ngo in partners)

97
partner_relations/view/res_partner_relation.xml

@ -1,97 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<openerp>
<data>
<record id="form_res_partner_relation" model="ir.ui.view">
<field name="model">res.partner.relation</field>
<field type="xml" name="arch">
<form string="Partner Relation">
<sheet>
<field name="left_partner_id" />
<field name="type_id" />
<field name="right_partner_id" />
</sheet>
</form>
</field>
</record>
<record id="tree_res_partner_relation" model="ir.ui.view">
<field name="model">res.partner.relation</field>
<field name="arch" type="xml">
<tree
string="Partner Relations"
colors="gray:date_end and date_end &lt; current_date or not active;blue:date_start &gt; current_date"
editable="top"
>
<field
name="left_partner_id"
options="{'create': false, 'create_edit': false}"
/>
<field
name="type_selection_id"
required="True"
options="{'create': false, 'create_edit': false}"/>
<field
name="right_partner_id"
options="{'create': false, 'create_edit': false}"
/>
<field name="date_start" />
<field name="date_end" />
<field name="active" />
</tree>
</field>
</record>
<record id="search_res_partner_relation" model="ir.ui.view">
<field name="model">res.partner.relation</field>
<field name="arch" type="xml">
<search string="Search Relations">
<field name="any_partner_id" widget="many2one"/>
<field name="left_partner_id"/>
<field name="right_partner_id"/>
<field name="type_id"/>
<field name="active"/>
<group expand="0" string="Group By">
<filter string="Left Partner" context="{'group_by': 'left_partner_id'}"/>
<filter string="Right Partner" context="{'group_by': 'right_partner_id'}"/>
<filter string="Relationship Type" context="{'group_by': 'type_id'}"/>
</group>
</search>
</field>
</record>
<record id="action_res_partner_relation" model="ir.actions.act_window">
<field name="name">Relations</field>
<field name="res_model">res.partner.relation</field>
<field name="view_type">form</field>
<field name="view_mode">tree</field>
<field name="view_id" ref="tree_res_partner_relation"/>
<field name="search_view_id" ref="search_res_partner_relation"/>
<field name="domain">[('active', '=', True)]</field>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Record and track your partners' relations. Relations may be linked to other partners with a type either directly or inversely.
</p>
</field>
</record>
<record id="action_show_right_relation_partners" model="ir.actions.server">
<field name="sequence" eval="5"/>
<field name="state">code</field>
<field name="type">ir.actions.server</field>
<field name="model_id" ref="model_res_partner_relation"/>
<field name="code">action = self.get_action_related_partners(cr, uid, context.get('active_ids', []), dict(context or {}, partner_relations_show_side='right'))</field>
<field name="condition">True</field>
<field name="name">Show partners</field>
</record>
<record id="action_show_right_relation_partners_value" model="ir.values">
<field name="name">Show partners</field>
<field name="key">action</field>
<field name="key2">client_action_multi</field>
<field name="model">res.partner.relation.all</field>
<field name="value" eval="'ir.actions.server,%d' % ref('partner_relations.action_show_right_relation_partners')" />
</record>
</data>
</openerp>

92
partner_relations/view/res_partner_relation_all.xml

@ -1,92 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<openerp>
<data>
<record id="tree_res_partner_relation_all" model="ir.ui.view">
<field name="model">res.partner.relation.all</field>
<field name="arch" type="xml">
<tree
string="Partner Relations"
colors="gray:(date_end and date_end &lt; current_date) or not active;blue:date_start &gt; current_date"
editable="top"
>
<field
name="this_partner_id"
readonly="1"
/>
<field
name="type_selection_id"
required="True"
domain="[
'|',
('contact_type_this', '=', False),
('contact_type_this', '=', contact_type),
]"
options="{'create': false, 'create_edit': false}"
/>
<field
name="other_partner_id"
attrs="{
'readonly': [('type_selection_id', '=', False)],
}"
options="{'create': false, 'create_edit': false}"
/>
<field name="date_start" />
<field name="date_end" />
<field name="active" />
<field name="contact_type" invisible="1"/>
</tree>
</field>
</record>
<record id="form_res_partner_relation_all" model="ir.ui.view">
<field name="model">res.partner.relation.all</field>
<field name="arch" type="xml">
<form string="Partner relation">
<group>
<field name="this_partner_id" />
<field name="type_selection_id" />
<field name="other_partner_id" />
</group>
<group>
<field name="date_start" />
<field name="date_end" />
<field name="active" />
</group>
</form>
</field>
</record>
<record id="search_res_partner_relation_all" model="ir.ui.view">
<field name="model">res.partner.relation.all</field>
<field name="arch" type="xml">
<search string="Search Relations">
<field name="this_partner_id"/>
<field name="other_partner_id"/>
<field name="type_id"/>
<field name="active"/>
<group expand="0" string="Group By">
<filter string="Other Partner" context="{'group_by': 'other_partner_id'}"/>
<filter string="Relationship Type" context="{'group_by': 'type_id'}"/>
</group>
</search>
</field>
</record>
<record id="action_res_partner_relation_all" model="ir.actions.act_window">
<field name="name">Relations</field>
<field name="res_model">res.partner.relation.all</field>
<field name="view_type">form</field>
<field name="view_mode">tree</field>
<field name="view_id" ref="tree_res_partner_relation_all"/>
<field name="search_view_id" ref="search_res_partner_relation_all"/>
<field name="domain">[('active', '=', True)]</field>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Record and track your partners' relations. Relations may be linked to other partners with a type either directly or inversely.
</p>
</field>
</record>
</data>
</openerp>

2
partner_relations/view/menu.xml → partner_relations/views/menu.xml

@ -5,7 +5,7 @@
id="menu_res_partner_relation_sales"
sequence="2"
parent="base.menu_sales"
action="action_res_partner_relation"
action="action_res_partner_relation_all"
/>
<act_window

34
partner_relations/view/res_partner.xml → partner_relations/views/res_partner.xml

@ -10,7 +10,7 @@
<data>
<field name="parent_id" position="after">
<field name="search_relation_partner_id" />
<field name="search_relation_id" />
<field name="search_relation_type_id" />
<field name="search_relation_date" />
<field name="search_relation_partner_category_id" />
</field>
@ -24,19 +24,25 @@
<field name="model">res.partner</field>
<field type="xml" name="arch">
<xpath expr="//div[@name='buttons']" position="inside">
<button class="oe_inline oe_stat_button"
type="action"
context="{
'search_default_this_partner_id': active_id,
'default_left_partner_id': active_id,
'active_model': 'res.partner',
'active_id': id,
'active_ids': [id],
'active_test': False,
}"
name="%(action_res_partner_relation_all)d"
icon="fa-users">
<field string="Relations" name="relation_count" widget="statinfo"/>
<button
class="oe_inline oe_stat_button"
type="action"
context="{
'search_default_this_partner_id': active_id,
'default_this_partner_id': active_id,
'active_model': 'res.partner',
'active_id': id,
'active_ids': [id],
'active_test': False,
}"
name="%(action_res_partner_relation_all)d"
icon="fa-users"
>
<field
string="Relations"
name="relation_count"
widget="statinfo"
/>
</button>
</xpath>
</field>

96
partner_relations/views/res_partner_relation_all.xml

@ -0,0 +1,96 @@
<?xml version="1.0" encoding="UTF-8"?>
<openerp>
<data>
<record id="tree_res_partner_relation_all" model="ir.ui.view">
<field name="model">res.partner.relation.all</field>
<field name="arch" type="xml">
<tree
string="Partner Relations"
colors="gray:not active; blue:date_start &gt; current_date"
editable="top"
>
<field
name="this_partner_id"
required="True"
options="{'no_create': True}"
/>
<field
name="type_selection_id"
required="True"
options="{'no_create': True}"
/>
<field
name="other_partner_id"
required="True"
options="{'no_create': True}"
/>
<field name="date_start" />
<field name="date_end" />
<field name="active" invisible="1" />
</tree>
</field>
</record>
<record id="search_res_partner_relation_all" model="ir.ui.view">
<field name="model">res.partner.relation.all</field>
<field name="arch" type="xml">
<search string="Search Relations">
<field name="any_partner_id" widget="many2one"/>
<field name="this_partner_id"/>
<field name="other_partner_id"/>
<field name="type_selection_id"/>
<filter
string="Left to right"
domain="[('record_type', '=', 'a')]"
/>
<filter
string="Right to left"
domain="[('record_type', '=', 'b')]"
/>
<filter
string="Include past records"
context="{'active_test': False}"
/>
<group expand="0" string="Group By">
<filter
string="One Partner"
context="{'group_by': 'this_partner_id'}"
/>
<filter
string="Relationship Type"
context="{'group_by': 'type_selection_id'}"
/>
<filter
string="Other Partner"
context="{'group_by': 'other_partner_id'}"
/>
</group>
</search>
</field>
</record>
<record
id="action_res_partner_relation_all"
model="ir.actions.act_window"
>
<field name="name">Relations</field>
<field name="res_model">res.partner.relation.all</field>
<field name="view_type">form</field>
<field name="view_mode">tree</field>
<field name="view_id" ref="tree_res_partner_relation_all"/>
<field
name="search_view_id"
ref="search_res_partner_relation_all"
/>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Record and track your partners' relations. Relations may
be linked to other partners with a type either directly
or inversely.
</p>
</field>
</record>
</data>
</openerp>

23
partner_relations/view/res_partner_relation_type.xml → partner_relations/views/res_partner_relation_type.xml

@ -2,42 +2,47 @@
<data>
<record id="tree_res_partner_relation_type" model="ir.ui.view">
<field name="model">res.partner.relation.type</field>
<field name="type">tree</field>
<field type="xml" name="arch">
<tree version="7.0" string="Partner relation">
<tree>
<field name="name" />
<field name="name_inverse" />
<field name="contact_type_left" />
<field name="contact_type_right" />
<field name="allow_self" />
<field name="is_symmetric" />
</tree>
</field>
</record>
<record id="form_res_partner_relation_type" model="ir.ui.view">
<field name="model">res.partner.relation.type</field>
<field name="type">form</field>
<field type="xml" name="arch">
<form version="7.0" string="Partner relation">
<form>
<sheet>
<group>
<field name="allow_self" />
<group
colspan="2" col="2"
string="Left side of relation"
>
name="left"
>
<field name="name" />
<field name="contact_type_left" />
<field name="partner_category_left" />
</group>
<group
colspan="2" col="2"
string="Right side of relation"
>
name="right"
attrs="{'invisible': [('is_symmetric', '=', True)]}"
>
<field name="name_inverse" />
<field name="contact_type_right" />
<field name="partner_category_right" />
</group>
</group>
<group name="properties" string="Properties">
<field name="allow_self" />
<field name="is_symmetric" />
<field name="handle_invalid_onchange" />
</group>
</sheet>
</form>
</field>
Loading…
Cancel
Save