Merge lp:~openerp-dev/openobject-addons/7.0-base-contact-xal into lp:openobject-addons/7.0

Proposed by Martin Trigaux (OpenERP)
Status: Needs review
Proposed branch: lp:~openerp-dev/openobject-addons/7.0-base-contact-xal
Merge into: lp:openobject-addons/7.0
Diff against target: 691 lines (+653/-0)
7 files modified
base_contact/__init__.py (+23/-0)
base_contact/__openerp__.py (+52/-0)
base_contact/base_contact.py (+185/-0)
base_contact/base_contact_demo.xml (+29/-0)
base_contact/base_contact_view.xml (+202/-0)
base_contact/tests/__init__.py (+26/-0)
base_contact/tests/test_base_contact.py (+136/-0)
To merge this branch: bzr merge lp:~openerp-dev/openobject-addons/7.0-base-contact-xal
Reviewer Review Type Date Requested Status
Jacques-Etienne Baudoux (community) Needs Fixing
OpenERP Community Reviewer/Maintainer Pending
Martin Trigaux (OpenERP) Pending
Review via email: mp+192129@code.launchpad.net

Description of the change

Module base_contact porte de 6 -> 7

To post a comment you must log in.
Revision history for this message
Martin Trigaux (OpenERP) (mat-openerp) wrote :

Bonjour Xavier,

Voila quelques remarques concernant le module:

l87: vals['name'] = self.browse(...). Il manque un .name apres le browse. Cependant 'name' est required sur le partenaire donc ce cas ne devrait pas arriver (du moins via l'interface web)...

La valeur du context search_show_all_positions va doubler le nombre de recherche sur partner (eg: le name_get sur partner est deja lent avec un grand nombre de partenaire). J'ai beau reflechir je ne vois pas comment on pourrait eviter cela si on veut en effet cacher les resultats des attached.

Est-ce que le champ contact_type est réellement nécessaire ? Il se resume globalement a indiquer si contact_id vaut False.

9535. By Xavier ALT

[FIX] base_contact: fix contact staying attached after settings it's type to standalone

9536. By Xavier ALT

[FIX] base_contact: missing .name

9537. By Xavier ALT

[IMP] base_contact: add contact fields (name + title) sync between partner attached to the same contact

Revision history for this message
Xavier ALT (dex-phx) wrote :

Bonjour Martin,

l87: fixé

Pour ``search()``; oui pour le moment je suis obligé de faire deux recherches consécutives, c'est la seule manière de retourner un résultat correct, i.e retourner le contact "standalone" dont au moins un contact "attaché" match les critères de recherche.

Pour ``contact_type`` il est nécessaire dans l'interface, car il permet en alternance soit ``contact_id``, soit ``name``. (Ex depuis un partenaire de type "company", ajouter un contact => il te propose de choisir entre 'Standalone Contact' & 'Attached to an existing contact').

J'ai aussi fixé le bug rencontré par Olivier (olt) et ajouter la synchro du champs "Nom" & "Title".

Revision history for this message
Jacques-Etienne Baudoux (jbaudoux) wrote :

Hello,

Thanks for making base contact available in v7.0.

Here are my comments:
*/ Customer -> Other Positions (o2m). At first sight I thought the purpose is to create a private contact and then consider 'other positions' as the list of job positions. But it appears you can start by creating a professional contact and then add other professional contacts or private contacts. Why not. But:
- in the list or kanban, you only see the first created contact. I would expect to see all contacts. When you filter a company, you see the company and all their contacts. I'd like the same when I filter a contact and see all the positions.
- you cannot access directly the other contacts from the customer view (because you don't see them)
- the name is confusing because "Other Positions" suggest it's only contacts that have a position in a company and it would mean you could only create professional contacts but the company field is not required.
- in the other contacts, the view is missing all the tabs you have on a contact. If I create in other positions a private contact, I need to manage e.g. the accounting for the sales to that private contact.

*/ From another view that has a m2o to res.partner and looking for a contact. I start typing the name and I see all the contacts (private and professional) and I decide to create a new contact (a new job position in another company). I am not able to link this contact to the other already existing contacts.

*/ I have to 2 professional contacts related to the same person. How can I create a link between those? This will always occur due to previous remark.

*/ I don't understand the purpose of the "Personal Information" tab for a professional contact (a contact linked to a company)

*/ When you filter a company, it's not the company contact that is listed but the 'main' contact that is linked to the company contact. That is weird.
*/ In Other Positions, when I create a new professional contact and use company address, why contact type is still displayed?
*/ In Other Positions, when I create a new professional contact and use company address, the company address should be displayed and editable like on the parent contact.
*/ In Other Positions, when I create a new professional contact and fill a new company, why it is not tagged as company?
*/ In Other Positions, the form is called "OtherS Positions"
*/ When you create or edit a contact from a company, you can choose between standalone and attached, but it's like it shouldn't visible/editable. I think contact_type should be readonly especially that there is no write method.

More to come when the concept will be clarified. Please also update description of the module explaining how it works.

kr,
J-E

review: Needs Fixing
Revision history for this message
Pierre Lamarche (www.savoirfairelinux.com) (pierre-lamarche) wrote :

Hello,

First of all, thanks for this work.

Here are our review and comments about the functionnalities :

* One contact should have multiple relation with companies on the same level.
(why differenciate a main one and other positions ?)
We should be able to leave blank the parent company field on the top and only add multiple positions in the "Other Positions" tab below

* When you are creating a contact directly on the already opened partner form of a company, it only creates the database entry for the contact, company instead of create a one entry for the contact alone, and one entry for the contact, company.

* The idea is to manage a list of contact, a list of company and a list of relation between them.
one contact can have several relations with one or several companies, a company can have several relations with one or several contacts. Each relation should be based on a job position with professional addresses, telephone, etc...

I would be happy to discuss about that, Xavier.

BR,

Pierre

Revision history for this message
Olivier Dony (Odoo) (odo-openerp) wrote :

Hi everyone,

Thanks for your feedback! Xavier has done a great job and provided a simple implementation that covers the main contact management feature that is not covered in the standard v7 distribution: the ability to easily link a single person to multiple companies, while maintaining the important semantics of the OpenERP v7 partner model.

This is the main goal of the `base_contact` port for v7 (the name is not very appropriate anymore, as contact management is now builtin without needing this module). The point is not to reproduce exactly the behavior of base_contact in v6.1, nor to implement new features related to the management of multiple positions for a given contact. The point is to establish clean groundwork for managing multiple positions for a single person under the OpenERP v7 model.
In this context the single most important aspect is the way we model these concepts at the database level - if that is correct, the rest are simple UI/UX details that can be fine-tuned easily.

We will complete the review of the functional and technical aspects of this new module very soon, and will attempt to answer your various remarks at the same time.

Do not hesitate to post further feedback here, but please be patient and keep the scope of this development in mind!

Revision history for this message
Rolf Wojtech (rolf-wojtech) wrote :

I have to admit that my knowledge of the OpenERP module landscape is not vast.
We currently have a 6.1 installation using base_contact that we want to migrate from to 7.0.

I am now wondering if this module will take care of the migration as well, I did not see any migration code in the diffs.
I also found this non-merge openupgrade-server-Fix for migrating base_contact's database model into the OpenERP 7 model:
https://code.launchpad.net/~savoirfairelinux-openerp/openupgrade-server/base_contact/+merge/194762

So basically I wonder if Xavier has covered migration as well or if I should resort to Sandy Carter's migration code.

(I know this is not a support forum but I feel like having this information here will help many others as well).

Revision history for this message
Fabrice (OpenERP) (fhe) wrote :

Rolf, for migration, see https://www.openerp.com/pricing

Revision history for this message
Stefan Rijnhart (Opener) (stefan-opener) wrote :

@rolf Yes, please refer to the OpenERP migration service if you have an OPW or consider getting one if you can. As for the OpenUpgrade branch that you have found, its code migrates an existing installation of 6.1 base_contact to regular partners in 7.0 because the status of this branch was still unclear at the time. That is probably not what you want.

Revision history for this message
Sandy Carter (http://www.savoirfairelinux.com) (sandy-carter) wrote :

What is the current status of this MP?

Revision history for this message
Leonardo Pistone (lepistone) wrote :

Thanks for your work Martin.

If this merge is not a priority for OpenERP SA and someone else needs it, maybe we could propose it for merging in OCB?

Revision history for this message
Sandy Carter (http://www.savoirfairelinux.com) (sandy-carter) wrote :

Unmerged revisions

9537. By Xavier ALT

[IMP] base_contact: add contact fields (name + title) sync between partner attached to the same contact

9536. By Xavier ALT

[FIX] base_contact: missing .name

9535. By Xavier ALT

[FIX] base_contact: fix contact staying attached after settings it's type to standalone

9534. By Xavier ALT

[+] base_contact module

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== added directory 'base_contact'
=== added file 'base_contact/__init__.py'
--- base_contact/__init__.py 1970-01-01 00:00:00 +0000
+++ base_contact/__init__.py 2013-10-23 13:04:00 +0000
@@ -0,0 +1,23 @@
1# -*- coding: utf-8 -*-
2##############################################################################
3#
4# OpenERP, Open Source Management Solution
5# Copyright (C) 2013-TODAY OpenERP SA (<http://www.openerp.com>).
6#
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU Affero General Public License as
9# published by the Free Software Foundation, either version 3 of the
10# License, or (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU Affero General Public License for more details.
16#
17# You should have received a copy of the GNU Affero General Public License
18# along with this program. If not, see <http://www.gnu.org/licenses/>.
19#
20##############################################################################
21
22import base_contact
23
024
=== added file 'base_contact/__openerp__.py'
--- base_contact/__openerp__.py 1970-01-01 00:00:00 +0000
+++ base_contact/__openerp__.py 2013-10-23 13:04:00 +0000
@@ -0,0 +1,52 @@
1# -*- coding: utf-8 -*-
2##############################################################################
3#
4# OpenERP, Open Source Business Applications
5# Copyright (C) 2013-TODAY OpenERP S.A. (<http://openerp.com>).
6#
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU Affero General Public License as
9# published by the Free Software Foundation, either version 3 of the
10# License, or (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU Affero General Public License for more details.
16#
17# You should have received a copy of the GNU Affero General Public License
18# along with this program. If not, see <http://www.gnu.org/licenses/>.
19#
20##############################################################################
21
22{
23 'name': 'Contacts Management',
24 'version': '1.0',
25 'category': 'Customer Relationship Management',
26 'complexity': "expert",
27 'description': """
28This module allows you to manage your contacts
29==============================================
30
31It lets you define groups of contacts sharing some common information, like:
32 * Birthdate
33 * Nationality
34 * Native Language
35
36 """,
37 'author': 'OpenERP SA',
38 'website': 'http://www.openerp.com',
39 'depends': ['base', 'process', 'contacts'],
40 'init_xml': [],
41 'update_xml': [
42 'base_contact_view.xml',
43 ],
44 'demo_xml': [
45 'base_contact_demo.xml',
46 ],
47 'installable': True,
48 'auto_install': False,
49 #'certificate': '0031287885469',
50 'images': [],
51}
52# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
053
=== added file 'base_contact/base_contact.py'
--- base_contact/base_contact.py 1970-01-01 00:00:00 +0000
+++ base_contact/base_contact.py 2013-10-23 13:04:00 +0000
@@ -0,0 +1,185 @@
1# -*- coding: utf-8 -*-
2##############################################################################
3#
4# OpenERP, Open Source Management Solution
5# Copyright (C) 2013-TODAY OpenERP SA (<http://www.openerp.com>).
6#
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU Affero General Public License as
9# published by the Free Software Foundation, either version 3 of the
10# License, or (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU Affero General Public License for more details.
16#
17# You should have received a copy of the GNU Affero General Public License
18# along with this program. If not, see <http://www.gnu.org/licenses/>.
19#
20##############################################################################
21
22from openerp.osv import fields, osv, expression
23
24
25class res_partner(osv.osv):
26 _inherit = 'res.partner'
27
28 _contact_type = [
29 ('standalone', 'Standalone Contact'),
30 ('attached', 'Attached to existing Contact'),
31 ]
32
33 def _get_contact_type(self, cr, uid, ids, field_name, args, context=None):
34 result = dict.fromkeys(ids, 'standalone')
35 for partner in self.browse(cr, uid, ids, context=context):
36 if partner.contact_id:
37 result[partner.id] = 'attached'
38 return result
39
40 _columns = {
41 'contact_type': fields.function(_get_contact_type, type='selection', selection=_contact_type,
42 string='Contact Type', required=True, select=1, store=True),
43 'contact_id': fields.many2one('res.partner', 'Main Contact',
44 domain=[('is_company','=',False),('contact_type','=','standalone')]),
45 'other_contact_ids': fields.one2many('res.partner', 'contact_id', 'Others Positions'),
46
47 # Person specific fields
48 'birthdate_date': fields.date('Birthdate'), # add a 'birthdate' as date field, i.e different from char 'birthdate' introduced v6.1!
49 'nationality_id': fields.many2one('res.country', 'Nationality'),
50 }
51
52 _defaults = {
53 'contact_type': 'standalone',
54 }
55
56 def _basecontact_check_context(self, cr, user, mode, context=None):
57 if context is None:
58 context = {}
59 # Remove 'search_show_all_positions' for non-search mode.
60 # Keeping it in context can result in unexpected behaviour (ex: reading
61 # one2many might return wrong result - i.e with "attached contact" removed
62 # even if it's directly linked to a company).
63 if mode != 'search':
64 context.pop('search_show_all_positions', None)
65 return context
66
67 def search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False):
68 if context is None:
69 context = {}
70 if context.get('search_show_all_positions') is False:
71 # display only standalone contact matching ``args`` or having
72 # attached contact matching ``args``
73 args = expression.normalize_domain(args)
74 attached_contact_args = expression.AND((args, [('contact_type', '=', 'attached')]))
75 attached_contact_ids = super(res_partner, self).search(cr, user, attached_contact_args,
76 context=context)
77 args = expression.OR((
78 expression.AND(([('contact_type', '=', 'standalone')], args)),
79 [('other_contact_ids', 'in', attached_contact_ids)],
80 ))
81 return super(res_partner, self).search(cr, user, args, offset=offset, limit=limit,
82 order=order, context=context, count=count)
83
84 def create(self, cr, user, vals, context=None):
85 context = self._basecontact_check_context(cr, user, 'create', context)
86 if not vals.get('name') and vals.get('contact_id'):
87 vals['name'] = self.browse(cr, user, vals['contact_id'], context=context).name
88 return super(res_partner, self).create(cr, user, vals, context=context)
89
90 def read(self, cr, user, ids, fields=None, context=None, load='_classic_read'):
91 context = self._basecontact_check_context(cr, user, 'read', context)
92 return super(res_partner, self).read(cr, user, ids, fields=fields, context=context, load=load)
93
94 def write(self, cr, user, ids, vals, context=None):
95 context = self._basecontact_check_context(cr, user, 'write', context)
96 return super(res_partner, self).write(cr, user, ids, vals, context=context)
97
98 def unlink(self, cr, user, ids, context=None):
99 context = self._basecontact_check_context(cr, user, 'unlink', context)
100 return super(res_partner, self).unlink(cr, user, ids, context=context)
101
102 def _commercial_partner_compute(self, cr, uid, ids, name, args, context=None):
103 """ Returns the partner that is considered the commercial
104 entity of this partner. The commercial entity holds the master data
105 for all commercial fields (see :py:meth:`~_commercial_fields`) """
106 result = super(res_partner, self)._commercial_partner_compute(cr, uid, ids, name, args, context=context)
107 for partner in self.browse(cr, uid, ids, context=context):
108 if partner.contact_type == 'attached' and not partner.parent_id:
109 result[partner.id] = partner.contact_id.id
110 return result
111
112 def _contact_fields(self, cr, uid, context=None):
113 """ Returns the list of contact fields that are synced from the parent
114 when a partner is attached to him. """
115 return ['name', 'title']
116
117 def _contact_sync_from_parent(self, cr, uid, partner, context=None):
118 """ Handle sync of contact fields when a new parent contact entity is set,
119 as if they were related fields """
120 if partner.contact_id:
121 contact_fields = self._contact_fields(cr, uid, context=context)
122 sync_vals = self._update_fields_values(cr, uid, partner.contact_id,
123 contact_fields, context=context)
124 partner.write(sync_vals)
125
126 def update_contact(self, cr, uid, ids, vals, context=None):
127 if context is None:
128 context = {}
129 if context.get('__update_contact_lock'):
130 return
131 contact_fields = self._contact_fields(cr, uid, context=context)
132 contact_vals = dict((field, vals[field]) for field in contact_fields if field in vals)
133 if contact_vals:
134 ctx = dict(context, __update_contact_lock=True)
135 self.write(cr, uid, ids, contact_vals, context=ctx)
136
137 def _fields_sync(self, cr, uid, partner, update_values, context=None):
138 """ Sync commercial fields and address fields from company and to children,
139 contact fields from contact and to attached contact after create/update,
140 just as if those were all modeled as fields.related to the parent """
141 super(res_partner, self)._fields_sync(cr, uid, partner, update_values, context=context)
142 contact_fields = self._contact_fields(cr, uid, context=context)
143 # 1. From UPSTREAM: sync from parent contact
144 if update_values.get('contact_id'):
145 self._contact_sync_from_parent(cr, uid, partner, context=context)
146 # 2. To DOWNSTREAM: sync contact fields to parent or related
147 elif any(field in contact_fields for field in update_values):
148 update_ids = [c.id for c in partner.other_contact_ids if not c.is_company]
149 if partner.contact_id:
150 update_ids.append(partner.contact_id.id)
151 self.update_contact(cr, uid, update_ids, update_values, context=context)
152
153 def onchange_contact_id(self, cr, uid, ids, contact_id, context=None):
154 values = {}
155 if contact_id:
156 values['name'] = self.browse(cr, uid, contact_id, context=context).name
157 return {'value': values}
158
159 def onchange_contact_type(self, cr, uid, ids, contact_type, context=None):
160 values = {}
161 if contact_type == 'standalone':
162 values['contact_id'] = False
163 return {'value': values}
164
165
166class ir_actions_window(osv.osv):
167 _inherit = 'ir.actions.act_window'
168
169 def read(self, cr, user, ids, fields=None, context=None, load='_classic_read'):
170 action_ids = ids
171 if isinstance(ids, (int, long)):
172 action_ids = [ids]
173 actions = super(ir_actions_window, self).read(cr, user, action_ids, fields=fields, context=context, load=load)
174 for action in actions:
175 if action.get('res_model', '') == 'res.partner':
176 # By default, only show standalone contact
177 action_context = action.get('context', '{}') or '{}'
178 if 'search_show_all_positions' not in action_context:
179 action['context'] = action_context.replace('{',
180 "{'search_show_all_positions': False,", 1)
181 if isinstance(ids, (int, long)):
182 if actions:
183 return actions[0]
184 return False
185 return actions
0186
=== added file 'base_contact/base_contact_demo.xml'
--- base_contact/base_contact_demo.xml 1970-01-01 00:00:00 +0000
+++ base_contact/base_contact_demo.xml 2013-10-23 13:04:00 +0000
@@ -0,0 +1,29 @@
1<?xml version="1.0" encoding="UTF-8"?>
2<openerp>
3 <data>
4
5 <record id="res_partner_main2_position_consultant" model="res.partner">
6 <field name="name">Roger Scott</field>
7 <field name="function">Consultant</field>
8 <field name="parent_id" ref="base.res_partner_11"/>
9 <field name="contact_id" ref="base.res_partner_main2"/>
10 <field name="use_parent_address" eval="True"/>
11 </record>
12
13 <record id="res_partner_contact1" model="res.partner">
14 <field name="name">Bob Egnops</field>
15 <field name="birthdate_date">1984-01-01</field>
16 <field name="email">bob@hillenburg-oceaninstitute.com</field>
17 </record>
18
19 <record id="res_partner_contact1_work_position1" model="res.partner">
20 <field name="name">Bob Egnops</field>
21 <field name="function">Technician</field>
22 <field name="email">bob@yourcompany.com</field>
23 <field name="parent_id" ref="base.main_partner"/>
24 <field name="contact_id" ref="res_partner_contact1"/>
25 <field name="use_parent_address" eval="True"/>
26 </record>
27
28 </data>
29</openerp>
0\ No newline at end of file30\ No newline at end of file
131
=== added file 'base_contact/base_contact_view.xml'
--- base_contact/base_contact_view.xml 1970-01-01 00:00:00 +0000
+++ base_contact/base_contact_view.xml 2013-10-23 13:04:00 +0000
@@ -0,0 +1,202 @@
1<?xml version="1.0" encoding="utf-8"?>
2<openerp>
3<data>
4
5 <record id="view_res_partner_filter_contact" model="ir.ui.view">
6 <field name="name">res.partner.select.contact</field>
7 <field name="model">res.partner</field>
8 <field name="inherit_id" ref="base.view_res_partner_filter"/>
9 <field name="arch" type="xml">
10 <filter name="type_company" position="after">
11 <separator/>
12 <filter string="All positions" name="type_otherpositions"
13 context="{'search_show_all_positions': True}"
14 help="All partner positions"/>
15 </filter>
16 <xpath expr="/search/group/filter[@string='Company']" position="before">
17 <filter string="Person" name="group_person" context="{'group_by': 'contact_id'}"/>
18 </xpath>
19 </field>
20 </record>
21
22 <record id="view_res_partner_tree_contact" model="ir.ui.view">
23 <field name="name">res.partner.tree.contact</field>
24 <field name="model">res.partner</field>
25 <field name="inherit_id" ref="base.view_partner_tree"/>
26 <field name="arch" type="xml">
27 <field name="parent_id" position="after">
28 <field name="contact_id" invisible="1"/>
29 </field>
30 </field>
31 </record>
32
33 <record model="ir.ui.view" id="view_partner_form_inherit">
34 <field name="name">res.partner.form.contact</field>
35 <field name="model">res.partner</field>
36 <field name="inherit_id" ref="base.view_partner_form"/>
37 <field name="type">form</field>
38 <field name="arch" type="xml">
39 <field name="is_company" position="after">
40 <field name="contact_type" invisible="1"/>
41 </field>
42 <page string="Contacts" position="after">
43 <page string="Other Positions" attrs="{'invisible': ['|',('is_company','=',True),('contact_id','!=',False)]}">
44 <field name="other_contact_ids" context="{'default_contact_id': active_id, 'default_name': name, 'default_street': street, 'default_street2': street2, 'default_city': city, 'default_state_id': state_id, 'default_zip': zip, 'default_country_id': country_id, 'default_supplier': supplier}}" mode="kanban">
45 <kanban>
46 <field name="color"/>
47 <field name="name"/>
48 <field name="title"/>
49 <field name="email"/>
50 <field name="parent_id"/>
51 <field name="is_company"/>
52 <field name="function"/>
53 <field name="phone"/>
54 <field name="street"/>
55 <field name="street2"/>
56 <field name="zip"/>
57 <field name="city"/>
58 <field name="country_id"/>
59 <field name="mobile"/>
60 <field name="fax"/>
61 <field name="state_id"/>
62 <field name="has_image"/>
63 <templates>
64 <t t-name="kanban-box">
65 <t t-set="color" t-value="kanban_color(record.color.raw_value)"/>
66 <div t-att-class="color + (record.title.raw_value == 1 ? ' oe_kanban_color_alert' : '')" style="position: relative">
67 <a t-if="! read_only_mode" type="delete" style="position: absolute; right: 0; padding: 4px; diplay: inline-block">X</a>
68 <div class="oe_module_vignette">
69 <a type="open">
70 <t t-if="record.has_image.raw_value === true">
71 <img t-att-src="kanban_image('res.partner', 'image', record.id.value, {'preview_image': 'image_small'})" class="oe_avatar oe_kanban_avatar_smallbox"/>
72 </t>
73 <t t-if="record.image and record.image.raw_value !== false">
74 <img t-att-src="'data:image/png;base64,'+record.image.raw_value" class="oe_avatar oe_kanban_avatar_smallbox"/>
75 </t>
76 <t t-if="record.has_image.raw_value === false and (!record.image or record.image.raw_value === false)">
77 <t t-if="record.is_company.raw_value === true">
78 <img t-att-src='_s + "/base/static/src/img/company_image.png"' class="oe_kanban_image oe_kanban_avatar_smallbox"/>
79 </t>
80 <t t-if="record.is_company.raw_value === false">
81 <img t-att-src='_s + "/base/static/src/img/avatar.png"' class="oe_kanban_image oe_kanban_avatar_smallbox"/>
82 </t>
83 </t>
84 </a>
85 <div class="oe_module_desc">
86 <div class="oe_kanban_box_content oe_kanban_color_bglight oe_kanban_color_border">
87 <table class="oe_kanban_table">
88 <tr>
89 <td class="oe_kanban_title1" align="left" valign="middle">
90 <h4><a type="open"><field name="name"/></a></h4>
91 <i>
92 <t t-if="record.parent_id.raw_value and !record.function.raw_value"><field name="parent_id"/></t>
93 <t t-if="!record.parent_id.raw_value and record.function.raw_value"><field name="function"/></t>
94 <t t-if="record.parent_id.raw_value and record.function.raw_value"><field name="function"/> at <field name="parent_id"/></t>
95 </i>
96 <div><a t-if="record.email.raw_value" title="Mail" t-att-href="'mailto:'+record.email.value">
97 <field name="email"/>
98 </a></div>
99 <div t-if="record.phone.raw_value">Phone: <field name="phone"/></div>
100 <div t-if="record.mobile.raw_value">Mobile: <field name="mobile"/></div>
101 <div t-if="record.fax.raw_value">Fax: <field name="fax"/></div>
102 </td>
103 </tr>
104 </table>
105 </div>
106 </div>
107 </div>
108 </div>
109 </t>
110 </templates>
111 </kanban>
112 <form string="Contact" version="7.0">
113 <sheet>
114 <field name="image" widget='image' class="oe_avatar oe_left" options='{"preview_image": "image_medium"}'/>
115 <div class="oe_title">
116 <label for="name" class="oe_edit_only"/>
117 <h1><field name="name" style="width: 70%%"/></h1>
118 </div>
119 <group>
120 <!-- inherited part -->
121 <field name="category_id" widget="many2many_tags" placeholder="Tags..." style="width: 70%%"/>
122 <field name="parent_id" placeholder="Company" domain="[('is_company','=',True)]"/>
123 <!-- inherited part end -->
124 <field name="function" placeholder="e.g. Sales Director"/>
125 <field name="email"/>
126 <field name="phone"/>
127 <field name="mobile"/>
128 </group>
129 <div>
130 <field name="use_parent_address"/><label for="use_parent_address"/>
131 </div>
132 <group>
133 <label for="type"/>
134 <div name="div_type">
135 <field class="oe_inline" name="type"/>
136 </div>
137 <label for="street" string="Address" attrs="{'invisible': [('use_parent_address','=', True)]}"/>
138 <div attrs="{'invisible': [('use_parent_address','=', True)]}" name="div_address">
139 <field name="street" placeholder="Street..."/>
140 <field name="street2"/>
141 <div class="address_format">
142 <field name="city" placeholder="City" style="width: 40%%"/>
143 <field name="state_id" class="oe_no_button" placeholder="State" style="width: 37%%" options='{"no_open": True}' on_change="onchange_state(state_id)"/>
144 <field name="zip" placeholder="ZIP" style="width: 20%%"/>
145 </div>
146 <field name="country_id" placeholder="Country" class="oe_no_button" options='{"no_open": True}'/>
147 </div>
148 </group>
149 <field name="supplier" invisible="True"/>
150 </sheet>
151 </form>
152 </field>
153 </page>
154 <page name="personal-info" string="Personal Information" attrs="{'invisible': ['|',('is_company','=',True)]}">
155 <p attrs="{'invisible': [('contact_id','=',False)]}">
156 To see personal information about this contact, please go to to the his person form: <field name="contact_id" class="oe_inline" domain="[('contact_type','!=','attached')]" context="{'show_address': 1}"
157 on_change="onchange_contact_id(contact_id)" options="{'always_reload': True}"/>
158 </p>
159 <group attrs="{'invisible': [('contact_id','!=',False)]}">
160 <field name="birthdate_date"/>
161 <field name="nationality_id"/>
162 </group>
163 </page>
164 </page>
165 <xpath expr="//field[@name='child_ids']/form//field[@name='name']/.." position="before">
166 <field name="contact_type" readonly="0" on_change="onchange_contact_type(contact_type)"/>
167 </xpath>
168 <xpath expr="//field[@name='child_ids']/form//field[@name='name']" position="after">
169 <field name="contact_id" on_change="onchange_contact_id(contact_id)" string="Contact"
170 attrs="{'invisible': [('contact_type','!=','attached')], 'required': [('contact_type','=','attached')]}"/>
171 </xpath>
172 <xpath expr="//field[@name='child_ids']/form//field[@name='name']" position="attributes">
173 <attribute name="attrs">{'invisible': [('contact_type','=','attached')]}</attribute>
174 </xpath>
175 </field>
176 </record>
177
178 <record model="ir.ui.view" id="view_res_partner_kanban_contact">
179 <field name="name">res.partner.kanban.contact</field>
180 <field name="model">res.partner</field>
181 <field name="inherit_id" ref="base.res_partner_kanban_view"/>
182 <field name="arch" type="xml">
183 <field name="is_company" position="after">
184 <field name="other_contact_ids">
185 <tree>
186 <field name="parent_id"/>
187 <field name="function"/>
188 </tree>
189 </field>
190 </field>
191 <xpath expr="//t[@t-name='kanban-box']//div[@class='oe_kanban_details']/ul/li[3]" position="after">
192 <t t-if="record.other_contact_ids.raw_value.length &gt; 0">
193 <li>+<t t-esc="record.other_contact_ids.raw_value.length"/>
194 <t t-if="record.other_contact_ids.raw_value.length == 1">other position</t>
195 <t t-if="record.other_contact_ids.raw_value.length &gt; 1">other positions</t></li>
196 </t>
197 </xpath>
198 </field>
199 </record>
200
201</data>
202</openerp>
0203
=== added directory 'base_contact/images'
=== added directory 'base_contact/tests'
=== added file 'base_contact/tests/__init__.py'
--- base_contact/tests/__init__.py 1970-01-01 00:00:00 +0000
+++ base_contact/tests/__init__.py 2013-10-23 13:04:00 +0000
@@ -0,0 +1,26 @@
1# -*- coding: utf-8 ⁻*-
2##############################################################################
3#
4# OpenERP, Open Source Business Applications
5# Copyright (C) 2013-TODAY OpenERP S.A. (<http://openerp.com>).
6#
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU Affero General Public License as
9# published by the Free Software Foundation, either version 3 of the
10# License, or (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU Affero General Public License for more details.
16#
17# You should have received a copy of the GNU Affero General Public License
18# along with this program. If not, see <http://www.gnu.org/licenses/>.
19#
20##############################################################################
21
22from . import test_base_contact
23
24checks = [
25 test_base_contact,
26]
027
=== added file 'base_contact/tests/test_base_contact.py'
--- base_contact/tests/test_base_contact.py 1970-01-01 00:00:00 +0000
+++ base_contact/tests/test_base_contact.py 2013-10-23 13:04:00 +0000
@@ -0,0 +1,136 @@
1# -*- coding: utf-8 ⁻*-
2##############################################################################
3#
4# OpenERP, Open Source Business Applications
5# Copyright (C) 2013-TODAY OpenERP S.A. (<http://openerp.com>).
6#
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU Affero General Public License as
9# published by the Free Software Foundation, either version 3 of the
10# License, or (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU Affero General Public License for more details.
16#
17# You should have received a copy of the GNU Affero General Public License
18# along with this program. If not, see <http://www.gnu.org/licenses/>.
19#
20##############################################################################
21
22from openerp.tests import common
23
24
25class Test_Base_Contact(common.TransactionCase):
26
27 def setUp(self):
28 """*****setUp*****"""
29 super(Test_Base_Contact, self).setUp()
30 cr, uid = self.cr, self.uid
31 ModelData = self.registry('ir.model.data')
32 self.partner = self.registry('res.partner')
33
34 # Get test records reference
35 for attr, module, name in [
36 ('main_partner_id', 'base', 'main_partner'),
37 ('bob_contact_id', 'base_contact', 'res_partner_contact1'),
38 ('bob_job1_id', 'base_contact', 'res_partner_contact1_work_position1'),
39 ('roger_contact_id', 'base', 'res_partner_main2'),
40 ('roger_job2_id', 'base_contact', 'res_partner_main2_position_consultant')]:
41 r = ModelData.get_object_reference(cr, uid, module, name)
42 setattr(self, attr, r[1] if r else False)
43
44 def test_00_show_only_standalone_contact(self):
45 """Check that only standalone contact are shown if context explicitly state to not display all positions"""
46 cr, uid = self.cr, self.uid
47 ctx = {'search_show_all_positions': False}
48 partner_ids = self.partner.search(cr, uid, [], context=ctx)
49 partner_ids.sort()
50 self.assertTrue(self.bob_job1_id not in partner_ids)
51 self.assertTrue(self.roger_job2_id not in partner_ids)
52
53 def test_01_show_all_positions(self):
54 """Check that all contact are show if context is empty or explicitly state to display all positions"""
55 cr, uid = self.cr, self.uid
56
57 partner_ids = self.partner.search(cr, uid, [], context=None)
58 self.assertTrue(self.bob_job1_id in partner_ids)
59 self.assertTrue(self.roger_job2_id in partner_ids)
60
61 ctx = {'search_show_all_positions': True}
62 partner_ids = self.partner.search(cr, uid, [], context=ctx)
63 self.assertTrue(self.bob_job1_id in partner_ids)
64 self.assertTrue(self.roger_job2_id in partner_ids)
65
66 def test_02_reading_other_contact_one2many_show_all_positions(self):
67 """Check that readonly partner's ``other_contact_ids`` return all values whatever the context"""
68 cr, uid = self.cr, self.uid
69
70 def read_other_contacts(pid, context=None):
71 return self.partner.read(cr, uid, [pid], ['other_contact_ids'], context=context)[0]['other_contact_ids']
72
73 def read_contacts(pid, context=None):
74 return self.partner.read(cr, uid, [pid], ['child_ids'], context=context)[0]['child_ids']
75
76 ctx = None
77 self.assertEqual(read_other_contacts(self.bob_contact_id, context=ctx), [self.bob_job1_id])
78 ctx = {'search_show_all_positions': False}
79 self.assertEqual(read_other_contacts(self.bob_contact_id, context=ctx), [self.bob_job1_id])
80 ctx = {'search_show_all_positions': True}
81 self.assertEqual(read_other_contacts(self.bob_contact_id, context=ctx), [self.bob_job1_id])
82
83 ctx = None
84 self.assertTrue(self.bob_job1_id in read_contacts(self.main_partner_id, context=ctx))
85 ctx = {'search_show_all_positions': False}
86 self.assertTrue(self.bob_job1_id in read_contacts(self.main_partner_id, context=ctx))
87 ctx = {'search_show_all_positions': True}
88 self.assertTrue(self.bob_job1_id in read_contacts(self.main_partner_id, context=ctx))
89
90 def test_03_search_match_attached_contacts(self):
91 """Check that searching partner also return partners having attached contacts matching search criteria"""
92 cr, uid = self.cr, self.uid
93 # Bob's contact has one other position which is related to 'Your Company'
94 # so search for all contacts working for 'Your Company' should contain bob position.
95 partner_ids = self.partner.search(cr, uid, [('parent_id', 'ilike', 'Your Company')], context=None)
96 self.assertTrue(self.bob_job1_id in partner_ids)
97
98 # but when searching without 'all positions', we should get the position standalone contact instead.
99 ctx = {'search_show_all_positions': False}
100 partner_ids = self.partner.search(cr, uid, [('parent_id', 'ilike', 'Your Company')], context=ctx)
101 self.assertTrue(self.bob_contact_id in partner_ids)
102
103 def test_04_contact_creation(self):
104 """Check that we're begin to create a contact"""
105 cr, uid = self.cr, self.uid
106
107 # Create a contact using only name
108 new_contact_id = self.partner.create(cr, uid, {'name': 'Bob Egnops'})
109 self.assertEqual(self.partner.browse(cr, uid, new_contact_id).contact_type, 'standalone')
110
111 # Create a contact with only contact_id
112 new_contact_id = self.partner.create(cr, uid, {'contact_id': self.bob_contact_id})
113 new_contact = self.partner.browse(cr, uid, new_contact_id)
114 self.assertEqual(new_contact.name, 'Bob Egnops')
115 self.assertEqual(new_contact.contact_type, 'attached')
116
117 # Create a contact with both contact_id and name;
118 # contact's name should override provided value in that case
119 new_contact_id = self.partner.create(cr, uid, {'contact_id': self.bob_contact_id, 'name': 'Rob Egnops'})
120 self.assertEqual(self.partner.browse(cr, uid, new_contact_id).name, 'Bob Egnops')
121
122 # Reset contact to standalone
123 self.partner.write(cr, uid, [new_contact_id], {'contact_id': False})
124 self.assertEqual(self.partner.browse(cr, uid, new_contact_id).contact_type, 'standalone')
125
126 def test_05_contact_fields_sync(self):
127 """Check that contact's fields are correctly synced between parent contact or related contacts"""
128 cr, uid = self.cr, self.uid
129
130 # Test DOWNSTREAM sync
131 self.partner.write(cr, uid, [self.bob_contact_id], {'name': 'Rob Egnops'})
132 self.assertEqual(self.partner.browse(cr, uid, self.bob_job1_id).name, 'Rob Egnops')
133
134 # Test UPSTREAM sync
135 self.partner.write(cr, uid, [self.bob_job1_id], {'name': 'Bob Egnops'})
136 self.assertEqual(self.partner.browse(cr, uid, self.bob_contact_id).name, 'Bob Egnops')