diff --git a/openerp/addons/base/res/res_company.py b/openerp/addons/base/res/res_company.py
index 022c9c06da0..cd17f6da3d9 100644
--- a/openerp/addons/base/res/res_company.py
+++ b/openerp/addons/base/res/res_company.py
@@ -97,7 +97,7 @@ class res_company(osv.osv):
address_data = part_obj.address_get(cr, uid, [company.partner_id.id], adr_pref=['default'])
address = address_data['default']
if address:
- part_obj.write(cr, uid, [address], {name: value or False})
+ part_obj.write(cr, uid, [address], {name: value or False}, context=context)
else:
part_obj.create(cr, uid, {name: value or False, 'parent_id': company.partner_id.id}, context=context)
return True
@@ -253,7 +253,7 @@ class res_company(osv.osv):
vals.update({'partner_id': partner_id})
self.cache_restart(cr)
company_id = super(res_company, self).create(cr, uid, vals, context=context)
- obj_partner.write(cr, uid, partner_id, {'company_id': company_id}, context=context)
+ obj_partner.write(cr, uid, [partner_id], {'company_id': company_id}, context=context)
return company_id
def write(self, cr, uid, ids, values, context=None):
diff --git a/openerp/addons/base/res/res_partner.py b/openerp/addons/base/res/res_partner.py
index 31e2d44b306..d140fe62ccc 100644
--- a/openerp/addons/base/res/res_partner.py
+++ b/openerp/addons/base/res/res_partner.py
@@ -30,6 +30,7 @@ from openerp import SUPERUSER_ID
from openerp import 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,8 @@ 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')
class res_partner(osv.osv, format_address):
_description = 'Partner'
@@ -193,13 +194,42 @@ class res_partner(osv.osv, format_address):
result[obj.id] = obj.image != False
return result
- _order = "name"
+ def _commercial_partner_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
+
+ def _display_name_compute(self, cr, uid, ids, name, args, context=None):
+ return dict(self.name_get(cr, uid, ids, context=context))
+
+ # indirections to avoid passing a copy of the overridable method when declaring the function field
+ _commercial_partner_id = lambda self, *args, **kwargs: self._commercial_partner_compute(*args, **kwargs)
+ _display_name = lambda self, *args, **kwargs: self._display_name_compute(*args, **kwargs)
+
+ _commercial_partner_store_triggers = {
+ 'res.partner': (lambda self,cr,uid,ids,context=None: self.search(cr, uid, [('id','child_of',ids)]),
+ ['parent_id', 'is_company'], 10)
+ }
+ _display_name_store_triggers = {
+ 'res.partner': (lambda self,cr,uid,ids,context=None: self.search(cr, uid, [('id','child_of',ids)]),
+ ['parent_id', 'is_company', 'name'], 10)
+ }
+
+ _order = "display_name"
_columns = {
'name': fields.char('Name', size=128, required=True, select=True),
+ 'display_name': fields.function(_display_name, type='char', string='Name', store=_display_name_store_triggers),
'date': fields.date('Date', select=1),
'title': fields.many2one('res.partner.title', 'Title'),
'parent_id': fields.many2one('res.partner', 'Related Company'),
- 'child_ids': fields.one2many('res.partner', 'parent_id', 'Contacts'),
+ 'child_ids': fields.one2many('res.partner', 'parent_id', 'Contacts', domain=[('active','=',True)]), # force "active_test" domain to bypass _search() override
'ref': fields.char('Reference', size=64, select=1),
'lang': fields.selection(_lang_get, 'Language',
help="If the selected language is loaded in the system, all documents related to this contact will be printed in this language. If not, it will be English."),
@@ -264,6 +294,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_partner_id': fields.function(_commercial_partner_id, type='many2one', relation='res.partner', string='Commercial Entity', store=_commercial_partner_store_triggers)
}
def _default_category(self, cr, uid, context=None):
@@ -302,11 +335,15 @@ class res_partner(osv.osv, format_address):
'company_id': lambda self, cr, uid, ctx: self.pool['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,
}
+ _constraints = [
+ (osv.osv._check_recursion, 'You cannot create recursive Partner hierarchies.', ['parent_id']),
+ ]
+
def copy(self, cr, uid, id, default=None, context=None):
if default is None:
default = {}
@@ -318,7 +355,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')]}
@@ -328,11 +364,22 @@ class res_partner(osv.osv, format_address):
def value_or_id(val):
""" 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:
+ result = {}
+ if parent_id:
+ if ids:
+ partner = self.browse(cr, uid, ids[0], context=context)
+ if partner.parent_id and partner.parent_id.id != parent_id:
+ result['warning'] = {'title': _('Warning'),
+ 'message': _('Changing the company of a contact should only be done if it '
+ 'was never correctly set. If an existing contact starts working for a new '
+ 'company then a new contact should be created under that new '
+ 'company. You can use the "Discard" button to abandon this change.')}
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)
+ result['value'] = dict((key, value_or_id(parent[key])) for key in address_fields)
+ else:
+ result['value'] = {'use_parent_address': False}
+ return result
def onchange_state(self, cr, uid, ids, state_id, context=None):
if state_id:
@@ -358,50 +405,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]
- for partner in self.browse(cr, uid, ids, context=context):
- update_ids = []
- if partner.is_company:
- domain_children = [('parent_id', 'child_of', partner.id), ('use_parent_address', '=', True)]
- update_ids = self.search(cr, uid, domain_children, context=context)
- elif partner.parent_id and vals.get('use_parent_address', 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 list(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_partner_id != partner:
+ commercial_fields = self._commercial_fields(cr, uid, context=context)
+ sync_vals = self._update_fields_values(cr, uid, partner.commercial_partner_id,
+ commercial_fields, context=context)
+ partner.write(sync_vals)
+
+ 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_partner_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_parent_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', {})
+ partner.update_address(onchange_vals)
+
+ # 2. To DOWNSTREAM: sync children
+ if partner.child_ids:
+ # 2a. Commercial Fields: sync if commercial entity
+ if partner.commercial_partner_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_fields = self._address_fields(cr, uid, context=context)
+ 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_fields) and not any(parent[f] for f in address_fields):
+ addr_vals = self._update_fields_values(cr, uid, partner, address_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_partner_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 = {}
@@ -410,8 +541,8 @@ class res_partner(osv.osv, format_address):
res = []
for record in self.browse(cr, uid, ids, context=context):
name = record.name
- if record.parent_id:
- name = "%s (%s)" % (name, record.parent_id.name)
+ if record.parent_id and not record.is_company:
+ name = "%s, %s" % (record.parent_id.name, name)
if context.get('show_address'):
name = name + "\n" + self._display_address(cr, uid, record, without_company=True, context=context)
name = name.replace('\n\n','\n')
@@ -450,6 +581,15 @@ class res_partner(osv.osv, format_address):
rec_id = self.create(cr, uid, {self._rec_name: name or email, 'email': email or False}, context=context)
return self.name_get(cr, uid, [rec_id], context)[0]
+ def _search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False, access_rights_uid=None):
+ """ Override search() to always show inactive children when searching via ``child_of`` operator. The ORM will
+ always call search() with a simple domain of the form [('parent_id', 'in', [ids])]. """
+ # a special ``domain`` is set on the ``child_ids`` o2m to bypass this logic, as it uses similar domain expressions
+ if len(args) == 1 and len(args[0]) == 3 and args[0][:2] == ('parent_id','in'):
+ context = dict(context or {}, active_test=False)
+ return super(res_partner, self)._search(cr, user, args, offset=offset, limit=limit, order=order, context=context,
+ count=count, access_rights_uid=access_rights_uid)
+
def name_search(self, cr, uid, name, args=None, operator='ilike', context=None, limit=100):
if not args:
args = []
@@ -510,25 +650,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
+ provided partner itself 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 partner itself
+ default = result.get('default', partner.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):
@@ -570,8 +727,7 @@ class res_partner(osv.osv, format_address):
'country_name': address.country_id and address.country_id.name or '',
'company_name': address.parent_id and address.parent_id.name or '',
}
- address_field = ['title', 'street', 'street2', 'zip', 'city']
- for field in address_field :
+ for field in self._address_fields(cr, uid, context=context):
args[field] = getattr(address, field) or ''
if without_company:
args['company_name'] = ''
diff --git a/openerp/addons/base/res/res_partner_view.xml b/openerp/addons/base/res/res_partner_view.xml
index 88a1266b689..13b500c4cb0 100644
--- a/openerp/addons/base/res/res_partner_view.xml
+++ b/openerp/addons/base/res/res_partner_view.xml
@@ -77,7 +77,7 @@
-
+
@@ -138,8 +138,8 @@
@@ -151,21 +151,24 @@