第一章 联系人

我们知道odoo中的联系人对象(res.partner)是包含了多个实体概念的广义的模型,包括并不限于客户、供应商、员工。那么联系人究竟有哪些属性并包含哪些特性?这就是本章要着力讲清楚的。

没有明确标识的情况下,本章基于odoo16.0。

联系人的基础属性

我们先来看联系人的代码定义:

class Partner(models.Model):
    _description = 'Contact'
    _inherit = ['format.address.mixin', 'avatar.mixin']
    _name = "res.partner"
    _order = "display_name, id"
    _rec_names_search = ['display_name', 'email', 'ref', 'vat', 'company_registry']  # TODO vat must be sanitized the same way for storing/searching

从联系人模型的定义上,我们可以看出,联系人是地址混合类头像混合类的子类。它的排序依据显示名称倒序排列。联系人在搜索时可以默认搜索的属性有:

  • 显示名称
  • 邮箱
  • 参考
  • 税号
  • 公司注册信息

联系人的类型

联系人默认有5种类型:

  • 联系人(contact): 用来管理合作伙伴公司的员工
  • 发票地址(invoice): 用来给合伙伙伴开具invocie的地址
  • 发货地址(delivery): 用来给客户发货的地址
  • 私有地址(private): 需要授权才可以访问的敏感地址(雇员家庭地址)
  • 其他(other): 公司的其他类型地址

公司类型

我们在用户UI上可以看到一个公司/个人的字段类型,可以供我们更新联系人的公司类型。

# company_type is only an interface field, do not use it in business logic
company_type = fields.Selection(string='Company Type',
    selection=[('person', 'Individual'), ('company', 'Company')],
    compute='_compute_company_type', inverse='_write_company_type')

但是正如代码注释一样,我们在业务逻辑的编写过程中,不能直接使用company_type, 而应该使用is_company字段来判断是否是公司。

联系人与公司属性同步

联系人与公司属性可以做到同步,如果用户更改了公司的某些字段属性,那么该公司下所有联系人的属性也要随之改变。这个特性原生odoo就已经支持,下面我们来看一下具体如何实现。

首先,我们先明确一点,在联系人对象中存在一个商业实体属性概念,所有的商业实体属性都对于非商业实体类型的联系人,都应该隐藏,且被商业实体托管。举个简单的列子,欧姆网络科技的一个客户叫薛定谔信息技术公司,该公司有个税号,A1234。薛定谔信息技术公司的CEO薛定谔同样作为联系人存在我们的系统中。那么对于薛定谔这个人,他本身是没有税号的,那么他的税号应该继承自他所属的公司薛定谔信息技术。

这个例子中,税号就是商业实体属性。薛定谔信息技术公司是商业实体,而薛定谔是非商业实体。

在联系人模型中,有一个私有方法用来返回联系人的商业实体属性: _commercial_fields:

@api.model
def _commercial_fields(self):
    """ 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', 'company_registry', 'industry_id']

由此,我们可以看到默认的商业实体属性是:vat,company_registry,industry_id。如果我们想要扩展更多的属性,只需要继承这个方法,把这个列表拓展即可。

认识了商业实体属性之后,我们再来看,联系人模型是如何将商业实体的商业实体属性同步到非商业实体中的。

在联系人的创建和编辑方法中,我们都看到了一个私有方法:_fields_sync的影子。

@api.model_create_multi
def create(self, vals_list):
    ...

    for partner, vals in zip(partners, vals_list):
        partner._fields_sync(vals)
        # Lang: propagate from parent if no value was given
        if 'lang' not in vals and partner.parent_id:
            partner._onchange_parent_id_for_lang()
        partner._handle_first_contact_creation()
    return partners

def write(self,vals):
    ...

    for partner in self:
        if any(u._is_internal() for u in partner.user_ids if u != self.env.user):
            self.env['res.users'].check_access_rights('write')
        partner._fields_sync(vals)
    return result

_fields_sync方法是专门用来同步商业实体和非商业实体之间的字段的。

def _fields_sync(self, values):
    """ 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 values.get('parent_id') or values.get('type') == 'contact':
        # 1a. Commercial fields: sync if parent changed
        if values.get('parent_id'):
            self.sudo()._commercial_sync_from_company()
        # 1b. Address fields: sync if parent or use_parent changed *and* both are now set
        if self.parent_id and self.type == 'contact':
            onchange_vals = self.onchange_parent_id().get('value', {})
            self.update_address(onchange_vals)

    # 2. To DOWNSTREAM: sync children
    self._children_sync(values)

_fields_sync同步规则是:

  • 如果非商业实体的父级发生改变,那么从新的父级对象继承商业实体属性。
  • 如果实例是商业实体,将本实例的商业实体属性同步至子联系人(非商业实体)。

这里的代码揭示了另外一个暗线,商业实体的地址发生改变时也会同步到子联系人中。

国家格地址格式化变量

我们知道odoo中国家的地址格式支持的变量如下:

  • state_code: 省份代码
  • state_name: 省份名称
  • country_code: 国家代码
  • country_name: 国家名称
  • company_name: 公司名称

如果我们希望扩展这个列表,改怎么做呢?

首先,我们需要拓展res.country这个模型的_check_address_format方法:

@api.constrains('address_format')
def _check_address_format(self):
    for record in self:
        if record.address_format:
            address_fields = self.env['res.partner']._formatting_address_fields() + ['state_code', 'state_name', 'country_code', 'country_name', 'company_name']
            try:
                record.address_format % {i: 1 for i in address_fields}
            except (ValueError, KeyError):
                raise UserError(_('The layout contains an invalid format key'))

在拓展列表中把对应的拓展值加入

其次,我们需要在res.partner模型中的_prepare_display_address方法中把响应的占位符填充:

def _prepare_display_address(self, without_company=False):
    # get the information that will be injected into the display format
    # get the address format
    address_format = self._get_address_format()
    args = defaultdict(str, {
        'state_code': self.state_id.code or '',
        'state_name': self.state_id.name or '',
        'country_code': self.country_id.code or '',
        'country_name': self._get_country_name(),
        'company_name': self.commercial_company_name or '',
    })
    for field in self._formatting_address_fields():
        args[field] = getattr(self, field) or ''
    if without_company:
        args['company_name'] = ''
    elif self.commercial_company_name:
        address_format = '%(company_name)s\n' + address_format
    return address_format, args

联系人电话格式

如果安装了phone_validation模块,那么odoo将根据联系人的国家来自动匹配电话格式。

phone_validation模块依赖python库phonenumbers,如果你的系统中缺少此依赖,并不会提示异常,电话号码将以原格式返回

支持的电话格式有如下几种:

  • E164: 格式的电话号码最多可以包含 15 位数字,并且通常包括国家代码、国家/地区代码和订户号码, 例如+861012345678。
  • INTERNATIONAL: 这是指包含国家代码的完整电话号码格式,通常用于国际电话拨号,例如: +86 10 12345678
  • NATIONAL: 这是指不包含国家代码的电话号码格式,通常用于国内电话拨号,例如:12345678
  • RFC3966: 这是一个由互联网工程任务组(IETF)定义的 URI(统一资源标识符)方案,用于表示电话号码,RFC3966 格式的电话号码通常以 tel: 开头,并可以包含国家代码、区域代码和其他参数。例如tel:+86-10-12345678

results matching ""

    No results matching ""