diff --git a/openerp/addons/base/res/res_partner.py b/openerp/addons/base/res/res_partner.py
index 72c221c5534..1193abcd862 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 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):
diff --git a/openerp/addons/base/res/res_partner_view.xml b/openerp/addons/base/res/res_partner_view.xml
index 71c789af1a7..3650b4d6d9d 100644
--- a/openerp/addons/base/res/res_partner_view.xml
+++ b/openerp/addons/base/res/res_partner_view.xml
@@ -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"/>
+