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

Proposed by Martin Trigaux (OpenERP) on 2013-10-22
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 on 2013-11-18
OpenERP Community Reviewer/Maintainer 2013-11-26 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.

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 on 2013-10-23

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

9536. By Xavier ALT on 2013-10-23

[FIX] base_contact: missing .name

9537. By Xavier ALT on 2013-10-23

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

Xavier ALT (xal-openerp) 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".

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

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

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!

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).

Fabrice (OpenERP) (fhe) wrote :

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

@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.

What is the current status of this MP?

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?

Unmerged revisions

9537. By Xavier ALT on 2013-10-23

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

9536. By Xavier ALT on 2013-10-23

[FIX] base_contact: missing .name

9535. By Xavier ALT on 2013-10-23

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

9534. By Xavier ALT on 2013-10-21

[+] base_contact module

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added directory 'base_contact'
2=== added file 'base_contact/__init__.py'
3--- base_contact/__init__.py 1970-01-01 00:00:00 +0000
4+++ base_contact/__init__.py 2013-10-23 13:04:00 +0000
5@@ -0,0 +1,23 @@
6+# -*- coding: utf-8 -*-
7+##############################################################################
8+#
9+# OpenERP, Open Source Management Solution
10+# Copyright (C) 2013-TODAY OpenERP SA (<http://www.openerp.com>).
11+#
12+# This program is free software: you can redistribute it and/or modify
13+# it under the terms of the GNU Affero General Public License as
14+# published by the Free Software Foundation, either version 3 of the
15+# License, or (at your option) any later version.
16+#
17+# This program is distributed in the hope that it will be useful,
18+# but WITHOUT ANY WARRANTY; without even the implied warranty of
19+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20+# GNU Affero General Public License for more details.
21+#
22+# You should have received a copy of the GNU Affero General Public License
23+# along with this program. If not, see <http://www.gnu.org/licenses/>.
24+#
25+##############################################################################
26+
27+import base_contact
28+
29
30=== added file 'base_contact/__openerp__.py'
31--- base_contact/__openerp__.py 1970-01-01 00:00:00 +0000
32+++ base_contact/__openerp__.py 2013-10-23 13:04:00 +0000
33@@ -0,0 +1,52 @@
34+# -*- coding: utf-8 -*-
35+##############################################################################
36+#
37+# OpenERP, Open Source Business Applications
38+# Copyright (C) 2013-TODAY OpenERP S.A. (<http://openerp.com>).
39+#
40+# This program is free software: you can redistribute it and/or modify
41+# it under the terms of the GNU Affero General Public License as
42+# published by the Free Software Foundation, either version 3 of the
43+# License, or (at your option) any later version.
44+#
45+# This program is distributed in the hope that it will be useful,
46+# but WITHOUT ANY WARRANTY; without even the implied warranty of
47+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
48+# GNU Affero General Public License for more details.
49+#
50+# You should have received a copy of the GNU Affero General Public License
51+# along with this program. If not, see <http://www.gnu.org/licenses/>.
52+#
53+##############################################################################
54+
55+{
56+ 'name': 'Contacts Management',
57+ 'version': '1.0',
58+ 'category': 'Customer Relationship Management',
59+ 'complexity': "expert",
60+ 'description': """
61+This module allows you to manage your contacts
62+==============================================
63+
64+It lets you define groups of contacts sharing some common information, like:
65+ * Birthdate
66+ * Nationality
67+ * Native Language
68+
69+ """,
70+ 'author': 'OpenERP SA',
71+ 'website': 'http://www.openerp.com',
72+ 'depends': ['base', 'process', 'contacts'],
73+ 'init_xml': [],
74+ 'update_xml': [
75+ 'base_contact_view.xml',
76+ ],
77+ 'demo_xml': [
78+ 'base_contact_demo.xml',
79+ ],
80+ 'installable': True,
81+ 'auto_install': False,
82+ #'certificate': '0031287885469',
83+ 'images': [],
84+}
85+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
86
87=== added file 'base_contact/base_contact.py'
88--- base_contact/base_contact.py 1970-01-01 00:00:00 +0000
89+++ base_contact/base_contact.py 2013-10-23 13:04:00 +0000
90@@ -0,0 +1,185 @@
91+# -*- coding: utf-8 -*-
92+##############################################################################
93+#
94+# OpenERP, Open Source Management Solution
95+# Copyright (C) 2013-TODAY OpenERP SA (<http://www.openerp.com>).
96+#
97+# This program is free software: you can redistribute it and/or modify
98+# it under the terms of the GNU Affero General Public License as
99+# published by the Free Software Foundation, either version 3 of the
100+# License, or (at your option) any later version.
101+#
102+# This program is distributed in the hope that it will be useful,
103+# but WITHOUT ANY WARRANTY; without even the implied warranty of
104+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
105+# GNU Affero General Public License for more details.
106+#
107+# You should have received a copy of the GNU Affero General Public License
108+# along with this program. If not, see <http://www.gnu.org/licenses/>.
109+#
110+##############################################################################
111+
112+from openerp.osv import fields, osv, expression
113+
114+
115+class res_partner(osv.osv):
116+ _inherit = 'res.partner'
117+
118+ _contact_type = [
119+ ('standalone', 'Standalone Contact'),
120+ ('attached', 'Attached to existing Contact'),
121+ ]
122+
123+ def _get_contact_type(self, cr, uid, ids, field_name, args, context=None):
124+ result = dict.fromkeys(ids, 'standalone')
125+ for partner in self.browse(cr, uid, ids, context=context):
126+ if partner.contact_id:
127+ result[partner.id] = 'attached'
128+ return result
129+
130+ _columns = {
131+ 'contact_type': fields.function(_get_contact_type, type='selection', selection=_contact_type,
132+ string='Contact Type', required=True, select=1, store=True),
133+ 'contact_id': fields.many2one('res.partner', 'Main Contact',
134+ domain=[('is_company','=',False),('contact_type','=','standalone')]),
135+ 'other_contact_ids': fields.one2many('res.partner', 'contact_id', 'Others Positions'),
136+
137+ # Person specific fields
138+ 'birthdate_date': fields.date('Birthdate'), # add a 'birthdate' as date field, i.e different from char 'birthdate' introduced v6.1!
139+ 'nationality_id': fields.many2one('res.country', 'Nationality'),
140+ }
141+
142+ _defaults = {
143+ 'contact_type': 'standalone',
144+ }
145+
146+ def _basecontact_check_context(self, cr, user, mode, context=None):
147+ if context is None:
148+ context = {}
149+ # Remove 'search_show_all_positions' for non-search mode.
150+ # Keeping it in context can result in unexpected behaviour (ex: reading
151+ # one2many might return wrong result - i.e with "attached contact" removed
152+ # even if it's directly linked to a company).
153+ if mode != 'search':
154+ context.pop('search_show_all_positions', None)
155+ return context
156+
157+ def search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False):
158+ if context is None:
159+ context = {}
160+ if context.get('search_show_all_positions') is False:
161+ # display only standalone contact matching ``args`` or having
162+ # attached contact matching ``args``
163+ args = expression.normalize_domain(args)
164+ attached_contact_args = expression.AND((args, [('contact_type', '=', 'attached')]))
165+ attached_contact_ids = super(res_partner, self).search(cr, user, attached_contact_args,
166+ context=context)
167+ args = expression.OR((
168+ expression.AND(([('contact_type', '=', 'standalone')], args)),
169+ [('other_contact_ids', 'in', attached_contact_ids)],
170+ ))
171+ return super(res_partner, self).search(cr, user, args, offset=offset, limit=limit,
172+ order=order, context=context, count=count)
173+
174+ def create(self, cr, user, vals, context=None):
175+ context = self._basecontact_check_context(cr, user, 'create', context)
176+ if not vals.get('name') and vals.get('contact_id'):
177+ vals['name'] = self.browse(cr, user, vals['contact_id'], context=context).name
178+ return super(res_partner, self).create(cr, user, vals, context=context)
179+
180+ def read(self, cr, user, ids, fields=None, context=None, load='_classic_read'):
181+ context = self._basecontact_check_context(cr, user, 'read', context)
182+ return super(res_partner, self).read(cr, user, ids, fields=fields, context=context, load=load)
183+
184+ def write(self, cr, user, ids, vals, context=None):
185+ context = self._basecontact_check_context(cr, user, 'write', context)
186+ return super(res_partner, self).write(cr, user, ids, vals, context=context)
187+
188+ def unlink(self, cr, user, ids, context=None):
189+ context = self._basecontact_check_context(cr, user, 'unlink', context)
190+ return super(res_partner, self).unlink(cr, user, ids, context=context)
191+
192+ def _commercial_partner_compute(self, cr, uid, ids, name, args, context=None):
193+ """ Returns the partner that is considered the commercial
194+ entity of this partner. The commercial entity holds the master data
195+ for all commercial fields (see :py:meth:`~_commercial_fields`) """
196+ result = super(res_partner, self)._commercial_partner_compute(cr, uid, ids, name, args, context=context)
197+ for partner in self.browse(cr, uid, ids, context=context):
198+ if partner.contact_type == 'attached' and not partner.parent_id:
199+ result[partner.id] = partner.contact_id.id
200+ return result
201+
202+ def _contact_fields(self, cr, uid, context=None):
203+ """ Returns the list of contact fields that are synced from the parent
204+ when a partner is attached to him. """
205+ return ['name', 'title']
206+
207+ def _contact_sync_from_parent(self, cr, uid, partner, context=None):
208+ """ Handle sync of contact fields when a new parent contact entity is set,
209+ as if they were related fields """
210+ if partner.contact_id:
211+ contact_fields = self._contact_fields(cr, uid, context=context)
212+ sync_vals = self._update_fields_values(cr, uid, partner.contact_id,
213+ contact_fields, context=context)
214+ partner.write(sync_vals)
215+
216+ def update_contact(self, cr, uid, ids, vals, context=None):
217+ if context is None:
218+ context = {}
219+ if context.get('__update_contact_lock'):
220+ return
221+ contact_fields = self._contact_fields(cr, uid, context=context)
222+ contact_vals = dict((field, vals[field]) for field in contact_fields if field in vals)
223+ if contact_vals:
224+ ctx = dict(context, __update_contact_lock=True)
225+ self.write(cr, uid, ids, contact_vals, context=ctx)
226+
227+ def _fields_sync(self, cr, uid, partner, update_values, context=None):
228+ """ Sync commercial fields and address fields from company and to children,
229+ contact fields from contact and to attached contact after create/update,
230+ just as if those were all modeled as fields.related to the parent """
231+ super(res_partner, self)._fields_sync(cr, uid, partner, update_values, context=context)
232+ contact_fields = self._contact_fields(cr, uid, context=context)
233+ # 1. From UPSTREAM: sync from parent contact
234+ if update_values.get('contact_id'):
235+ self._contact_sync_from_parent(cr, uid, partner, context=context)
236+ # 2. To DOWNSTREAM: sync contact fields to parent or related
237+ elif any(field in contact_fields for field in update_values):
238+ update_ids = [c.id for c in partner.other_contact_ids if not c.is_company]
239+ if partner.contact_id:
240+ update_ids.append(partner.contact_id.id)
241+ self.update_contact(cr, uid, update_ids, update_values, context=context)
242+
243+ def onchange_contact_id(self, cr, uid, ids, contact_id, context=None):
244+ values = {}
245+ if contact_id:
246+ values['name'] = self.browse(cr, uid, contact_id, context=context).name
247+ return {'value': values}
248+
249+ def onchange_contact_type(self, cr, uid, ids, contact_type, context=None):
250+ values = {}
251+ if contact_type == 'standalone':
252+ values['contact_id'] = False
253+ return {'value': values}
254+
255+
256+class ir_actions_window(osv.osv):
257+ _inherit = 'ir.actions.act_window'
258+
259+ def read(self, cr, user, ids, fields=None, context=None, load='_classic_read'):
260+ action_ids = ids
261+ if isinstance(ids, (int, long)):
262+ action_ids = [ids]
263+ actions = super(ir_actions_window, self).read(cr, user, action_ids, fields=fields, context=context, load=load)
264+ for action in actions:
265+ if action.get('res_model', '') == 'res.partner':
266+ # By default, only show standalone contact
267+ action_context = action.get('context', '{}') or '{}'
268+ if 'search_show_all_positions' not in action_context:
269+ action['context'] = action_context.replace('{',
270+ "{'search_show_all_positions': False,", 1)
271+ if isinstance(ids, (int, long)):
272+ if actions:
273+ return actions[0]
274+ return False
275+ return actions
276
277=== added file 'base_contact/base_contact_demo.xml'
278--- base_contact/base_contact_demo.xml 1970-01-01 00:00:00 +0000
279+++ base_contact/base_contact_demo.xml 2013-10-23 13:04:00 +0000
280@@ -0,0 +1,29 @@
281+<?xml version="1.0" encoding="UTF-8"?>
282+<openerp>
283+ <data>
284+
285+ <record id="res_partner_main2_position_consultant" model="res.partner">
286+ <field name="name">Roger Scott</field>
287+ <field name="function">Consultant</field>
288+ <field name="parent_id" ref="base.res_partner_11"/>
289+ <field name="contact_id" ref="base.res_partner_main2"/>
290+ <field name="use_parent_address" eval="True"/>
291+ </record>
292+
293+ <record id="res_partner_contact1" model="res.partner">
294+ <field name="name">Bob Egnops</field>
295+ <field name="birthdate_date">1984-01-01</field>
296+ <field name="email">bob@hillenburg-oceaninstitute.com</field>
297+ </record>
298+
299+ <record id="res_partner_contact1_work_position1" model="res.partner">
300+ <field name="name">Bob Egnops</field>
301+ <field name="function">Technician</field>
302+ <field name="email">bob@yourcompany.com</field>
303+ <field name="parent_id" ref="base.main_partner"/>
304+ <field name="contact_id" ref="res_partner_contact1"/>
305+ <field name="use_parent_address" eval="True"/>
306+ </record>
307+
308+ </data>
309+</openerp>
310\ No newline at end of file
311
312=== added file 'base_contact/base_contact_view.xml'
313--- base_contact/base_contact_view.xml 1970-01-01 00:00:00 +0000
314+++ base_contact/base_contact_view.xml 2013-10-23 13:04:00 +0000
315@@ -0,0 +1,202 @@
316+<?xml version="1.0" encoding="utf-8"?>
317+<openerp>
318+<data>
319+
320+ <record id="view_res_partner_filter_contact" model="ir.ui.view">
321+ <field name="name">res.partner.select.contact</field>
322+ <field name="model">res.partner</field>
323+ <field name="inherit_id" ref="base.view_res_partner_filter"/>
324+ <field name="arch" type="xml">
325+ <filter name="type_company" position="after">
326+ <separator/>
327+ <filter string="All positions" name="type_otherpositions"
328+ context="{'search_show_all_positions': True}"
329+ help="All partner positions"/>
330+ </filter>
331+ <xpath expr="/search/group/filter[@string='Company']" position="before">
332+ <filter string="Person" name="group_person" context="{'group_by': 'contact_id'}"/>
333+ </xpath>
334+ </field>
335+ </record>
336+
337+ <record id="view_res_partner_tree_contact" model="ir.ui.view">
338+ <field name="name">res.partner.tree.contact</field>
339+ <field name="model">res.partner</field>
340+ <field name="inherit_id" ref="base.view_partner_tree"/>
341+ <field name="arch" type="xml">
342+ <field name="parent_id" position="after">
343+ <field name="contact_id" invisible="1"/>
344+ </field>
345+ </field>
346+ </record>
347+
348+ <record model="ir.ui.view" id="view_partner_form_inherit">
349+ <field name="name">res.partner.form.contact</field>
350+ <field name="model">res.partner</field>
351+ <field name="inherit_id" ref="base.view_partner_form"/>
352+ <field name="type">form</field>
353+ <field name="arch" type="xml">
354+ <field name="is_company" position="after">
355+ <field name="contact_type" invisible="1"/>
356+ </field>
357+ <page string="Contacts" position="after">
358+ <page string="Other Positions" attrs="{'invisible': ['|',('is_company','=',True),('contact_id','!=',False)]}">
359+ <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">
360+ <kanban>
361+ <field name="color"/>
362+ <field name="name"/>
363+ <field name="title"/>
364+ <field name="email"/>
365+ <field name="parent_id"/>
366+ <field name="is_company"/>
367+ <field name="function"/>
368+ <field name="phone"/>
369+ <field name="street"/>
370+ <field name="street2"/>
371+ <field name="zip"/>
372+ <field name="city"/>
373+ <field name="country_id"/>
374+ <field name="mobile"/>
375+ <field name="fax"/>
376+ <field name="state_id"/>
377+ <field name="has_image"/>
378+ <templates>
379+ <t t-name="kanban-box">
380+ <t t-set="color" t-value="kanban_color(record.color.raw_value)"/>
381+ <div t-att-class="color + (record.title.raw_value == 1 ? ' oe_kanban_color_alert' : '')" style="position: relative">
382+ <a t-if="! read_only_mode" type="delete" style="position: absolute; right: 0; padding: 4px; diplay: inline-block">X</a>
383+ <div class="oe_module_vignette">
384+ <a type="open">
385+ <t t-if="record.has_image.raw_value === true">
386+ <img t-att-src="kanban_image('res.partner', 'image', record.id.value, {'preview_image': 'image_small'})" class="oe_avatar oe_kanban_avatar_smallbox"/>
387+ </t>
388+ <t t-if="record.image and record.image.raw_value !== false">
389+ <img t-att-src="'data:image/png;base64,'+record.image.raw_value" class="oe_avatar oe_kanban_avatar_smallbox"/>
390+ </t>
391+ <t t-if="record.has_image.raw_value === false and (!record.image or record.image.raw_value === false)">
392+ <t t-if="record.is_company.raw_value === true">
393+ <img t-att-src='_s + "/base/static/src/img/company_image.png"' class="oe_kanban_image oe_kanban_avatar_smallbox"/>
394+ </t>
395+ <t t-if="record.is_company.raw_value === false">
396+ <img t-att-src='_s + "/base/static/src/img/avatar.png"' class="oe_kanban_image oe_kanban_avatar_smallbox"/>
397+ </t>
398+ </t>
399+ </a>
400+ <div class="oe_module_desc">
401+ <div class="oe_kanban_box_content oe_kanban_color_bglight oe_kanban_color_border">
402+ <table class="oe_kanban_table">
403+ <tr>
404+ <td class="oe_kanban_title1" align="left" valign="middle">
405+ <h4><a type="open"><field name="name"/></a></h4>
406+ <i>
407+ <t t-if="record.parent_id.raw_value and !record.function.raw_value"><field name="parent_id"/></t>
408+ <t t-if="!record.parent_id.raw_value and record.function.raw_value"><field name="function"/></t>
409+ <t t-if="record.parent_id.raw_value and record.function.raw_value"><field name="function"/> at <field name="parent_id"/></t>
410+ </i>
411+ <div><a t-if="record.email.raw_value" title="Mail" t-att-href="'mailto:'+record.email.value">
412+ <field name="email"/>
413+ </a></div>
414+ <div t-if="record.phone.raw_value">Phone: <field name="phone"/></div>
415+ <div t-if="record.mobile.raw_value">Mobile: <field name="mobile"/></div>
416+ <div t-if="record.fax.raw_value">Fax: <field name="fax"/></div>
417+ </td>
418+ </tr>
419+ </table>
420+ </div>
421+ </div>
422+ </div>
423+ </div>
424+ </t>
425+ </templates>
426+ </kanban>
427+ <form string="Contact" version="7.0">
428+ <sheet>
429+ <field name="image" widget='image' class="oe_avatar oe_left" options='{"preview_image": "image_medium"}'/>
430+ <div class="oe_title">
431+ <label for="name" class="oe_edit_only"/>
432+ <h1><field name="name" style="width: 70%%"/></h1>
433+ </div>
434+ <group>
435+ <!-- inherited part -->
436+ <field name="category_id" widget="many2many_tags" placeholder="Tags..." style="width: 70%%"/>
437+ <field name="parent_id" placeholder="Company" domain="[('is_company','=',True)]"/>
438+ <!-- inherited part end -->
439+ <field name="function" placeholder="e.g. Sales Director"/>
440+ <field name="email"/>
441+ <field name="phone"/>
442+ <field name="mobile"/>
443+ </group>
444+ <div>
445+ <field name="use_parent_address"/><label for="use_parent_address"/>
446+ </div>
447+ <group>
448+ <label for="type"/>
449+ <div name="div_type">
450+ <field class="oe_inline" name="type"/>
451+ </div>
452+ <label for="street" string="Address" attrs="{'invisible': [('use_parent_address','=', True)]}"/>
453+ <div attrs="{'invisible': [('use_parent_address','=', True)]}" name="div_address">
454+ <field name="street" placeholder="Street..."/>
455+ <field name="street2"/>
456+ <div class="address_format">
457+ <field name="city" placeholder="City" style="width: 40%%"/>
458+ <field name="state_id" class="oe_no_button" placeholder="State" style="width: 37%%" options='{"no_open": True}' on_change="onchange_state(state_id)"/>
459+ <field name="zip" placeholder="ZIP" style="width: 20%%"/>
460+ </div>
461+ <field name="country_id" placeholder="Country" class="oe_no_button" options='{"no_open": True}'/>
462+ </div>
463+ </group>
464+ <field name="supplier" invisible="True"/>
465+ </sheet>
466+ </form>
467+ </field>
468+ </page>
469+ <page name="personal-info" string="Personal Information" attrs="{'invisible': ['|',('is_company','=',True)]}">
470+ <p attrs="{'invisible': [('contact_id','=',False)]}">
471+ 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}"
472+ on_change="onchange_contact_id(contact_id)" options="{'always_reload': True}"/>
473+ </p>
474+ <group attrs="{'invisible': [('contact_id','!=',False)]}">
475+ <field name="birthdate_date"/>
476+ <field name="nationality_id"/>
477+ </group>
478+ </page>
479+ </page>
480+ <xpath expr="//field[@name='child_ids']/form//field[@name='name']/.." position="before">
481+ <field name="contact_type" readonly="0" on_change="onchange_contact_type(contact_type)"/>
482+ </xpath>
483+ <xpath expr="//field[@name='child_ids']/form//field[@name='name']" position="after">
484+ <field name="contact_id" on_change="onchange_contact_id(contact_id)" string="Contact"
485+ attrs="{'invisible': [('contact_type','!=','attached')], 'required': [('contact_type','=','attached')]}"/>
486+ </xpath>
487+ <xpath expr="//field[@name='child_ids']/form//field[@name='name']" position="attributes">
488+ <attribute name="attrs">{'invisible': [('contact_type','=','attached')]}</attribute>
489+ </xpath>
490+ </field>
491+ </record>
492+
493+ <record model="ir.ui.view" id="view_res_partner_kanban_contact">
494+ <field name="name">res.partner.kanban.contact</field>
495+ <field name="model">res.partner</field>
496+ <field name="inherit_id" ref="base.res_partner_kanban_view"/>
497+ <field name="arch" type="xml">
498+ <field name="is_company" position="after">
499+ <field name="other_contact_ids">
500+ <tree>
501+ <field name="parent_id"/>
502+ <field name="function"/>
503+ </tree>
504+ </field>
505+ </field>
506+ <xpath expr="//t[@t-name='kanban-box']//div[@class='oe_kanban_details']/ul/li[3]" position="after">
507+ <t t-if="record.other_contact_ids.raw_value.length &gt; 0">
508+ <li>+<t t-esc="record.other_contact_ids.raw_value.length"/>
509+ <t t-if="record.other_contact_ids.raw_value.length == 1">other position</t>
510+ <t t-if="record.other_contact_ids.raw_value.length &gt; 1">other positions</t></li>
511+ </t>
512+ </xpath>
513+ </field>
514+ </record>
515+
516+</data>
517+</openerp>
518
519=== added directory 'base_contact/images'
520=== added directory 'base_contact/tests'
521=== added file 'base_contact/tests/__init__.py'
522--- base_contact/tests/__init__.py 1970-01-01 00:00:00 +0000
523+++ base_contact/tests/__init__.py 2013-10-23 13:04:00 +0000
524@@ -0,0 +1,26 @@
525+# -*- coding: utf-8 ⁻*-
526+##############################################################################
527+#
528+# OpenERP, Open Source Business Applications
529+# Copyright (C) 2013-TODAY OpenERP S.A. (<http://openerp.com>).
530+#
531+# This program is free software: you can redistribute it and/or modify
532+# it under the terms of the GNU Affero General Public License as
533+# published by the Free Software Foundation, either version 3 of the
534+# License, or (at your option) any later version.
535+#
536+# This program is distributed in the hope that it will be useful,
537+# but WITHOUT ANY WARRANTY; without even the implied warranty of
538+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
539+# GNU Affero General Public License for more details.
540+#
541+# You should have received a copy of the GNU Affero General Public License
542+# along with this program. If not, see <http://www.gnu.org/licenses/>.
543+#
544+##############################################################################
545+
546+from . import test_base_contact
547+
548+checks = [
549+ test_base_contact,
550+]
551
552=== added file 'base_contact/tests/test_base_contact.py'
553--- base_contact/tests/test_base_contact.py 1970-01-01 00:00:00 +0000
554+++ base_contact/tests/test_base_contact.py 2013-10-23 13:04:00 +0000
555@@ -0,0 +1,136 @@
556+# -*- coding: utf-8 ⁻*-
557+##############################################################################
558+#
559+# OpenERP, Open Source Business Applications
560+# Copyright (C) 2013-TODAY OpenERP S.A. (<http://openerp.com>).
561+#
562+# This program is free software: you can redistribute it and/or modify
563+# it under the terms of the GNU Affero General Public License as
564+# published by the Free Software Foundation, either version 3 of the
565+# License, or (at your option) any later version.
566+#
567+# This program is distributed in the hope that it will be useful,
568+# but WITHOUT ANY WARRANTY; without even the implied warranty of
569+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
570+# GNU Affero General Public License for more details.
571+#
572+# You should have received a copy of the GNU Affero General Public License
573+# along with this program. If not, see <http://www.gnu.org/licenses/>.
574+#
575+##############################################################################
576+
577+from openerp.tests import common
578+
579+
580+class Test_Base_Contact(common.TransactionCase):
581+
582+ def setUp(self):
583+ """*****setUp*****"""
584+ super(Test_Base_Contact, self).setUp()
585+ cr, uid = self.cr, self.uid
586+ ModelData = self.registry('ir.model.data')
587+ self.partner = self.registry('res.partner')
588+
589+ # Get test records reference
590+ for attr, module, name in [
591+ ('main_partner_id', 'base', 'main_partner'),
592+ ('bob_contact_id', 'base_contact', 'res_partner_contact1'),
593+ ('bob_job1_id', 'base_contact', 'res_partner_contact1_work_position1'),
594+ ('roger_contact_id', 'base', 'res_partner_main2'),
595+ ('roger_job2_id', 'base_contact', 'res_partner_main2_position_consultant')]:
596+ r = ModelData.get_object_reference(cr, uid, module, name)
597+ setattr(self, attr, r[1] if r else False)
598+
599+ def test_00_show_only_standalone_contact(self):
600+ """Check that only standalone contact are shown if context explicitly state to not display all positions"""
601+ cr, uid = self.cr, self.uid
602+ ctx = {'search_show_all_positions': False}
603+ partner_ids = self.partner.search(cr, uid, [], context=ctx)
604+ partner_ids.sort()
605+ self.assertTrue(self.bob_job1_id not in partner_ids)
606+ self.assertTrue(self.roger_job2_id not in partner_ids)
607+
608+ def test_01_show_all_positions(self):
609+ """Check that all contact are show if context is empty or explicitly state to display all positions"""
610+ cr, uid = self.cr, self.uid
611+
612+ partner_ids = self.partner.search(cr, uid, [], context=None)
613+ self.assertTrue(self.bob_job1_id in partner_ids)
614+ self.assertTrue(self.roger_job2_id in partner_ids)
615+
616+ ctx = {'search_show_all_positions': True}
617+ partner_ids = self.partner.search(cr, uid, [], context=ctx)
618+ self.assertTrue(self.bob_job1_id in partner_ids)
619+ self.assertTrue(self.roger_job2_id in partner_ids)
620+
621+ def test_02_reading_other_contact_one2many_show_all_positions(self):
622+ """Check that readonly partner's ``other_contact_ids`` return all values whatever the context"""
623+ cr, uid = self.cr, self.uid
624+
625+ def read_other_contacts(pid, context=None):
626+ return self.partner.read(cr, uid, [pid], ['other_contact_ids'], context=context)[0]['other_contact_ids']
627+
628+ def read_contacts(pid, context=None):
629+ return self.partner.read(cr, uid, [pid], ['child_ids'], context=context)[0]['child_ids']
630+
631+ ctx = None
632+ self.assertEqual(read_other_contacts(self.bob_contact_id, context=ctx), [self.bob_job1_id])
633+ ctx = {'search_show_all_positions': False}
634+ self.assertEqual(read_other_contacts(self.bob_contact_id, context=ctx), [self.bob_job1_id])
635+ ctx = {'search_show_all_positions': True}
636+ self.assertEqual(read_other_contacts(self.bob_contact_id, context=ctx), [self.bob_job1_id])
637+
638+ ctx = None
639+ self.assertTrue(self.bob_job1_id in read_contacts(self.main_partner_id, context=ctx))
640+ ctx = {'search_show_all_positions': False}
641+ self.assertTrue(self.bob_job1_id in read_contacts(self.main_partner_id, context=ctx))
642+ ctx = {'search_show_all_positions': True}
643+ self.assertTrue(self.bob_job1_id in read_contacts(self.main_partner_id, context=ctx))
644+
645+ def test_03_search_match_attached_contacts(self):
646+ """Check that searching partner also return partners having attached contacts matching search criteria"""
647+ cr, uid = self.cr, self.uid
648+ # Bob's contact has one other position which is related to 'Your Company'
649+ # so search for all contacts working for 'Your Company' should contain bob position.
650+ partner_ids = self.partner.search(cr, uid, [('parent_id', 'ilike', 'Your Company')], context=None)
651+ self.assertTrue(self.bob_job1_id in partner_ids)
652+
653+ # but when searching without 'all positions', we should get the position standalone contact instead.
654+ ctx = {'search_show_all_positions': False}
655+ partner_ids = self.partner.search(cr, uid, [('parent_id', 'ilike', 'Your Company')], context=ctx)
656+ self.assertTrue(self.bob_contact_id in partner_ids)
657+
658+ def test_04_contact_creation(self):
659+ """Check that we're begin to create a contact"""
660+ cr, uid = self.cr, self.uid
661+
662+ # Create a contact using only name
663+ new_contact_id = self.partner.create(cr, uid, {'name': 'Bob Egnops'})
664+ self.assertEqual(self.partner.browse(cr, uid, new_contact_id).contact_type, 'standalone')
665+
666+ # Create a contact with only contact_id
667+ new_contact_id = self.partner.create(cr, uid, {'contact_id': self.bob_contact_id})
668+ new_contact = self.partner.browse(cr, uid, new_contact_id)
669+ self.assertEqual(new_contact.name, 'Bob Egnops')
670+ self.assertEqual(new_contact.contact_type, 'attached')
671+
672+ # Create a contact with both contact_id and name;
673+ # contact's name should override provided value in that case
674+ new_contact_id = self.partner.create(cr, uid, {'contact_id': self.bob_contact_id, 'name': 'Rob Egnops'})
675+ self.assertEqual(self.partner.browse(cr, uid, new_contact_id).name, 'Bob Egnops')
676+
677+ # Reset contact to standalone
678+ self.partner.write(cr, uid, [new_contact_id], {'contact_id': False})
679+ self.assertEqual(self.partner.browse(cr, uid, new_contact_id).contact_type, 'standalone')
680+
681+ def test_05_contact_fields_sync(self):
682+ """Check that contact's fields are correctly synced between parent contact or related contacts"""
683+ cr, uid = self.cr, self.uid
684+
685+ # Test DOWNSTREAM sync
686+ self.partner.write(cr, uid, [self.bob_contact_id], {'name': 'Rob Egnops'})
687+ self.assertEqual(self.partner.browse(cr, uid, self.bob_job1_id).name, 'Rob Egnops')
688+
689+ # Test UPSTREAM sync
690+ self.partner.write(cr, uid, [self.bob_job1_id], {'name': 'Bob Egnops'})
691+ self.assertEqual(self.partner.browse(cr, uid, self.bob_contact_id).name, 'Bob Egnops')