Merge lp:~savoirfairelinux-openerp/openobject-addons/base_contact into lp:openobject-addons/7.0

Status: Needs review
Proposed branch: lp:~savoirfairelinux-openerp/openobject-addons/base_contact
Merge into: lp:openobject-addons/7.0
Diff against target: 1079 lines (+1030/-0)
9 files modified
base_contact/__init__.py (+22/-0)
base_contact/__openerp__.py (+52/-0)
base_contact/base_contact.py (+193/-0)
base_contact/base_contact_demo.xml (+29/-0)
base_contact/base_contact_view.xml (+202/-0)
base_contact/i18n/base_contact.pot (+184/-0)
base_contact/i18n/fr.po (+186/-0)
base_contact/tests/__init__.py (+26/-0)
base_contact/tests/test_base_contact.py (+136/-0)
To merge this branch: bzr merge lp:~savoirfairelinux-openerp/openobject-addons/base_contact
Reviewer Review Type Date Requested Status
OpenERP Core Team Pending
Review via email: mp+198634@code.launchpad.net

Description of the change

[ADD] add base_contact and pep8

To post a comment you must log in.

Unmerged revisions

9539. By El Hadji Dem (http://www.savoirfairelinux.com)

[IMP] pep8, add i18n files

9538. By El Hadji Dem (http://www.savoirfairelinux.com)

[ADD] add base_contact_by_functions module

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
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-12-11 19:37:11 +0000
5@@ -0,0 +1,22 @@
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=== added file 'base_contact/__openerp__.py'
30--- base_contact/__openerp__.py 1970-01-01 00:00:00 +0000
31+++ base_contact/__openerp__.py 2013-12-11 19:37:11 +0000
32@@ -0,0 +1,52 @@
33+# -*- coding: utf-8 -*-
34+##############################################################################
35+#
36+# OpenERP, Open Source Business Applications
37+# Copyright (C) 2013-TODAY OpenERP S.A. (<http://openerp.com>).
38+#
39+# This program is free software: you can redistribute it and/or modify
40+# it under the terms of the GNU Affero General Public License as
41+# published by the Free Software Foundation, either version 3 of the
42+# License, or (at your option) any later version.
43+#
44+# This program is distributed in the hope that it will be useful,
45+# but WITHOUT ANY WARRANTY; without even the implied warranty of
46+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
47+# GNU Affero General Public License for more details.
48+#
49+# You should have received a copy of the GNU Affero General Public License
50+# along with this program. If not, see <http://www.gnu.org/licenses/>.
51+#
52+##############################################################################
53+
54+{
55+ 'name': 'Contacts Management',
56+ 'version': '1.0',
57+ 'category': 'Customer Relationship Management',
58+ 'complexity': "expert",
59+ 'description': """
60+This module allows you to manage your contacts
61+==============================================
62+
63+It lets you define groups of contacts sharing some common information, like:
64+ * Birthdate
65+ * Nationality
66+ * Native Language
67+
68+ """,
69+ 'author': 'OpenERP SA',
70+ 'website': 'http://www.openerp.com',
71+ 'depends': ['base', 'process', 'contacts'],
72+ 'init_xml': [],
73+ 'update_xml': [
74+ 'base_contact_view.xml',
75+ ],
76+ 'demo_xml': [
77+ 'base_contact_demo.xml',
78+ ],
79+ 'installable': True,
80+ 'auto_install': False,
81+ #'certificate': '0031287885469',
82+ 'images': [],
83+}
84+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
85
86=== added file 'base_contact/base_contact.py'
87--- base_contact/base_contact.py 1970-01-01 00:00:00 +0000
88+++ base_contact/base_contact.py 2013-12-11 19:37:11 +0000
89@@ -0,0 +1,193 @@
90+# -*- coding: utf-8 -*-
91+##############################################################################
92+#
93+# OpenERP, Open Source Management Solution
94+# Copyright (C) 2013-TODAY OpenERP SA (<http://www.openerp.com>).
95+#
96+# This program is free software: you can redistribute it and/or modify
97+# it under the terms of the GNU Affero General Public License as
98+# published by the Free Software Foundation, either version 3 of the
99+# License, or (at your option) any later version.
100+#
101+# This program is distributed in the hope that it will be useful,
102+# but WITHOUT ANY WARRANTY; without even the implied warranty of
103+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
104+# GNU Affero General Public License for more details.
105+#
106+# You should have received a copy of the GNU Affero General Public License
107+# along with this program. If not, see <http://www.gnu.org/licenses/>.
108+#
109+##############################################################################
110+
111+from openerp.osv import fields, osv, expression
112+
113+
114+class res_partner(osv.osv):
115+ _inherit = 'res.partner'
116+
117+ _contact_type = [
118+ ('standalone', 'Standalone Contact'),
119+ ('attached', 'Attached to existing Contact'),
120+ ]
121+
122+ def _get_contact_type(self, cr, uid, ids, field_name, args, context=None):
123+ result = dict.fromkeys(ids, 'standalone')
124+ for partner in self.browse(cr, uid, ids, context=context):
125+ if partner.contact_id:
126+ result[partner.id] = 'attached'
127+ return result
128+
129+ _columns = {
130+ 'contact_type': fields.function(_get_contact_type, type='selection',
131+ selection=_contact_type,
132+ string='Contact Type',
133+ required=True,
134+ select=1,
135+ store=True),
136+ 'contact_id': fields.many2one('res.partner', 'Main Contact',
137+ domain=[('is_company', '=', False),
138+ ('contact_type', '=', 'standalone')]),
139+ 'other_contact_ids': fields.one2many('res.partner',
140+ 'contact_id',
141+ 'Others Positions'),
142+
143+ # Person specific fields
144+ # add a 'birthdate' as date field, i.e different from char 'birthdate' introduced v6.1!
145+ 'birthdate_date': fields.date('Birthdate'),
146+ 'nationality_id': fields.many2one('res.country', 'Nationality'),
147+ }
148+
149+ _defaults = {
150+ 'contact_type': 'standalone',
151+ }
152+
153+ def _basecontact_check_context(self, cr, user, mode, context=None):
154+ if context is None:
155+ context = {}
156+ # Remove 'search_show_all_positions' for non-search mode.
157+ # Keeping it in context can result in unexpected behaviour (ex: reading
158+ # one2many might return wrong result - i.e with "attached contact" removed
159+ # even if it's directly linked to a company).
160+ if mode != 'search':
161+ context.pop('search_show_all_positions', None)
162+ return context
163+
164+ def search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False):
165+ if context is None:
166+ context = {}
167+ if context.get('search_show_all_positions') is False:
168+ # display only standalone contact matching ``args`` or having
169+ # attached contact matching ``args``
170+ args = expression.normalize_domain(args)
171+ attached_contact_args = expression.AND((args, [('contact_type', '=', 'attached')]))
172+ attached_contact_ids = super(res_partner, self).search(cr, user, attached_contact_args,
173+ context=context)
174+ args = expression.OR((
175+ expression.AND(([('contact_type', '=', 'standalone')], args)),
176+ [('other_contact_ids', 'in', attached_contact_ids)],
177+ ))
178+ return super(res_partner, self).search(cr, user, args, offset=offset, limit=limit,
179+ order=order, context=context, count=count)
180+
181+ def create(self, cr, user, vals, context=None):
182+ context = self._basecontact_check_context(cr, user, 'create', context)
183+ if not vals.get('name') and vals.get('contact_id'):
184+ vals['name'] = self.browse(cr, user, vals['contact_id'], context=context).name
185+ return super(res_partner, self).create(cr, user, vals, context=context)
186+
187+ def read(self, cr, user, ids, fields=None, context=None, load='_classic_read'):
188+ context = self._basecontact_check_context(cr, user, 'read', context)
189+ return super(res_partner, self).read(cr, user, ids, fields=fields, context=context, load=load)
190+
191+ def write(self, cr, user, ids, vals, context=None):
192+ context = self._basecontact_check_context(cr, user, 'write', context)
193+ return super(res_partner, self).write(cr, user, ids, vals, context=context)
194+
195+ def unlink(self, cr, user, ids, context=None):
196+ context = self._basecontact_check_context(cr, user, 'unlink', context)
197+ return super(res_partner, self).unlink(cr, user, ids, context=context)
198+
199+ def _commercial_partner_compute(self, cr, uid, ids, name, args, context=None):
200+ """ Returns the partner that is considered the commercial
201+ entity of this partner. The commercial entity holds the master data
202+ for all commercial fields (see :py:meth:`~_commercial_fields`) """
203+ result = super(res_partner, self)._commercial_partner_compute(cr, uid, ids, name, args, context=context)
204+ for partner in self.browse(cr, uid, ids, context=context):
205+ if partner.contact_type == 'attached' and not partner.parent_id:
206+ result[partner.id] = partner.contact_id.id
207+ return result
208+
209+ def _contact_fields(self, cr, uid, context=None):
210+ """ Returns the list of contact fields that are synced from the parent
211+ when a partner is attached to him. """
212+ return ['name', 'title']
213+
214+ def _contact_sync_from_parent(self, cr, uid, partner, context=None):
215+ """ Handle sync of contact fields when a new parent contact entity is set,
216+ as if they were related fields """
217+ if partner.contact_id:
218+ contact_fields = self._contact_fields(cr, uid, context=context)
219+ sync_vals = self._update_fields_values(cr, uid, partner.contact_id,
220+ contact_fields, context=context)
221+ partner.write(sync_vals)
222+
223+ def update_contact(self, cr, uid, ids, vals, context=None):
224+ if context is None:
225+ context = {}
226+ if context.get('__update_contact_lock'):
227+ return
228+ contact_fields = self._contact_fields(cr, uid, context=context)
229+ contact_vals = dict((field, vals[field]) for field in contact_fields if field in vals)
230+ if contact_vals:
231+ ctx = dict(context, __update_contact_lock=True)
232+ self.write(cr, uid, ids, contact_vals, context=ctx)
233+
234+ def _fields_sync(self, cr, uid, partner, update_values, context=None):
235+ """ Sync commercial fields and address fields from company and to children,
236+ contact fields from contact and to attached contact after create/update,
237+ just as if those were all modeled as fields.related to the parent """
238+ super(res_partner, self)._fields_sync(cr, uid, partner, update_values, context=context)
239+ contact_fields = self._contact_fields(cr, uid, context=context)
240+ # 1. From UPSTREAM: sync from parent contact
241+ if update_values.get('contact_id'):
242+ self._contact_sync_from_parent(cr, uid, partner, context=context)
243+ # 2. To DOWNSTREAM: sync contact fields to parent or related
244+ elif any(field in contact_fields for field in update_values):
245+ update_ids = [c.id for c in partner.other_contact_ids if not c.is_company]
246+ if partner.contact_id:
247+ update_ids.append(partner.contact_id.id)
248+ self.update_contact(cr, uid, update_ids, update_values, context=context)
249+
250+ def onchange_contact_id(self, cr, uid, ids, contact_id, context=None):
251+ values = {}
252+ if contact_id:
253+ values['name'] = self.browse(cr, uid, contact_id, context=context).name
254+ return {'value': values}
255+
256+ def onchange_contact_type(self, cr, uid, ids, contact_type, context=None):
257+ values = {}
258+ if contact_type == 'standalone':
259+ values['contact_id'] = False
260+ return {'value': values}
261+
262+
263+class ir_actions_window(osv.osv):
264+ _inherit = 'ir.actions.act_window'
265+
266+ def read(self, cr, user, ids, fields=None, context=None, load='_classic_read'):
267+ action_ids = ids
268+ if isinstance(ids, (int, long)):
269+ action_ids = [ids]
270+ actions = super(ir_actions_window, self).read(cr, user, action_ids, fields=fields, context=context, load=load)
271+ for action in actions:
272+ if action.get('res_model', '') == 'res.partner':
273+ # By default, only show standalone contact
274+ action_context = action.get('context', '{}') or '{}'
275+ if 'search_show_all_positions' not in action_context:
276+ action['context'] = action_context.replace('{',
277+ "{'search_show_all_positions': False,", 1)
278+ if isinstance(ids, (int, long)):
279+ if actions:
280+ return actions[0]
281+ return False
282+ return actions
283
284=== added file 'base_contact/base_contact_demo.xml'
285--- base_contact/base_contact_demo.xml 1970-01-01 00:00:00 +0000
286+++ base_contact/base_contact_demo.xml 2013-12-11 19:37:11 +0000
287@@ -0,0 +1,29 @@
288+<?xml version="1.0" encoding="UTF-8"?>
289+<openerp>
290+ <data>
291+
292+ <record id="res_partner_main2_position_consultant" model="res.partner">
293+ <field name="name">Roger Scott</field>
294+ <field name="function">Consultant</field>
295+ <field name="parent_id" ref="base.res_partner_11"/>
296+ <field name="contact_id" ref="base.res_partner_main2"/>
297+ <field name="use_parent_address" eval="True"/>
298+ </record>
299+
300+ <record id="res_partner_contact1" model="res.partner">
301+ <field name="name">Bob Egnops</field>
302+ <field name="birthdate_date">1984-01-01</field>
303+ <field name="email">bob@hillenburg-oceaninstitute.com</field>
304+ </record>
305+
306+ <record id="res_partner_contact1_work_position1" model="res.partner">
307+ <field name="name">Bob Egnops</field>
308+ <field name="function">Technician</field>
309+ <field name="email">bob@yourcompany.com</field>
310+ <field name="parent_id" ref="base.main_partner"/>
311+ <field name="contact_id" ref="res_partner_contact1"/>
312+ <field name="use_parent_address" eval="True"/>
313+ </record>
314+
315+ </data>
316+</openerp>
317\ No newline at end of file
318
319=== added file 'base_contact/base_contact_view.xml'
320--- base_contact/base_contact_view.xml 1970-01-01 00:00:00 +0000
321+++ base_contact/base_contact_view.xml 2013-12-11 19:37:11 +0000
322@@ -0,0 +1,202 @@
323+<?xml version="1.0" encoding="utf-8"?>
324+<openerp>
325+<data>
326+
327+ <record id="view_res_partner_filter_contact" model="ir.ui.view">
328+ <field name="name">res.partner.select.contact</field>
329+ <field name="model">res.partner</field>
330+ <field name="inherit_id" ref="base.view_res_partner_filter"/>
331+ <field name="arch" type="xml">
332+ <filter name="type_company" position="after">
333+ <separator/>
334+ <filter string="All positions" name="type_otherpositions"
335+ context="{'search_show_all_positions': True}"
336+ help="All partner positions"/>
337+ </filter>
338+ <xpath expr="/search/group/filter[@string='Company']" position="before">
339+ <filter string="Person" name="group_person" context="{'group_by': 'contact_id'}"/>
340+ </xpath>
341+ </field>
342+ </record>
343+
344+ <record id="view_res_partner_tree_contact" model="ir.ui.view">
345+ <field name="name">res.partner.tree.contact</field>
346+ <field name="model">res.partner</field>
347+ <field name="inherit_id" ref="base.view_partner_tree"/>
348+ <field name="arch" type="xml">
349+ <field name="parent_id" position="after">
350+ <field name="contact_id" invisible="1"/>
351+ </field>
352+ </field>
353+ </record>
354+
355+ <record model="ir.ui.view" id="view_partner_form_inherit">
356+ <field name="name">res.partner.form.contact</field>
357+ <field name="model">res.partner</field>
358+ <field name="inherit_id" ref="base.view_partner_form"/>
359+ <field name="type">form</field>
360+ <field name="arch" type="xml">
361+ <field name="is_company" position="after">
362+ <field name="contact_type" invisible="1"/>
363+ </field>
364+ <page string="Contacts" position="after">
365+ <page string="Other Positions" attrs="{'invisible': ['|',('is_company','=',True),('contact_id','!=',False)]}">
366+ <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">
367+ <kanban>
368+ <field name="color"/>
369+ <field name="name"/>
370+ <field name="title"/>
371+ <field name="email"/>
372+ <field name="parent_id"/>
373+ <field name="is_company"/>
374+ <field name="function"/>
375+ <field name="phone"/>
376+ <field name="street"/>
377+ <field name="street2"/>
378+ <field name="zip"/>
379+ <field name="city"/>
380+ <field name="country_id"/>
381+ <field name="mobile"/>
382+ <field name="fax"/>
383+ <field name="state_id"/>
384+ <field name="has_image"/>
385+ <templates>
386+ <t t-name="kanban-box">
387+ <t t-set="color" t-value="kanban_color(record.color.raw_value)"/>
388+ <div t-att-class="color + (record.title.raw_value == 1 ? ' oe_kanban_color_alert' : '')" style="position: relative">
389+ <a t-if="! read_only_mode" type="delete" style="position: absolute; right: 0; padding: 4px; diplay: inline-block">X</a>
390+ <div class="oe_module_vignette">
391+ <a type="open">
392+ <t t-if="record.has_image.raw_value === true">
393+ <img t-att-src="kanban_image('res.partner', 'image', record.id.value, {'preview_image': 'image_small'})" class="oe_avatar oe_kanban_avatar_smallbox"/>
394+ </t>
395+ <t t-if="record.image and record.image.raw_value !== false">
396+ <img t-att-src="'data:image/png;base64,'+record.image.raw_value" class="oe_avatar oe_kanban_avatar_smallbox"/>
397+ </t>
398+ <t t-if="record.has_image.raw_value === false and (!record.image or record.image.raw_value === false)">
399+ <t t-if="record.is_company.raw_value === true">
400+ <img t-att-src='_s + "/base/static/src/img/company_image.png"' class="oe_kanban_image oe_kanban_avatar_smallbox"/>
401+ </t>
402+ <t t-if="record.is_company.raw_value === false">
403+ <img t-att-src='_s + "/base/static/src/img/avatar.png"' class="oe_kanban_image oe_kanban_avatar_smallbox"/>
404+ </t>
405+ </t>
406+ </a>
407+ <div class="oe_module_desc">
408+ <div class="oe_kanban_box_content oe_kanban_color_bglight oe_kanban_color_border">
409+ <table class="oe_kanban_table">
410+ <tr>
411+ <td class="oe_kanban_title1" align="left" valign="middle">
412+ <h4><a type="open"><field name="name"/></a></h4>
413+ <i>
414+ <t t-if="record.parent_id.raw_value and !record.function.raw_value"><field name="parent_id"/></t>
415+ <t t-if="!record.parent_id.raw_value and record.function.raw_value"><field name="function"/></t>
416+ <t t-if="record.parent_id.raw_value and record.function.raw_value"><field name="function"/> at <field name="parent_id"/></t>
417+ </i>
418+ <div><a t-if="record.email.raw_value" title="Mail" t-att-href="'mailto:'+record.email.value">
419+ <field name="email"/>
420+ </a></div>
421+ <div t-if="record.phone.raw_value">Phone: <field name="phone"/></div>
422+ <div t-if="record.mobile.raw_value">Mobile: <field name="mobile"/></div>
423+ <div t-if="record.fax.raw_value">Fax: <field name="fax"/></div>
424+ </td>
425+ </tr>
426+ </table>
427+ </div>
428+ </div>
429+ </div>
430+ </div>
431+ </t>
432+ </templates>
433+ </kanban>
434+ <form string="Contact" version="7.0">
435+ <sheet>
436+ <field name="image" widget='image' class="oe_avatar oe_left" options='{"preview_image": "image_medium"}'/>
437+ <div class="oe_title">
438+ <label for="name" class="oe_edit_only"/>
439+ <h1><field name="name" style="width: 70%%"/></h1>
440+ </div>
441+ <group>
442+ <!-- inherited part -->
443+ <field name="category_id" widget="many2many_tags" placeholder="Tags..." style="width: 70%%"/>
444+ <field name="parent_id" placeholder="Company" domain="[('is_company','=',True)]"/>
445+ <!-- inherited part end -->
446+ <field name="function" placeholder="e.g. Sales Director"/>
447+ <field name="email"/>
448+ <field name="phone"/>
449+ <field name="mobile"/>
450+ </group>
451+ <div>
452+ <field name="use_parent_address"/><label for="use_parent_address"/>
453+ </div>
454+ <group>
455+ <label for="type"/>
456+ <div name="div_type">
457+ <field class="oe_inline" name="type"/>
458+ </div>
459+ <label for="street" string="Address" attrs="{'invisible': [('use_parent_address','=', True)]}"/>
460+ <div attrs="{'invisible': [('use_parent_address','=', True)]}" name="div_address">
461+ <field name="street" placeholder="Street..."/>
462+ <field name="street2"/>
463+ <div class="address_format">
464+ <field name="city" placeholder="City" style="width: 40%%"/>
465+ <field name="state_id" class="oe_no_button" placeholder="State" style="width: 37%%" options='{"no_open": True}' on_change="onchange_state(state_id)"/>
466+ <field name="zip" placeholder="ZIP" style="width: 20%%"/>
467+ </div>
468+ <field name="country_id" placeholder="Country" class="oe_no_button" options='{"no_open": True}'/>
469+ </div>
470+ </group>
471+ <field name="supplier" invisible="True"/>
472+ </sheet>
473+ </form>
474+ </field>
475+ </page>
476+ <page name="personal-info" string="Personal Information" attrs="{'invisible': ['|',('is_company','=',True)]}">
477+ <p attrs="{'invisible': [('contact_id','=',False)]}">
478+ 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}"
479+ on_change="onchange_contact_id(contact_id)" options="{'always_reload': True}"/>
480+ </p>
481+ <group attrs="{'invisible': [('contact_id','!=',False)]}">
482+ <field name="birthdate_date"/>
483+ <field name="nationality_id"/>
484+ </group>
485+ </page>
486+ </page>
487+ <xpath expr="//field[@name='child_ids']/form//field[@name='name']/.." position="before">
488+ <field name="contact_type" readonly="0" on_change="onchange_contact_type(contact_type)"/>
489+ </xpath>
490+ <xpath expr="//field[@name='child_ids']/form//field[@name='name']" position="after">
491+ <field name="contact_id" on_change="onchange_contact_id(contact_id)" string="Contact"
492+ attrs="{'invisible': [('contact_type','!=','attached')], 'required': [('contact_type','=','attached')]}"/>
493+ </xpath>
494+ <xpath expr="//field[@name='child_ids']/form//field[@name='name']" position="attributes">
495+ <attribute name="attrs">{'invisible': [('contact_type','=','attached')]}</attribute>
496+ </xpath>
497+ </field>
498+ </record>
499+
500+ <record model="ir.ui.view" id="view_res_partner_kanban_contact">
501+ <field name="name">res.partner.kanban.contact</field>
502+ <field name="model">res.partner</field>
503+ <field name="inherit_id" ref="base.res_partner_kanban_view"/>
504+ <field name="arch" type="xml">
505+ <field name="is_company" position="after">
506+ <field name="other_contact_ids">
507+ <tree>
508+ <field name="parent_id"/>
509+ <field name="function"/>
510+ </tree>
511+ </field>
512+ </field>
513+ <xpath expr="//t[@t-name='kanban-box']//div[@class='oe_kanban_details']/ul/li[3]" position="after">
514+ <t t-if="record.other_contact_ids.raw_value.length &gt; 0">
515+ <li>+<t t-esc="record.other_contact_ids.raw_value.length"/>
516+ <t t-if="record.other_contact_ids.raw_value.length == 1">other position</t>
517+ <t t-if="record.other_contact_ids.raw_value.length &gt; 1">other positions</t></li>
518+ </t>
519+ </xpath>
520+ </field>
521+ </record>
522+
523+</data>
524+</openerp>
525
526=== added directory 'base_contact/i18n'
527=== added file 'base_contact/i18n/base_contact.pot'
528--- base_contact/i18n/base_contact.pot 1970-01-01 00:00:00 +0000
529+++ base_contact/i18n/base_contact.pot 2013-12-11 19:37:11 +0000
530@@ -0,0 +1,184 @@
531+# Translation of OpenERP Server.
532+# This file contains the translation of the following modules:
533+# * base_contact
534+#
535+msgid ""
536+msgstr ""
537+"Project-Id-Version: OpenERP Server 7.0\n"
538+"Report-Msgid-Bugs-To: \n"
539+"POT-Creation-Date: 2013-12-10 16:08+0000\n"
540+"PO-Revision-Date: 2013-12-10 11:08-0500\n"
541+"Last-Translator: \n"
542+"Language-Team: \n"
543+"MIME-Version: 1.0\n"
544+"Content-Type: text/plain; charset=UTF-8\n"
545+"Content-Transfer-Encoding: 8bit\n"
546+"Plural-Forms: \n"
547+"X-Generator: Poedit 1.5.4\n"
548+
549+#. module: base_contact
550+#: view:res.partner:0
551+msgid "City"
552+msgstr ""
553+
554+#. module: base_contact
555+#: view:res.partner:0
556+msgid "other position"
557+msgstr ""
558+
559+#. module: base_contact
560+#: view:res.partner:0
561+msgid "Contacts"
562+msgstr ""
563+
564+#. module: base_contact
565+#: view:res.partner:0
566+msgid ""
567+"To see personal information about this contact, please go to to the his "
568+"person form:"
569+msgstr ""
570+
571+#. module: base_contact
572+#: view:res.partner:0
573+msgid "Street..."
574+msgstr ""
575+
576+#. module: base_contact
577+#: view:res.partner:0
578+msgid "State"
579+msgstr ""
580+
581+#. module: base_contact
582+#: view:res.partner:0
583+msgid "at"
584+msgstr ""
585+
586+#. module: base_contact
587+#: view:res.partner:0
588+msgid "Tags..."
589+msgstr ""
590+
591+#. module: base_contact
592+#: view:res.partner:0
593+msgid "Other Positions"
594+msgstr ""
595+
596+#. module: base_contact
597+#: view:res.partner:0
598+msgid "Phone:"
599+msgstr ""
600+
601+#. module: base_contact
602+#: view:res.partner:0
603+msgid "Company"
604+msgstr ""
605+
606+#. module: base_contact
607+#: field:res.partner,contact_id:0
608+msgid "Main Contact"
609+msgstr ""
610+
611+#. module: base_contact
612+#: view:res.partner:0
613+msgid "Fax:"
614+msgstr ""
615+
616+#. module: base_contact
617+#: selection:res.partner,contact_type:0
618+msgid "Standalone Contact"
619+msgstr ""
620+
621+#. module: base_contact
622+#: view:res.partner:0
623+msgid "Address"
624+msgstr ""
625+
626+#. module: base_contact
627+#: field:res.partner,nationality_id:0
628+msgid "Nationality"
629+msgstr ""
630+
631+#. module: base_contact
632+#: selection:res.partner,contact_type:0
633+msgid "Attached to existing Contact"
634+msgstr ""
635+
636+#. module: base_contact
637+#: view:res.partner:0
638+msgid "ZIP"
639+msgstr ""
640+
641+#. module: base_contact
642+#: view:res.partner:0
643+msgid "Country"
644+msgstr ""
645+
646+#. module: base_contact
647+#: field:res.partner,birthdate_date:0
648+msgid "Birthdate"
649+msgstr ""
650+
651+#. module: base_contact
652+#: view:res.partner:0
653+msgid "Person"
654+msgstr ""
655+
656+#. module: base_contact
657+#: view:res.partner:0
658+msgid "Contact"
659+msgstr ""
660+
661+#. module: base_contact
662+#: view:res.partner:0
663+msgid "Mobile:"
664+msgstr ""
665+
666+#. module: base_contact
667+#: view:res.partner:0
668+msgid "All partner positions"
669+msgstr ""
670+
671+#. module: base_contact
672+#: view:res.partner:0
673+msgid "{'invisible': [('contact_type','=','attached')]}"
674+msgstr ""
675+
676+#. module: base_contact
677+#: view:res.partner:0
678+msgid "All positions"
679+msgstr ""
680+
681+#. module: base_contact
682+#: model:ir.model,name:base_contact.model_ir_actions_act_window
683+msgid "ir.actions.act_window"
684+msgstr ""
685+
686+#. module: base_contact
687+#: view:res.partner:0
688+msgid "other positions"
689+msgstr ""
690+
691+#. module: base_contact
692+#: field:res.partner,contact_type:0
693+msgid "Contact Type"
694+msgstr ""
695+
696+#. module: base_contact
697+#: model:ir.model,name:base_contact.model_res_partner
698+msgid "Partner"
699+msgstr ""
700+
701+#. module: base_contact
702+#: field:res.partner,other_contact_ids:0
703+msgid "Others Positions"
704+msgstr ""
705+
706+#. module: base_contact
707+#: view:res.partner:0
708+msgid "Personal Information"
709+msgstr ""
710+
711+#. module: base_contact
712+#: view:res.partner:0
713+msgid "e.g. Sales Director"
714+msgstr ""
715
716=== added file 'base_contact/i18n/fr.po'
717--- base_contact/i18n/fr.po 1970-01-01 00:00:00 +0000
718+++ base_contact/i18n/fr.po 2013-12-11 19:37:11 +0000
719@@ -0,0 +1,186 @@
720+# Translation of OpenERP Server.
721+# This file contains the translation of the following modules:
722+# * base_contact
723+#
724+msgid ""
725+msgstr ""
726+"Project-Id-Version: OpenERP Server 7.0\n"
727+"Report-Msgid-Bugs-To: \n"
728+"POT-Creation-Date: 2013-12-10 16:09+0000\n"
729+"PO-Revision-Date: 2013-12-10 11:15-0500\n"
730+"Last-Translator: \n"
731+"Language-Team: \n"
732+"MIME-Version: 1.0\n"
733+"Content-Type: text/plain; charset=UTF-8\n"
734+"Content-Transfer-Encoding: 8bit\n"
735+"Plural-Forms: \n"
736+"X-Generator: Poedit 1.5.4\n"
737+
738+#. module: base_contact
739+#: view:res.partner:0
740+msgid "City"
741+msgstr "Ville"
742+
743+#. module: base_contact
744+#: view:res.partner:0
745+msgid "other position"
746+msgstr "Autre fonction"
747+
748+#. module: base_contact
749+#: view:res.partner:0
750+msgid "Contacts"
751+msgstr "Contacts"
752+
753+#. module: base_contact
754+#: view:res.partner:0
755+msgid ""
756+"To see personal information about this contact, please go to to the his "
757+"person form:"
758+msgstr ""
759+"Pour voir des informations personnelles sur ce contact, s'il vous plaît "
760+"aller à la sa forme de personne:"
761+
762+#. module: base_contact
763+#: view:res.partner:0
764+msgid "Street..."
765+msgstr "Rue..."
766+
767+#. module: base_contact
768+#: view:res.partner:0
769+msgid "State"
770+msgstr "État"
771+
772+#. module: base_contact
773+#: view:res.partner:0
774+msgid "at"
775+msgstr "à"
776+
777+#. module: base_contact
778+#: view:res.partner:0
779+msgid "Tags..."
780+msgstr "Étiquettes..."
781+
782+#. module: base_contact
783+#: view:res.partner:0
784+msgid "Other Positions"
785+msgstr "Autres Fonctions"
786+
787+#. module: base_contact
788+#: view:res.partner:0
789+msgid "Phone:"
790+msgstr "Téléphone:"
791+
792+#. module: base_contact
793+#: view:res.partner:0
794+msgid "Company"
795+msgstr "Organisme"
796+
797+#. module: base_contact
798+#: field:res.partner,contact_id:0
799+msgid "Main Contact"
800+msgstr "Contact principal"
801+
802+#. module: base_contact
803+#: view:res.partner:0
804+msgid "Fax:"
805+msgstr "Fax :"
806+
807+#. module: base_contact
808+#: selection:res.partner,contact_type:0
809+msgid "Standalone Contact"
810+msgstr "Contact simple"
811+
812+#. module: base_contact
813+#: view:res.partner:0
814+msgid "Address"
815+msgstr "Adresse"
816+
817+#. module: base_contact
818+#: field:res.partner,nationality_id:0
819+msgid "Nationality"
820+msgstr "Nationalité"
821+
822+#. module: base_contact
823+#: selection:res.partner,contact_type:0
824+msgid "Attached to existing Contact"
825+msgstr "Attaché à contacter existante"
826+
827+#. module: base_contact
828+#: view:res.partner:0
829+msgid "ZIP"
830+msgstr "Code postal"
831+
832+#. module: base_contact
833+#: view:res.partner:0
834+msgid "Country"
835+msgstr "Pays"
836+
837+#. module: base_contact
838+#: field:res.partner,birthdate_date:0
839+msgid "Birthdate"
840+msgstr "Date de naissance"
841+
842+#. module: base_contact
843+#: view:res.partner:0
844+msgid "Person"
845+msgstr "Personne"
846+
847+#. module: base_contact
848+#: view:res.partner:0
849+msgid "Contact"
850+msgstr "Contact"
851+
852+#. module: base_contact
853+#: view:res.partner:0
854+msgid "Mobile:"
855+msgstr "Portable :"
856+
857+#. module: base_contact
858+#: view:res.partner:0
859+msgid "All partner positions"
860+msgstr "Toutes les fonctions"
861+
862+#. module: base_contact
863+#: view:res.partner:0
864+msgid "{'invisible': [('contact_type','=','attached')]}"
865+msgstr "{'invisible': [('contact_type','=','attached')]}"
866+
867+#. module: base_contact
868+#: view:res.partner:0
869+msgid "All positions"
870+msgstr "Toutes les positions"
871+
872+#. module: base_contact
873+#: model:ir.model,name:base_contact.model_ir_actions_act_window
874+msgid "ir.actions.act_window"
875+msgstr "ir.actions.act_window"
876+
877+#. module: base_contact
878+#: view:res.partner:0
879+msgid "other positions"
880+msgstr "Autres fonctions"
881+
882+#. module: base_contact
883+#: field:res.partner,contact_type:0
884+msgid "Contact Type"
885+msgstr "Type de contact"
886+
887+#. module: base_contact
888+#: model:ir.model,name:base_contact.model_res_partner
889+msgid "Partner"
890+msgstr "Partenaire"
891+
892+#. module: base_contact
893+#: field:res.partner,other_contact_ids:0
894+msgid "Others Positions"
895+msgstr "Autres fonctions"
896+
897+#. module: base_contact
898+#: view:res.partner:0
899+msgid "Personal Information"
900+msgstr "Informations personnelles"
901+
902+#. module: base_contact
903+#: view:res.partner:0
904+msgid "e.g. Sales Director"
905+msgstr "e.g. Directeur"
906
907=== added directory 'base_contact/images'
908=== added directory 'base_contact/tests'
909=== added file 'base_contact/tests/__init__.py'
910--- base_contact/tests/__init__.py 1970-01-01 00:00:00 +0000
911+++ base_contact/tests/__init__.py 2013-12-11 19:37:11 +0000
912@@ -0,0 +1,26 @@
913+# -*- coding: utf-8 ⁻*-
914+##############################################################################
915+#
916+# OpenERP, Open Source Business Applications
917+# Copyright (C) 2013-TODAY OpenERP S.A. (<http://openerp.com>).
918+#
919+# This program is free software: you can redistribute it and/or modify
920+# it under the terms of the GNU Affero General Public License as
921+# published by the Free Software Foundation, either version 3 of the
922+# License, or (at your option) any later version.
923+#
924+# This program is distributed in the hope that it will be useful,
925+# but WITHOUT ANY WARRANTY; without even the implied warranty of
926+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
927+# GNU Affero General Public License for more details.
928+#
929+# You should have received a copy of the GNU Affero General Public License
930+# along with this program. If not, see <http://www.gnu.org/licenses/>.
931+#
932+##############################################################################
933+
934+from . import test_base_contact
935+
936+checks = [
937+ test_base_contact,
938+]
939
940=== added file 'base_contact/tests/test_base_contact.py'
941--- base_contact/tests/test_base_contact.py 1970-01-01 00:00:00 +0000
942+++ base_contact/tests/test_base_contact.py 2013-12-11 19:37:11 +0000
943@@ -0,0 +1,136 @@
944+# -*- coding: utf-8 ⁻*-
945+##############################################################################
946+#
947+# OpenERP, Open Source Business Applications
948+# Copyright (C) 2013-TODAY OpenERP S.A. (<http://openerp.com>).
949+#
950+# This program is free software: you can redistribute it and/or modify
951+# it under the terms of the GNU Affero General Public License as
952+# published by the Free Software Foundation, either version 3 of the
953+# License, or (at your option) any later version.
954+#
955+# This program is distributed in the hope that it will be useful,
956+# but WITHOUT ANY WARRANTY; without even the implied warranty of
957+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
958+# GNU Affero General Public License for more details.
959+#
960+# You should have received a copy of the GNU Affero General Public License
961+# along with this program. If not, see <http://www.gnu.org/licenses/>.
962+#
963+##############################################################################
964+
965+from openerp.tests import common
966+
967+
968+class Test_Base_Contact(common.TransactionCase):
969+
970+ def setUp(self):
971+ """*****setUp*****"""
972+ super(Test_Base_Contact, self).setUp()
973+ cr, uid = self.cr, self.uid
974+ ModelData = self.registry('ir.model.data')
975+ self.partner = self.registry('res.partner')
976+
977+ # Get test records reference
978+ for attr, module, name in [
979+ ('main_partner_id', 'base', 'main_partner'),
980+ ('bob_contact_id', 'base_contact', 'res_partner_contact1'),
981+ ('bob_job1_id', 'base_contact', 'res_partner_contact1_work_position1'),
982+ ('roger_contact_id', 'base', 'res_partner_main2'),
983+ ('roger_job2_id', 'base_contact', 'res_partner_main2_position_consultant')]:
984+ r = ModelData.get_object_reference(cr, uid, module, name)
985+ setattr(self, attr, r[1] if r else False)
986+
987+ def test_00_show_only_standalone_contact(self):
988+ """Check that only standalone contact are shown if context explicitly state to not display all positions"""
989+ cr, uid = self.cr, self.uid
990+ ctx = {'search_show_all_positions': False}
991+ partner_ids = self.partner.search(cr, uid, [], context=ctx)
992+ partner_ids.sort()
993+ self.assertTrue(self.bob_job1_id not in partner_ids)
994+ self.assertTrue(self.roger_job2_id not in partner_ids)
995+
996+ def test_01_show_all_positions(self):
997+ """Check that all contact are show if context is empty or explicitly state to display all positions"""
998+ cr, uid = self.cr, self.uid
999+
1000+ partner_ids = self.partner.search(cr, uid, [], context=None)
1001+ self.assertTrue(self.bob_job1_id in partner_ids)
1002+ self.assertTrue(self.roger_job2_id in partner_ids)
1003+
1004+ ctx = {'search_show_all_positions': True}
1005+ partner_ids = self.partner.search(cr, uid, [], context=ctx)
1006+ self.assertTrue(self.bob_job1_id in partner_ids)
1007+ self.assertTrue(self.roger_job2_id in partner_ids)
1008+
1009+ def test_02_reading_other_contact_one2many_show_all_positions(self):
1010+ """Check that readonly partner's ``other_contact_ids`` return all values whatever the context"""
1011+ cr, uid = self.cr, self.uid
1012+
1013+ def read_other_contacts(pid, context=None):
1014+ return self.partner.read(cr, uid, [pid], ['other_contact_ids'], context=context)[0]['other_contact_ids']
1015+
1016+ def read_contacts(pid, context=None):
1017+ return self.partner.read(cr, uid, [pid], ['child_ids'], context=context)[0]['child_ids']
1018+
1019+ ctx = None
1020+ self.assertEqual(read_other_contacts(self.bob_contact_id, context=ctx), [self.bob_job1_id])
1021+ ctx = {'search_show_all_positions': False}
1022+ self.assertEqual(read_other_contacts(self.bob_contact_id, context=ctx), [self.bob_job1_id])
1023+ ctx = {'search_show_all_positions': True}
1024+ self.assertEqual(read_other_contacts(self.bob_contact_id, context=ctx), [self.bob_job1_id])
1025+
1026+ ctx = None
1027+ self.assertTrue(self.bob_job1_id in read_contacts(self.main_partner_id, context=ctx))
1028+ ctx = {'search_show_all_positions': False}
1029+ self.assertTrue(self.bob_job1_id in read_contacts(self.main_partner_id, context=ctx))
1030+ ctx = {'search_show_all_positions': True}
1031+ self.assertTrue(self.bob_job1_id in read_contacts(self.main_partner_id, context=ctx))
1032+
1033+ def test_03_search_match_attached_contacts(self):
1034+ """Check that searching partner also return partners having attached contacts matching search criteria"""
1035+ cr, uid = self.cr, self.uid
1036+ # Bob's contact has one other position which is related to 'Your Company'
1037+ # so search for all contacts working for 'Your Company' should contain bob position.
1038+ partner_ids = self.partner.search(cr, uid, [('parent_id', 'ilike', 'Your Company')], context=None)
1039+ self.assertTrue(self.bob_job1_id in partner_ids)
1040+
1041+ # but when searching without 'all positions', we should get the position standalone contact instead.
1042+ ctx = {'search_show_all_positions': False}
1043+ partner_ids = self.partner.search(cr, uid, [('parent_id', 'ilike', 'Your Company')], context=ctx)
1044+ self.assertTrue(self.bob_contact_id in partner_ids)
1045+
1046+ def test_04_contact_creation(self):
1047+ """Check that we're begin to create a contact"""
1048+ cr, uid = self.cr, self.uid
1049+
1050+ # Create a contact using only name
1051+ new_contact_id = self.partner.create(cr, uid, {'name': 'Bob Egnops'})
1052+ self.assertEqual(self.partner.browse(cr, uid, new_contact_id).contact_type, 'standalone')
1053+
1054+ # Create a contact with only contact_id
1055+ new_contact_id = self.partner.create(cr, uid, {'contact_id': self.bob_contact_id})
1056+ new_contact = self.partner.browse(cr, uid, new_contact_id)
1057+ self.assertEqual(new_contact.name, 'Bob Egnops')
1058+ self.assertEqual(new_contact.contact_type, 'attached')
1059+
1060+ # Create a contact with both contact_id and name;
1061+ # contact's name should override provided value in that case
1062+ new_contact_id = self.partner.create(cr, uid, {'contact_id': self.bob_contact_id, 'name': 'Rob Egnops'})
1063+ self.assertEqual(self.partner.browse(cr, uid, new_contact_id).name, 'Bob Egnops')
1064+
1065+ # Reset contact to standalone
1066+ self.partner.write(cr, uid, [new_contact_id], {'contact_id': False})
1067+ self.assertEqual(self.partner.browse(cr, uid, new_contact_id).contact_type, 'standalone')
1068+
1069+ def test_05_contact_fields_sync(self):
1070+ """Check that contact's fields are correctly synced between parent contact or related contacts"""
1071+ cr, uid = self.cr, self.uid
1072+
1073+ # Test DOWNSTREAM sync
1074+ self.partner.write(cr, uid, [self.bob_contact_id], {'name': 'Rob Egnops'})
1075+ self.assertEqual(self.partner.browse(cr, uid, self.bob_job1_id).name, 'Rob Egnops')
1076+
1077+ # Test UPSTREAM sync
1078+ self.partner.write(cr, uid, [self.bob_job1_id], {'name': 'Bob Egnops'})
1079+ self.assertEqual(self.partner.browse(cr, uid, self.bob_contact_id).name, 'Bob Egnops')