[FIX] res.partner: autosync of commercial fields and address fields + correct views accordingly + basic tests
* Commercial fields (bug 1160365)
Fix autosync of accounting/invoicing-related fields on contacts,
just as if they were actually modeled as fields.related to the
parent commercial entity.
This starts with the addition of the new functional field
`commercial_id`, to locate the commercial entity for any partner.
The commercial entity is defined as the first ancestor (starting
at the partner itself) that is either marked `is_company` or has
no parent. [This is Part A of the solution on bug 1160365].
Then, whenever a partner is created or modified:
- if it is a commercial entity, the commercial fields are synced
downstream to all its descendants, stopping at company boundaries.
- if is is not a commercial entity, the commercial fields are
synced from its parent commercial entity.
The list of fields to sync is determined by calling the new
res.partner method `_commercial_fields()` which can be easily
extended by any module that adds commercial fields on res.partner.
A utility method `open_commercial_entity()` was added to
res.partner to make it easy to include a button for editing the
parent commercial entity, to be displayed instead of now-hidden
commercial fields.
[This is part B of the solution on bug 1160365]
The res.partner.address_get() method (used to find child partners
of certain types, e.g. "invoice") was udpated to take the new
"commercial entity" notion into account: it will now look for
matches in children but also parents and siblings that are part
of the same "commercial entity". The default partner `type` is
now "contact" to reflect the new model ; "default" is
inappropriate because it is a wildcard and would stop the type
lookup early.
[This composes parts C and D of the solution on bug 1160365]
Note: This fix comes with a matching addons fix to implement
module-specific extensions of part B, as well as part E of the
solution.
* Address fields (bug 1160425)
Corrected autosync of address fields is also included in the
same branch, because those two mechanisms are closely related
and share some parts of the implementation.
The `use_parent_address` field now defaults to False (except
in the mini-kanban view of contacts on a partner form), and
the autosync of address fields has been modified to only work
downstream and never update a parent company if a child contact
is modified. Instead, the address fields are now displayed
readonly on contacts that use the parent address, with a button
to edit the parent address (new method open_parent(), similar
to open_commercial_entity() but opening the direct parent).
To make the initial creation of a contact + company pair,
a special case was added: when a new contact is created for
a company that has no other contact and no address, the
address of the contact is assumed to be that of the company
so it is moved to the company, then the `use_parent_address`
flag is enabled on the contact, and the `is_company` flag on
the company. This covers a use case where contact and
company are created on-the-fly when creating a new document.
Many logical flaws in the autosync of address fields have been
corrected and are now covered by unit tests.
* Misc related fixes
- checking `is_company` does not reset the parent_id field
anymore, to allow for multi-level structures. The field is
still hidden automatically because having an empty "Company"
field on the form view of a company is quite suprising), but
this UI behavior is easily customized;
- the `email`, `phone`, `fax`, `mobile`, `lang`, etc. that
were sometimes synced when changing parent company are now
properly left alone;
- the `use_parent_address` field is now always visible next
to the parent_id field when a parent is set
lp bug: https://launchpad.net/bugs/1160425 fixed
lp bug: https://launchpad.net/bugs/1160365 fixed
bzr revid: odo@openerp.com-20130408013742-tm8w0w0nmunanokk
This commit is contained in:
@@ -30,6 +30,7 @@ from openerp import SUPERUSER_ID
|
||||
from openerp import pooler, tools
|
||||
from openerp.osv import osv, fields
|
||||
from openerp.tools.translate import _
|
||||
from openerp.tools.yaml_import import is_comment
|
||||
|
||||
class format_address(object):
|
||||
def fields_view_get_address(self, cr, uid, arch, context={}):
|
||||
@@ -159,8 +160,9 @@ def _lang_get(self, cr, uid, context=None):
|
||||
res = lang_pool.read(cr, uid, ids, ['code', 'name'], context)
|
||||
return [(r['code'], r['name']) for r in res]
|
||||
|
||||
POSTAL_ADDRESS_FIELDS = ('street', 'street2', 'zip', 'city', 'state_id', 'country_id')
|
||||
ADDRESS_FIELDS = POSTAL_ADDRESS_FIELDS + ('email', 'phone', 'fax', 'mobile', 'website', 'ref', 'lang')
|
||||
# fields copy if 'use_parent_address' is checked
|
||||
ADDRESS_FIELDS = ('street', 'street2', 'zip', 'city', 'state_id', 'country_id', 'type')
|
||||
POSTAL_ADDRESS_FIELDS = ADDRESS_FIELDS # deprecated, to remove after 7.0
|
||||
|
||||
class res_partner(osv.osv, format_address):
|
||||
_description = 'Partner'
|
||||
@@ -193,6 +195,18 @@ class res_partner(osv.osv, format_address):
|
||||
result[obj.id] = obj.image != False
|
||||
return result
|
||||
|
||||
def _commercial_id_compute(self, cr, uid, ids, name, args, context=None):
|
||||
""" Returns the partner that is considered the commercial
|
||||
entity of this partner. The commercial entity holds the master data
|
||||
for all commercial fields (see :py:meth:`~_commercial_fields`) """
|
||||
result = dict.fromkeys(ids, False)
|
||||
for partner in self.browse(cr, uid, ids, context=context):
|
||||
current_partner = partner
|
||||
while not current_partner.is_company and current_partner.parent_id:
|
||||
current_partner = current_partner.parent_id
|
||||
result[partner.id] = current_partner.id
|
||||
return result
|
||||
|
||||
_order = "name"
|
||||
_columns = {
|
||||
'name': fields.char('Name', size=128, required=True, select=True),
|
||||
@@ -264,6 +278,9 @@ class res_partner(osv.osv, format_address):
|
||||
'color': fields.integer('Color Index'),
|
||||
'user_ids': fields.one2many('res.users', 'partner_id', 'Users'),
|
||||
'contact_address': fields.function(_address_display, type='char', string='Complete Address'),
|
||||
|
||||
# technical field used for managing commercial fields
|
||||
'commercial_id': fields.function(_commercial_id_compute, type='many2one', relation='res.partner', string='Commercial Entity')
|
||||
}
|
||||
|
||||
def _default_category(self, cr, uid, context=None):
|
||||
@@ -302,8 +319,8 @@ class res_partner(osv.osv, format_address):
|
||||
'company_id': lambda self, cr, uid, ctx: self.pool.get('res.company')._company_default_get(cr, uid, 'res.partner', context=ctx),
|
||||
'color': 0,
|
||||
'is_company': False,
|
||||
'type': 'default',
|
||||
'use_parent_address': True,
|
||||
'type': 'contact', # type 'default' is wildcard and thus inappropriate
|
||||
'use_parent_address': False,
|
||||
'image': False,
|
||||
}
|
||||
|
||||
@@ -318,7 +335,6 @@ class res_partner(osv.osv, format_address):
|
||||
value = {}
|
||||
value['title'] = False
|
||||
if is_company:
|
||||
value['parent_id'] = False
|
||||
domain = {'title': [('domain', '=', 'partner')]}
|
||||
else:
|
||||
domain = {'title': [('domain', '=', 'contact')]}
|
||||
@@ -329,10 +345,11 @@ class res_partner(osv.osv, format_address):
|
||||
""" return val or val.id if val is a browse record """
|
||||
return val if isinstance(val, (bool, int, long, float, basestring)) else val.id
|
||||
|
||||
if use_parent_address and parent_id:
|
||||
if parent_id:
|
||||
parent = self.browse(cr, uid, parent_id, context=context)
|
||||
return {'value': dict((key, value_or_id(parent[key])) for key in ADDRESS_FIELDS)}
|
||||
return {}
|
||||
address_fields = self._address_fields(cr, uid, context=context)
|
||||
return {'value': dict((key, value_or_id(parent[key])) for key in address_fields)}
|
||||
return {'value': {'use_parent_address': False}}
|
||||
|
||||
def onchange_state(self, cr, uid, ids, state_id, context=None):
|
||||
if state_id:
|
||||
@@ -358,56 +375,134 @@ class res_partner(osv.osv, format_address):
|
||||
|
||||
# _constraints = [(_check_ean_key, 'Error: Invalid ean code', ['ean13'])]
|
||||
|
||||
def write(self, cr, uid, ids, vals, context=None):
|
||||
# Update parent and siblings or children records
|
||||
if isinstance(ids, (int, long)):
|
||||
ids = [ids]
|
||||
if vals.get('is_company')==False:
|
||||
vals.update({'child_ids' : [(5,)]})
|
||||
for partner in self.browse(cr, uid, ids, context=context):
|
||||
update_ids = []
|
||||
if partner.is_company:
|
||||
domain_children = [('parent_id', '=', partner.id), ('use_parent_address', '=', True)]
|
||||
update_ids = self.search(cr, uid, domain_children, context=context)
|
||||
elif partner.parent_id:
|
||||
if vals.get('use_parent_address')==True:
|
||||
domain_siblings = [('parent_id', '=', partner.parent_id.id), ('use_parent_address', '=', True)]
|
||||
update_ids = [partner.parent_id.id] + self.search(cr, uid, domain_siblings, context=context)
|
||||
if 'use_parent_address' not in vals and partner.use_parent_address:
|
||||
domain_siblings = [('parent_id', '=', partner.parent_id.id), ('use_parent_address', '=', True)]
|
||||
update_ids = [partner.parent_id.id] + self.search(cr, uid, domain_siblings, context=context)
|
||||
self.update_address(cr, uid, update_ids, vals, context)
|
||||
return super(res_partner,self).write(cr, uid, ids, vals, context=context)
|
||||
|
||||
def create(self, cr, uid, vals, context=None):
|
||||
if context is None:
|
||||
context = {}
|
||||
# Update parent and siblings records
|
||||
if vals.get('parent_id'):
|
||||
if 'use_parent_address' in vals:
|
||||
use_parent_address = vals['use_parent_address']
|
||||
def _update_fields_values(self, cr, uid, partner, fields, context=None):
|
||||
""" Returns dict of write() values for synchronizing ``fields`` """
|
||||
values = {}
|
||||
for field in fields:
|
||||
column = self._all_columns[field].column
|
||||
if column._type == 'one2many':
|
||||
raise AssertionError('One2Many fields cannot be synchronized as part of `commercial_fields` or `address fields`')
|
||||
if column._type == 'many2one':
|
||||
values[field] = partner[field].id if partner[field] else False
|
||||
elif column._type == 'many2many':
|
||||
values[field] = [(6,0,[r.id for r in partner[field] or []])]
|
||||
else:
|
||||
use_parent_address = self.default_get(cr, uid, ['use_parent_address'], context=context)['use_parent_address']
|
||||
values[field] = partner[field]
|
||||
return values
|
||||
|
||||
if use_parent_address:
|
||||
domain_siblings = [('parent_id', '=', vals['parent_id']), ('use_parent_address', '=', True)]
|
||||
update_ids = [vals['parent_id']] + self.search(cr, uid, domain_siblings, context=context)
|
||||
self.update_address(cr, uid, update_ids, vals, context)
|
||||
|
||||
# add missing address keys
|
||||
onchange_values = self.onchange_address(cr, uid, [], use_parent_address,
|
||||
vals['parent_id'], context=context).get('value') or {}
|
||||
vals.update(dict((key, value)
|
||||
for key, value in onchange_values.iteritems()
|
||||
if key in ADDRESS_FIELDS and key not in vals))
|
||||
|
||||
return super(res_partner, self).create(cr, uid, vals, context=context)
|
||||
def _address_fields(self, cr, uid, context=None):
|
||||
""" Returns the list of address fields that are synced from the parent
|
||||
when the `use_parent_address` flag is set. """
|
||||
return ADDRESS_FIELDS
|
||||
|
||||
def update_address(self, cr, uid, ids, vals, context=None):
|
||||
addr_vals = dict((key, vals[key]) for key in POSTAL_ADDRESS_FIELDS if key in vals)
|
||||
address_fields = self._address_fields(cr, uid, context=context)
|
||||
addr_vals = dict((key, vals[key]) for key in address_fields if key in vals)
|
||||
if addr_vals:
|
||||
return super(res_partner, self).write(cr, uid, ids, addr_vals, context)
|
||||
|
||||
def _commercial_fields(self, cr, uid, context=None):
|
||||
""" Returns the list of fields that are managed by the commercial entity
|
||||
to which a partner belongs. These fields are meant to be hidden on
|
||||
partners that aren't `commercial entities` themselves, and will be
|
||||
delegated to the parent `commercial entity`. The list is meant to be
|
||||
extended by inheriting classes. """
|
||||
return ['vat']
|
||||
|
||||
def _commercial_sync_from_company(self, cr, uid, partner, context=None):
|
||||
""" Handle sync of commercial fields when a new parent commercial entity is set,
|
||||
as if they were related fields """
|
||||
if partner.commercial_id != partner:
|
||||
commercial_fields = self._commercial_fields(cr, uid, context=context)
|
||||
sync_vals = self._update_fields_values(cr, uid, partner.commercial_id,
|
||||
commercial_fields, context=context)
|
||||
return self.write(cr, uid, partner.id, sync_vals, context=context)
|
||||
|
||||
def _commercial_sync_to_children(self, cr, uid, partner, context=None):
|
||||
""" Handle sync of commercial fields to descendants """
|
||||
commercial_fields = self._commercial_fields(cr, uid, context=context)
|
||||
sync_vals = self._update_fields_values(cr, uid, partner.commercial_id,
|
||||
commercial_fields, context=context)
|
||||
sync_children = [c for c in partner.child_ids if not c.is_company]
|
||||
for child in sync_children:
|
||||
self._commercial_sync_to_children(cr, uid, child, context=context)
|
||||
return self.write(cr, uid, [c.id for c in sync_children], sync_vals, context=context)
|
||||
|
||||
def _fields_sync(self, cr, uid, partner, update_values, context=None):
|
||||
""" Sync commercial fields and address fields from company and to children after create/update,
|
||||
just as if those were all modeled as fields.related to the parent """
|
||||
# 1. From UPSTREAM: sync from parent
|
||||
if update_values.get('parent_id') or update_values.get('use_company_address'):
|
||||
# 1a. Commercial fields: sync if parent changed
|
||||
if update_values.get('parent_id'):
|
||||
self._commercial_sync_from_company(cr, uid, partner, context=context)
|
||||
# 1b. Address fields: sync if parent or use_parent changed *and* both are now set
|
||||
if partner.parent_id and partner.use_parent_address:
|
||||
onchange_vals = self.onchange_address(cr, uid, [partner.id],
|
||||
use_parent_address=partner.use_parent_address,
|
||||
parent_id=partner.parent_id.id,
|
||||
context=context).get('value', {})
|
||||
self.update_address(cr, uid, partner.id, onchange_vals, context=context)
|
||||
|
||||
# 2. To DOWNSTREAM: sync children
|
||||
if partner.child_ids:
|
||||
# 2a. Commercial Fields: sync if commercial entity
|
||||
if partner.commercial_id == partner:
|
||||
self._commercial_sync_to_children(cr, uid, partner, context=context)
|
||||
# 2b. Address fields: sync if address changed
|
||||
address_fields = self._address_fields(cr, uid, context=context)
|
||||
if any(field in update_values for field in address_fields):
|
||||
domain_children = [('parent_id', '=', partner.id), ('use_parent_address', '=', True)]
|
||||
update_ids = self.search(cr, uid, domain_children, context=context)
|
||||
self.update_address(cr, uid, update_ids, update_values, context=context)
|
||||
|
||||
def _handle_first_contact_creation(self, cr, uid, partner, context=None):
|
||||
""" On creation of first contact for a company (or root) that has no address, assume contact address
|
||||
was meant to be company address """
|
||||
parent = partner.parent_id
|
||||
address_data_fields = list(set(self._address_fields(cr, uid, context=context)).difference(['type']))
|
||||
if parent and (parent.is_company or not parent.parent_id) and len(parent.child_ids) == 1 and \
|
||||
any(partner[f] for f in address_data_fields) and not any(parent[f] for f in address_data_fields):
|
||||
addr_vals = self._update_fields_values(cr, uid, partner, address_data_fields, context=context)
|
||||
parent.update_address(addr_vals)
|
||||
if not parent.is_company:
|
||||
parent.write({'is_company': True})
|
||||
|
||||
def write(self, cr, uid, ids, vals, context=None):
|
||||
if isinstance(ids, (int, long)):
|
||||
ids = [ids]
|
||||
result = super(res_partner,self).write(cr, uid, ids, vals, context=context)
|
||||
for partner in self.browse(cr, uid, ids, context=context):
|
||||
self._fields_sync(cr, uid, partner, vals, context)
|
||||
return result
|
||||
|
||||
def create(self, cr, uid, vals, context=None):
|
||||
new_id = super(res_partner, self).create(cr, uid, vals, context=context)
|
||||
partner = self.browse(cr, uid, new_id, context=context)
|
||||
self._fields_sync(cr, uid, partner, vals, context)
|
||||
self._handle_first_contact_creation(cr, uid, partner, context)
|
||||
return new_id
|
||||
|
||||
def open_commercial_entity(self, cr, uid, ids, context=None):
|
||||
""" Utility method used to add an "Open Company" button in partner views """
|
||||
partner = self.browse(cr, uid, ids[0], context=context)
|
||||
return {'type': 'ir.actions.act_window',
|
||||
'res_model': 'res.partner',
|
||||
'view_mode': 'form',
|
||||
'res_id': partner.commercial_id.id,
|
||||
'target': 'new',
|
||||
'flags': {'form': {'action_buttons': True}}}
|
||||
|
||||
def open_parent(self, cr, uid, ids, context=None):
|
||||
""" Utility method used to add an "Open Parent" button in partner views """
|
||||
partner = self.browse(cr, uid, ids[0], context=context)
|
||||
return {'type': 'ir.actions.act_window',
|
||||
'res_model': 'res.partner',
|
||||
'view_mode': 'form',
|
||||
'res_id': partner.parent_id.id,
|
||||
'target': 'new',
|
||||
'flags': {'form': {'action_buttons': True}}}
|
||||
|
||||
def name_get(self, cr, uid, ids, context=None):
|
||||
if context is None:
|
||||
context = {}
|
||||
@@ -516,25 +611,42 @@ class res_partner(osv.osv, format_address):
|
||||
ids = ids[16:]
|
||||
return True
|
||||
|
||||
def address_get(self, cr, uid, ids, adr_pref=None):
|
||||
if adr_pref is None:
|
||||
adr_pref = ['default']
|
||||
def address_get(self, cr, uid, ids, adr_pref=None, context=None):
|
||||
""" Find contacts/addresses of the right type(s) by doing a depth-first-search
|
||||
through descendants within company boundaries (stop at entities flagged ``is_company``)
|
||||
then continuing the search at the ancestors that are within the same company boundaries.
|
||||
Defaults to partners of type ``'default'`` when the exact type is not found, or to the
|
||||
commercial entity if no type ``'default'`` is found either. """
|
||||
adr_pref = set(adr_pref or [])
|
||||
if 'default' not in adr_pref:
|
||||
adr_pref.add('default')
|
||||
result = {}
|
||||
# retrieve addresses from the partner itself and its children
|
||||
res = []
|
||||
# need to fix the ids ,It get False value in list like ids[False]
|
||||
if ids and ids[0]!=False:
|
||||
for p in self.browse(cr, uid, ids):
|
||||
res.append((p.type, p.id))
|
||||
res.extend((c.type, c.id) for c in p.child_ids)
|
||||
address_dict = dict(reversed(res))
|
||||
# get the id of the (first) default address if there is one,
|
||||
# otherwise get the id of the first address in the list
|
||||
default_address = False
|
||||
if res:
|
||||
default_address = address_dict.get('default', res[0][1])
|
||||
for adr in adr_pref:
|
||||
result[adr] = address_dict.get(adr, default_address)
|
||||
visited = set()
|
||||
for partner in self.browse(cr, uid, filter(None, ids), context=context):
|
||||
current_partner = partner
|
||||
while current_partner:
|
||||
to_scan = [current_partner]
|
||||
# Scan descendants, DFS
|
||||
while to_scan:
|
||||
record = to_scan.pop(0)
|
||||
visited.add(record)
|
||||
if record.type in adr_pref and not result.get(record.type):
|
||||
result[record.type] = record.id
|
||||
if len(result) == len(adr_pref):
|
||||
return result
|
||||
to_scan = [c for c in record.child_ids
|
||||
if c not in visited
|
||||
if not c.is_company] + to_scan
|
||||
|
||||
# Continue scanning at ancestor if current_partner is not a commercial entity
|
||||
if current_partner.is_company or not current_partner.parent_id:
|
||||
break
|
||||
current_partner = current_partner.parent_id
|
||||
|
||||
# default to type 'default' or the commercial_entity of the main/last partner
|
||||
default = result.get('default', partner.commercial_id.id)
|
||||
for adr_type in adr_pref:
|
||||
result[adr_type] = result.get(adr_type) or default
|
||||
return result
|
||||
|
||||
def view_header_get(self, cr, uid, view_id, view_type, context):
|
||||
|
||||
@@ -140,7 +140,12 @@
|
||||
placeholder="Company"
|
||||
domain="[('is_company', '=', True)]" context="{'default_is_company': True}"
|
||||
attrs="{'invisible': [('is_company','=', True)]}"
|
||||
on_change="onchange_address(use_parent_address, parent_id)"/>
|
||||
on_change="onchange_address(use_parent_address, parent_id)" class="oe_inline"/>
|
||||
<field name="use_parent_address" class="oe_edit_only oe_inline"
|
||||
on_change="onchange_address(use_parent_address, parent_id)"
|
||||
attrs="{'invisible': [('parent_id','=', False)]}"/>
|
||||
<label for="use_parent_address" class="oe_edit_only"
|
||||
attrs="{'invisible': [('parent_id','=', False)]}"/>
|
||||
<field name="category_id" widget="many2many_tags" placeholder="Tags..."/>
|
||||
</div>
|
||||
<div class="oe_right oe_button_box" name="buttons"> </div>
|
||||
@@ -151,21 +156,20 @@
|
||||
<div attrs="{'invisible': [('parent_id','=', False)]}" name="div_type">
|
||||
<field class="oe_inline"
|
||||
name="type"/>
|
||||
<label for="use_parent_address" class="oe_edit_only"/>
|
||||
<field name="use_parent_address" class="oe_edit_only oe_inline"
|
||||
on_change="onchange_address(use_parent_address, parent_id)"/>
|
||||
</div>
|
||||
|
||||
<label for="street" string="Address"/>
|
||||
<div>
|
||||
<field name="street" placeholder="Street..."/>
|
||||
<field name="street2"/>
|
||||
<button name="open_parent" type="object" string="(edit company address)" class="oe_link oe_edit_only"
|
||||
attrs="{'invisible': ['|',('parent_id','=', False),('use_parent_address','=',False)]}"/>
|
||||
<field name="street" placeholder="Street..." attrs="{'readonly': [('use_parent_address','=',True)]}"/>
|
||||
<field name="street2" attrs="{'readonly': [('use_parent_address','=',True)]}"/>
|
||||
<div class="address_format">
|
||||
<field name="city" placeholder="City" style="width: 40%%"/>
|
||||
<field name="state_id" class="oe_no_button" placeholder="State" style="width: 37%%" options='{"no_open": True}' on_change="onchange_state(state_id)"/>
|
||||
<field name="zip" placeholder="ZIP" style="width: 20%%"/>
|
||||
<field name="city" placeholder="City" style="width: 40%%" attrs="{'readonly': [('use_parent_address','=',True)]}"/>
|
||||
<field name="state_id" class="oe_no_button" placeholder="State" style="width: 37%%" options='{"no_open": True}' on_change="onchange_state(state_id)" attrs="{'readonly': [('use_parent_address','=',True)]}"/>
|
||||
<field name="zip" placeholder="ZIP" style="width: 20%%" attrs="{'readonly': [('use_parent_address','=',True)]}"/>
|
||||
</div>
|
||||
<field name="country_id" placeholder="Country" class="oe_no_button" options='{"no_open": True}'/>
|
||||
<field name="country_id" placeholder="Country" class="oe_no_button" options='{"no_open": True}' attrs="{'readonly': [('use_parent_address','=',True)]}"/>
|
||||
</div>
|
||||
<field name="website" widget="url" placeholder="e.g. www.openerp.com"/>
|
||||
</group>
|
||||
@@ -183,7 +187,7 @@
|
||||
|
||||
<notebook colspan="4">
|
||||
<page string="Contacts" attrs="{'invisible': [('is_company','=',False)]}">
|
||||
<field name="child_ids" context="{'default_parent_id': active_id}" mode="kanban">
|
||||
<field name="child_ids" context="{'default_parent_id': active_id, 'default_use_parent_address': 1}" mode="kanban">
|
||||
<kanban>
|
||||
<field name="color"/>
|
||||
<field name="name"/>
|
||||
@@ -252,6 +256,7 @@
|
||||
<field name="image" widget='image' class="oe_avatar oe_left" options='{"preview_image": "image_medium"}'/>
|
||||
<div class="oe_title">
|
||||
<group>
|
||||
<field name="use_parent_address"/>
|
||||
<field name="name"/>
|
||||
<field name="category_id" widget="many2many_tags" placeholder="Tags..."/>
|
||||
<field name="function" placeholder="e.g. Sales Director"/>
|
||||
|
||||
@@ -39,5 +39,211 @@ class test_base(common.TransactionCase):
|
||||
self.assertTrue(new_id2 > new_id, 'find_or_create failed - should have created new one again')
|
||||
|
||||
|
||||
def test_20_res_partner_address_sync(self):
|
||||
cr, uid = self.cr, self.uid
|
||||
ghoststep = self.res_partner.browse(cr, uid, self.res_partner.create(cr, uid,
|
||||
{'name': 'GhostStep',
|
||||
'is_company': True,
|
||||
'street': 'Main Street, 10',
|
||||
'phone': '123456789',
|
||||
'email': 'info@ghoststep.com',
|
||||
'vat': 'BE0477472701'}))
|
||||
p1 = self.res_partner.browse(cr, uid, self.res_partner.name_create(cr, uid, 'Denis Bladesmith <denis.bladesmith@ghoststep.com>')[0])
|
||||
p1phone = '123456789#34'
|
||||
p1.write({'phone': p1phone,
|
||||
'parent_id': ghoststep.id,
|
||||
'use_parent_address': True})
|
||||
p1.refresh()
|
||||
self.assertEqual(p1.street, ghoststep.street, 'Address fields must be synced')
|
||||
self.assertEqual(p1.phone, p1phone, 'Phone should be preserved after address sync')
|
||||
self.assertEqual(p1.email, 'denis.bladesmith@ghoststep.com', 'Email should be preserved after sync')
|
||||
ghoststreet = 'South Street, 25'
|
||||
ghoststep.write({'street': ghoststreet})
|
||||
p1.refresh()
|
||||
self.assertEqual(p1.street, ghoststreet, 'Address fields must be synced automatically')
|
||||
self.assertEqual(p1.phone, p1phone, 'Phone should not be synced')
|
||||
self.assertEqual(p1.email, 'denis.bladesmith@ghoststep.com', 'Email should be preserved after sync')
|
||||
|
||||
p1street = 'My Street, 11'
|
||||
p1.write({'street': p1street})
|
||||
ghoststep.refresh()
|
||||
self.assertEqual(ghoststep.street, ghoststreet, 'Touching contact should never alter parent')
|
||||
|
||||
|
||||
def test_30_res_partner_first_contact_sync(self):
|
||||
""" Test initial creation of company/contact pair where contact address gets copied to
|
||||
company """
|
||||
cr, uid = self.cr, self.uid
|
||||
ironshield = self.res_partner.browse(cr, uid, self.res_partner.name_create(cr, uid, 'IronShield')[0])
|
||||
self.assertFalse(ironshield.is_company, 'Partners are not companies by default')
|
||||
self.assertFalse(ironshield.use_parent_address, 'use_parent_address defaults to False')
|
||||
self.assertEqual(ironshield.type, 'contact', 'Default type must be "contact"')
|
||||
p1 = self.res_partner.browse(cr, uid, self.res_partner.create(cr, uid,
|
||||
{'name': 'Isen Hardearth',
|
||||
'street': 'Strongarm Avenue, 12',
|
||||
'parent_id': ironshield.id}))
|
||||
self.assertEquals(p1.type, 'contact', 'Default type must be "contact"')
|
||||
ironshield.refresh()
|
||||
self.assertEqual(ironshield.street, p1.street, 'Address fields should be copied to company')
|
||||
self.assertTrue(ironshield.is_company, 'Company flag should be turned on after first contact creation')
|
||||
|
||||
def test_40_res_partner_address_getc(self):
|
||||
""" Test address_get address resolution mechanism: it should first go down through descendants,
|
||||
stopping when encountering another is_copmany entity, then go up, stopping again at the first
|
||||
is_company entity or the root ancestor and if nothing matches, it should use the commercial entity
|
||||
of the partner (which could be itself) """
|
||||
cr, uid = self.cr, self.uid
|
||||
elmtree = self.res_partner.browse(cr, uid, self.res_partner.name_create(cr, uid, 'Elmtree')[0])
|
||||
branch1 = self.res_partner.browse(cr, uid, self.res_partner.create(cr, uid, {'name': 'Branch 1',
|
||||
'parent_id': elmtree.id,
|
||||
'is_company': True}))
|
||||
leaf10 = self.res_partner.browse(cr, uid, self.res_partner.create(cr, uid, {'name': 'Leaf 10',
|
||||
'parent_id': branch1.id,
|
||||
'type': 'invoice'}))
|
||||
branch11 = self.res_partner.browse(cr, uid, self.res_partner.create(cr, uid, {'name': 'Branch 11',
|
||||
'parent_id': branch1.id,
|
||||
'type': 'other'}))
|
||||
leaf111 = self.res_partner.browse(cr, uid, self.res_partner.create(cr, uid, {'name': 'Leaf 111',
|
||||
'parent_id': branch11.id,
|
||||
'type': 'delivery'}))
|
||||
branch11.write({'is_company': False}) # force is_company after creating 1rst child
|
||||
branch2 = self.res_partner.browse(cr, uid, self.res_partner.create(cr, uid, {'name': 'Branch 2',
|
||||
'parent_id': elmtree.id,
|
||||
'is_company': True}))
|
||||
leaf21 = self.res_partner.browse(cr, uid, self.res_partner.create(cr, uid, {'name': 'Leaf 21',
|
||||
'parent_id': branch2.id,
|
||||
'type': 'delivery'}))
|
||||
leaf22 = self.res_partner.browse(cr, uid, self.res_partner.create(cr, uid, {'name': 'Leaf 22',
|
||||
'parent_id': branch2.id}))
|
||||
leaf23 = self.res_partner.browse(cr, uid, self.res_partner.create(cr, uid, {'name': 'Leaf 23',
|
||||
'parent_id': branch2.id,
|
||||
'type': 'default'}))
|
||||
# go up, stop at branch1
|
||||
self.assertEqual(self.res_partner.address_get(cr, uid, [leaf111.id], ['delivery', 'invoice', 'contact', 'other', 'default']),
|
||||
{'delivery': leaf111.id,
|
||||
'invoice': leaf10.id,
|
||||
'contact': branch1.id,
|
||||
'other': branch11.id,
|
||||
'default': branch1.id}, 'Invalid address resolution')
|
||||
self.assertEqual(self.res_partner.address_get(cr, uid, [branch11.id], ['delivery', 'invoice', 'contact', 'other', 'default']),
|
||||
{'delivery': leaf111.id,
|
||||
'invoice': leaf10.id,
|
||||
'contact': branch1.id,
|
||||
'other': branch11.id,
|
||||
'default': branch1.id}, 'Invalid address resolution')
|
||||
|
||||
# go down, stop at at all child companies
|
||||
self.assertEqual(self.res_partner.address_get(cr, uid, [elmtree.id], ['delivery', 'invoice', 'contact', 'other', 'default']),
|
||||
{'delivery': elmtree.id,
|
||||
'invoice': elmtree.id,
|
||||
'contact': elmtree.id,
|
||||
'other': elmtree.id,
|
||||
'default': elmtree.id}, 'Invalid address resolution')
|
||||
|
||||
# go down through children
|
||||
self.assertEqual(self.res_partner.address_get(cr, uid, [branch1.id], ['delivery', 'invoice', 'contact', 'other', 'default']),
|
||||
{'delivery': leaf111.id,
|
||||
'invoice': leaf10.id,
|
||||
'contact': branch1.id,
|
||||
'other': branch11.id,
|
||||
'default': branch1.id}, 'Invalid address resolution')
|
||||
self.assertEqual(self.res_partner.address_get(cr, uid, [branch2.id], ['delivery', 'invoice', 'contact', 'other', 'default']),
|
||||
{'delivery': leaf21.id,
|
||||
'invoice': leaf23.id,
|
||||
'contact': branch2.id,
|
||||
'other': leaf23.id,
|
||||
'default': leaf23.id}, 'Invalid address resolution')
|
||||
|
||||
# go up then down through siblings
|
||||
self.assertEqual(self.res_partner.address_get(cr, uid, [leaf21.id], ['delivery', 'invoice', 'contact', 'other', 'default']),
|
||||
{'delivery': leaf21.id,
|
||||
'invoice': leaf23.id,
|
||||
'contact': branch2.id,
|
||||
'other': leaf23.id,
|
||||
'default': leaf23.id
|
||||
}, 'Invalid address resolution, should scan commercial entity ancestor and its descendants')
|
||||
self.assertEqual(self.res_partner.address_get(cr, uid, [leaf22.id], ['delivery', 'invoice', 'contact', 'other', 'default']),
|
||||
{'delivery': leaf21.id,
|
||||
'invoice': leaf23.id,
|
||||
'contact': leaf22.id,
|
||||
'other': leaf23.id,
|
||||
'default': leaf23.id}, 'Invalid address resolution, should scan commercial entity ancestor and its descendants')
|
||||
self.assertEqual(self.res_partner.address_get(cr, uid, [leaf23.id], ['delivery', 'invoice', 'contact', 'other', 'default']),
|
||||
{'delivery': leaf21.id,
|
||||
'invoice': leaf23.id,
|
||||
'contact': branch2.id,
|
||||
'other': leaf23.id,
|
||||
'default': leaf23.id}, 'Invalid address resolution, `default` should only override if no partner with specific type exists')
|
||||
|
||||
# empty adr_pref means only 'default'
|
||||
self.assertEqual(self.res_partner.address_get(cr, uid, [elmtree.id], []),
|
||||
{'default': elmtree.id}, 'Invalid address resolution, no default means commercial entity ancestor')
|
||||
self.assertEqual(self.res_partner.address_get(cr, uid, [leaf111.id], []),
|
||||
{'default': branch1.id}, 'Invalid address resolution, no default means commercial entity ancestor')
|
||||
branch11.write({'type': 'default'})
|
||||
self.assertEqual(self.res_partner.address_get(cr, uid, [leaf111.id], []),
|
||||
{'default': branch11.id}, 'Invalid address resolution, branch11 should now be default')
|
||||
|
||||
|
||||
def test_50_res_partner_commercial_sync(self):
|
||||
cr, uid = self.cr, self.uid
|
||||
p0 = self.res_partner.browse(cr, uid, self.res_partner.create(cr, uid,
|
||||
{'name': 'Sigurd Sunknife',
|
||||
'email': 'ssunknife@gmail.com'}))
|
||||
sunhelm = self.res_partner.browse(cr, uid, self.res_partner.create(cr, uid,
|
||||
{'name': 'Sunhelm',
|
||||
'is_company': True,
|
||||
'street': 'Rainbow Street, 13',
|
||||
'phone': '1122334455',
|
||||
'email': 'info@sunhelm.com',
|
||||
'vat': 'BE0477472701',
|
||||
'child_ids': [(4, p0.id),
|
||||
(0, 0, {'name': 'Alrik Greenthorn',
|
||||
'email': 'agr@sunhelm.com'})],
|
||||
}))
|
||||
p1 = self.res_partner.browse(cr, uid, self.res_partner.create(cr, uid,
|
||||
{'name': 'Otto Blackwood',
|
||||
'email': 'otto.blackwood@sunhelm.com',
|
||||
'parent_id': sunhelm.id}))
|
||||
p11 = self.res_partner.browse(cr, uid, self.res_partner.create(cr, uid,
|
||||
{'name': 'Gini Graywool',
|
||||
'email': 'ggr@sunhelm.com',
|
||||
'parent_id': p1.id}))
|
||||
p2 = self.res_partner.browse(cr, uid, self.res_partner.search(cr, uid,
|
||||
[('email', '=', 'agr@sunhelm.com')])[0])
|
||||
|
||||
for p in (p0, p1, p11, p2):
|
||||
p.refresh()
|
||||
self.assertEquals(p.commercial_id, sunhelm, 'Incorrect commercial entity resolution')
|
||||
self.assertEquals(p.vat, sunhelm.vat, 'Commercial fields must be automatically synced')
|
||||
sunhelmvat = 'BE0123456789'
|
||||
sunhelm.write({'vat': sunhelmvat})
|
||||
for p in (p0, p1, p11, p2):
|
||||
p.refresh()
|
||||
self.assertEquals(p.vat, sunhelmvat, 'Commercial fields must be automatically and recursively synced')
|
||||
|
||||
p1vat = 'BE0987654321'
|
||||
p1.write({'vat': p1vat})
|
||||
for p in (sunhelm, p0, p11, p2):
|
||||
p.refresh()
|
||||
self.assertEquals(p.vat, sunhelmvat, 'Sync to children should only work downstream and on commercial entities')
|
||||
|
||||
# promote p1 to commercial entity
|
||||
vals = p1.onchange_type(is_company=True)['value']
|
||||
p1.write(dict(vals, parent_id=sunhelm.id,
|
||||
is_company=True,
|
||||
name='Sunhelm Subsidiary'))
|
||||
p1.refresh()
|
||||
self.assertEquals(p1.vat, p1vat, 'Setting is_company should stop auto-sync of commercial fields')
|
||||
self.assertEquals(p1.commercial_id, p1, 'Incorrect commercial entity resolution after setting is_company')
|
||||
|
||||
# writing on parent should not touch child commercial entities
|
||||
sunhelmvat2 = 'BE0112233445'
|
||||
sunhelm.write({'vat': sunhelmvat2})
|
||||
p1.refresh()
|
||||
self.assertEquals(p1.vat, p1vat, 'Setting is_company should stop auto-sync of commercial fields')
|
||||
p0.refresh()
|
||||
self.assertEquals(p0.vat, sunhelmvat2, 'Commercial fields must be automatically synced')
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest2.main()
|
||||
Reference in New Issue
Block a user