Merge lp:~savoirfairelinux-openerp/partner-contact-management/partner-contact-management-base_contact into lp:~partner-contact-core-editors/partner-contact-management/7.0

Status: Merged
Merged at revision: 38
Proposed branch: lp:~savoirfairelinux-openerp/partner-contact-management/partner-contact-management-base_contact
Merge into: lp:~partner-contact-core-editors/partner-contact-management/7.0
Diff against target: 694 lines (+657/-0)
7 files modified
base_contact/__init__.py (+22/-0)
base_contact/__openerp__.py (+56/-0)
base_contact/base_contact.py (+186/-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:~savoirfairelinux-openerp/partner-contact-management/partner-contact-management-base_contact
Reviewer Review Type Date Requested Status
Guewen Baconnier @ Camptocamp code review Approve
Mario Arias (community) code review and test Approve
Sandy Carter (http://www.savoirfairelinux.com) code review, test Approve
Partner and Contact Core Editors Pending
Xavier ALT Pending
Review via email: mp+203979@code.launchpad.net

Description of the change

- Added base_contact module.This module has been migrated from OP6.1 to OP7.0 and a merge is proposed at 2013-10-22.
Details of MP: https://code.launchpad.net/~openerp-dev/openobject-addons/7.0-base-contact-xal/+merge/192129

To post a comment you must log in.
Revision history for this message
Sandy Carter (http://www.savoirfairelinux.com) (sandy-carter) wrote :

LGTM

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

The MP: https://code.launchpad.net/~openerp-dev/openobject-addons/7.0-base-contact-xal/+merge/192129 is going nowhere.
There is no technical reason that it cannot be moved here. It will be beneficial to have it here instead of OCB

Revision history for this message
Pedro Manuel Baeza (pedro.baeza) wrote :

But do you approve the module as is? If the module is finally merged with changes, how can we handle migration?

At least I think that we must rename it to avoid these things, but this means also renaming fields. Uf, too many doubts...

Regards.

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

We have a few options:
1/ If openobject-addons MP changes, keep up with those changes, if it gets merged, remove it from here
2/ Ask that the openobject-addons MP be closed and suggest here as the repository to put it
3/ Duplicate the module under a different name

I would rather do 2. The openobjects MP is dead in its tracks.

Revision history for this message
Pedro Manuel Baeza (pedro.baeza) wrote :

Let's wait what others say.

Regards.

33. By Sandy Carter (http://www.savoirfairelinux.com)

[FIX] base_location - Add context propagation to base_location

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

[ADD] Add passport and passport_partner modules: You can manage several passports for each contact.

35. By Jonathan Nemry (Acsone)

[MRG]

For Partner:

1) Create always the SQL constraint on 'lastname'
2) Better implementation of "_write_name" allowing more intuitive update of the partner name when processing from another model (ex: res_user), i.e., try to keep the firstname if unchanged
3) When duplicating a partner, avoid to repeat the firstname in the name
4) Beautify the inner form for children contacts (placing fields as in the main form)
5) Allow edition of the field name in the inner form if child is a company

For User:

1) Reintegrate the name as "required"
2) When duplicating a user, avoid to repeat the firstname in the related partner name

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

Any update on this?

Revision history for this message
Mario Arias (the-clone-master) wrote :

We are using the original one for a basic project, to map clients and employees to multiple companies.

It works flawlessly, but the scope is really reduced (no HR, no stock management, only service invoices, ...)

Is a very handy module, but OpenERP S.A. seems to have forgotten about it...

:(

I prefer having it here, so others could also try and improve it...

review: Approve (code review and test)
Revision history for this message
Guewen Baconnier @ Camptocamp (gbaconnier-c2c) wrote :

> There is no technical reason that it cannot be moved here. It will be beneficial to have it here instead of OCB

> We have a few options:
> 1/ If openobject-addons MP changes, keep up with those changes, if it gets
> merged, remove it from here
> 2/ Ask that the openobject-addons MP be closed and suggest here as the
> repository to put it
> 3/ Duplicate the module under a different name
>
> I would rather do 2. The openobjects MP is dead in its tracks.

Why would it be beneficial to have it here rather than in OCB? Wouldn't be easier to track the changes if the branches have the same origin? On the other hand I'm unsure what is the OCB policy about adding a module, even if it is "supposed" to be merged in the official addons.

Unrelated: the bzr history and authorship has been lost, please use bzr-super-replay / --author must be used to keep the original author.

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

For one, I do not use OCB, so there would be no point for me to propose a merge into it if I want to use it. ;)
Second, I made a mistake in my comment when saying OCB, I meant OpenObject-Addons.

You are absolutely right about the authorship, I will fix that. Thanks for pointing that out with a solution.

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

@Guewen
I reconstructed the history to the best of my abilities.

Revision history for this message
Guewen Baconnier @ Camptocamp (gbaconnier-c2c) wrote :

Thanks!
That's ok for me now.

review: Approve (code review)

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