Big update of base_partner_one2many_phone: new types, add email support

Migration script provided
This commit is contained in:
Alexis de Lattre
2020-01-10 16:02:03 +01:00
parent cc0da43bdc
commit bb23254830
8 changed files with 419 additions and 89 deletions

View File

@@ -1,24 +1,3 @@
# -*- encoding: utf-8 -*-
##############################################################################
#
# Base Partner One2many Phone module for OpenERP
# Copyright (C) 2014 Artisanat Monastique de Provence
# (http://www.barroux.org)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
from . import partner_phone
from .post_install import migrate_to_partner_phone

View File

@@ -7,20 +7,20 @@
{
'name': 'Base Partner One2many Phone',
'version': '10.0.1.0.0',
'version': '10.0.2.0.0',
'category': 'Phone',
'license': 'AGPL-3',
'summary': 'One2many link between partners and phone numbers',
'summary': 'One2many link between partners and phone numbers/emails',
'description': """
Base Partner One2many Phone
===========================
With this module, one partner can have N phone numbers. It adds a new table dedicated to phone numbers and a one2many link between partners and phone numbers.
With this module, one partner can have several phone numbers and several emails. It adds a new table dedicated to phone numbers and emails and a one2many link between partners and phone numbers. This module keeps compatibility with the native behavior of Odoo on phone numbers and emails.
It has been developped by brother Bernard from Barroux Abbey and Alexis de Lattre from Akretion.
""",
'author': 'Barroux',
'website': 'http://www.barroux.org',
'author': 'Akretion',
'website': 'https://akretion.com/',
'depends': ['base_phone', 'sales_team'],
'data': [
'partner_phone_view.xml',

View File

@@ -0,0 +1,75 @@
# -*- coding: utf-8 -*-
# Copyright 2020 Akretion France (http://www.akretion.com/)
# @author: Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import api, SUPERUSER_ID
oldtype2label = {
'1_home': 'Ancien type : Maison',
# '2_mobile': 'Ancien type : Portable',
'3_office': 'Ancien type : Bureau',
'4_home_fax': 'Ancien type : Fax maison',
'5_office_fax': 'Ancien type : Fax bureau',
'6_phone_fax_home': u'Ancien type : Tél/fax maison',
'7_other': 'Ancien type : Autre',
}
def migrate(cr, version):
if not version:
return
with api.Environment.manage():
env = api.Environment(cr, SUPERUSER_ID, {})
rppo = env['res.partner.phone']
wdict = {} # key = partnerID, values = {id: {'type': '1_home', 'phone': '+33'}}
for rec in rppo.search_read([('type', '!=', False)], ['type', 'phone', 'partner_id', 'note']):
if rec['partner_id'][0] not in wdict:
wdict[rec['partner_id'][0]] = {}
wdict[rec['partner_id'][0]][rec['id']] = rec
# first pass for primary phone
for partner_id, xdict in wdict.items():
mig_phone_entries(cr, xdict, '3_phone_primary', '4_phone_secondary', ['1_home', '6_phone_fax_home', '3_office', '7_other'])
mig_phone_entries(cr, xdict, '5_mobile_primary', '6_mobile_secondary', ['2_mobile'])
mig_phone_entries(cr, xdict, '7_fax_primary', '8_fax_secondary', ['4_home_fax', '5_office_fax'])
cr.execute('select id, email from res_partner where email is not null')
for partner in cr.dictfetchall():
email = partner['email'].strip()
if email:
email_split = email.split(',')
# primary:
email_primary = email_split.pop(0).strip()
rppo.create({
'type': '1_email_primary',
'partner_id': partner['id'],
'email': email_primary,
})
cr.execute('UPDATE res_partner set email=%s where id=%s', (email_primary, partner['id']))
for email_sec in email_split:
rppo.create({
'type': '2_email_secondary',
'partner_id': partner['id'],
'email': email_sec.strip(),
})
def mig_phone_entries(cr, xdict, new_type_primary, new_type_secondary, old_type_list):
zdict = {}
for phone_id, values in xdict.items():
if values['type'] in old_type_list:
zdict[phone_id] = values
if zdict:
values_sorted = sorted(zdict.values(), key=lambda x: x['type'])
primary_phone_val = values_sorted[0]
cr.execute("""UPDATE res_partner_phone SET type=%s WHERE id=%s""", (new_type_primary, primary_phone_val['id']))
if not primary_phone_val.get('note') and oldtype2label.get(primary_phone_val['type']):
cr.execute("""UPDATE res_partner_phone SET note=%s WHERE id=%s""", (oldtype2label[primary_phone_val['type']], primary_phone_val['id']))
zdict.pop(primary_phone_val['id'])
for secondary_phone_val in zdict.values():
cr.execute("""UPDATE res_partner_phone SET type=%s WHERE id=%s""", (new_type_secondary, secondary_phone_val['id']))
if not secondary_phone_val.get('note') and oldtype2label.get(secondary_phone_val['type']):
cr.execute("""UPDATE res_partner_phone SET note=%s WHERE id=%s""", (oldtype2label[secondary_phone_val['type']], secondary_phone_val['id']))

View File

@@ -5,9 +5,12 @@
# @author: Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import models, fields, api
from odoo import models, fields, api, _
from odoo.exceptions import ValidationError
from odoo.addons.base_phone.fields import Phone, Fax
import phonenumbers
EMAIL_TYPES = ('1_email_primary', '2_email_secondary')
PHONE_TYPES = ('3_phone_primary', '4_phone_secondary', '5_mobile_primary', '6_mobile_secondary', '7_fax_primary', '8_fax_secondary')
class ResPartnerPhone(models.Model):
@@ -15,25 +18,55 @@ class ResPartnerPhone(models.Model):
_order = 'partner_id, type'
_phone_name_sequence = 8
partner_id = fields.Many2one('res.partner', string='Related Partner')
partner_id = fields.Many2one(
'res.partner', string='Related Partner', index=True, ondelete='cascade')
type = fields.Selection([
('1_home', 'Home'),
('2_mobile', 'Mobile'),
('3_office', 'Office'),
('4_home_fax', 'Home Fax'),
('5_office_fax', 'Office Fax'),
('6_phone_fax_home', 'Phone/fax Home'),
('7_other', 'Other')],
string='Phone Type', required=True)
phone = Phone('Phone', required=True, partner_field='partner_id')
('1_email_primary', 'Primary E-mail'),
('2_email_secondary', 'Secondary E-mail'),
('3_phone_primary', 'Primary Phone'),
('4_phone_secondary', 'Secondary Phone'),
('5_mobile_primary', 'Primary Mobile'),
('6_mobile_secondary', 'Secondary Mobile'),
('7_fax_primary', 'Primary Fax'),
('8_fax_secondary', 'Secondary Fax'),
],
string='Type', required=True, index=True)
phone = Phone('Phone', required=False, partner_field='partner_id')
email = fields.Char(string='E-Mail')
note = fields.Char('Note')
@api.onchange('type')
def type_change(self):
if self.type:
if self.type in EMAIL_TYPES:
self.phone = False
elif self.type in PHONE_TYPES:
self.email = False
@api.constrains('type', 'phone', 'email')
def _check_partner_phone(self):
for rec in self:
if rec.type in EMAIL_TYPES:
if not rec.email:
raise ValidationError(_(
"E-mail field must have a value when type is Primary E-mail or Secondary E-mail."))
if rec.phone:
raise ValidationError(_(
"Phone field must be empty when type is Primary E-mail or Secondary E-mail."))
elif rec.type in PHONE_TYPES:
if not rec.phone:
raise ValidationError(_(
"Phone field must have a value when type is Primary/Secondary Phone, Primary/Secondary Mobile or Primary/Secondary Fax."))
if rec.email:
raise ValidationError(_(
"E-mail field must be empty when type is Primary/Secondary Phone, Primary/Secondary Mobile or Primary/Secondary Fax."))
def name_get(self):
res = []
for pphone in self:
if pphone.partner_id:
if self._context.get('callerid'):
name = pphone.partner_id.name_get()[0][1]
name = pphone.partner_id.display_name
else:
name = u'%s (%s)' % (pphone.phone, pphone.partner_id.name)
else:
@@ -41,45 +74,41 @@ class ResPartnerPhone(models.Model):
res.append((pphone.id, name))
return res
@api.model_cr
def init(self):
self._cr.execute('''
CREATE UNIQUE INDEX IF NOT EXISTS single_email_primary
ON res_partner_phone (partner_id, type)
WHERE (type='1_email_primary')
''')
self._cr.execute('''
CREATE UNIQUE INDEX IF NOT EXISTS single_phone_primary
ON res_partner_phone (partner_id, type)
WHERE (type='3_phone_primary')
''')
self._cr.execute('''
CREATE UNIQUE INDEX IF NOT EXISTS single_mobile_primary
ON res_partner_phone (partner_id, type)
WHERE (type='5_mobile_primary')
''')
self._cr.execute('''
CREATE UNIQUE INDEX IF NOT EXISTS single_fax_primary
ON res_partner_phone (partner_id, type)
WHERE (type='7_fax_primary')
''')
class ResPartner(models.Model):
_inherit = 'res.partner'
@api.model
def convert_from_international_to_e164(self, phone_num):
res = False
try:
res_parse = phonenumbers.parse(phone_num)
res = phonenumbers.format_number(
res_parse, phonenumbers.PhoneNumberFormat.E164)
except:
pass
return res
# without this convert, we would have in DB:
# E.164 format in res_partner_phone table
# phonenumbers.PhoneNumberFormat.INTERNATIONAL in res_partner
# TODO bug: but even with this, it doesn't work, the format
# is stored in international format in res_partner
# => I'll try to find the reason later
@api.multi
@api.depends('phone_ids.phone', 'phone_ids.type')
def _compute_partner_phone(self):
for partner in self:
phone = mobile = fax = False
for partner_phone in partner.phone_ids:
num_e164 = self.convert_from_international_to_e164(
partner_phone.phone)
if num_e164:
if partner_phone.type == '2_mobile':
mobile = num_e164
elif partner_phone.type in ('5_office_fax', '4_home_fax'):
fax = num_e164
else:
phone = num_e164
partner.phone = phone
partner.mobile = mobile
partner.fax = fax
# in v10, we are supposed to have in DB E.164 format
# with the current implementation, we have:
# in res.partner : PhoneNumberFormat.INTERNATIONAL
# in res.partner.phone : E.164
# It is not good, but it is not a big bug and it's complex to fix
# so let's let it like that. In v12, we store in
# PhoneNumberFormat.INTERNATIONAL, so this bug is kind of an anticipation
# for the future :)
phone_ids = fields.One2many(
'res.partner.phone', 'partner_id', string='Phones')
@@ -89,3 +118,74 @@ class ResPartner(models.Model):
compute='_compute_partner_phone', store=True, readonly=True)
fax = Fax(
compute='_compute_partner_phone', store=True, readonly=True)
email = fields.Char(
compute='_compute_partner_phone', store=True, readonly=True)
@api.depends('phone_ids.phone', 'phone_ids.type', 'phone_ids.email')
def _compute_partner_phone(self):
for partner in self:
phone = mobile = fax = email = False
for pphone in partner.phone_ids:
if pphone.type == '1_email_primary' and pphone.email:
email = pphone.email
elif pphone.phone:
if pphone.type == '5_mobile_primary':
mobile = pphone.phone
elif pphone.type == '7_fax_primary':
fax = pphone.phone
elif pphone.type == '3_phone_primary':
phone = pphone.phone
partner.phone = phone
partner.mobile = mobile
partner.fax = fax
partner.email = email
def _update_create_vals(
self, vals, type, partner_field, partner_phone_field):
if vals.get(partner_field):
vals['phone_ids'].append(
(0, 0, {'type': type, partner_phone_field: vals[partner_field]}))
@api.model
def create(self, vals):
if 'phone_ids' not in vals:
vals['phone_ids'] = []
self._update_create_vals(vals, '1_email_primary', 'email', 'email')
self._update_create_vals(vals, '3_phone_primary', 'phone', 'phone')
self._update_create_vals(vals, '5_mobile_primary', 'mobile', 'phone')
self._update_create_vals(vals, '7_fax_primary', 'fax', 'phone')
return super(ResPartner, self).create(vals)
def _update_write_vals(
self, vals, type, partner_field, partner_phone_field):
self.ensure_one()
rppo = self.env['res.partner.phone']
if partner_field in vals:
pphone = rppo.search([
('partner_id', '=', self.id),
('type', '=', type)], limit=1)
if vals[partner_field]:
if pphone:
vals['phone_ids'].append((1, pphone.id, {
partner_phone_field: vals[partner_field]}))
else:
vals['phone_ids'].append((0, 0, {
'type': type,
partner_phone_field: vals[partner_field],
}))
else:
if pphone:
vals['phone_ids'].append((2, pphone.id))
def write(self, vals):
if 'phone_ids' not in vals:
for rec in self:
vals['phone_ids'] = []
rec._update_write_vals(vals, '1_email_primary', 'email', 'email')
rec._update_write_vals(vals, '3_phone_primary', 'phone', 'phone')
rec._update_write_vals(vals, '5_mobile_primary', 'mobile', 'phone')
rec._update_write_vals(vals, '7_fax_primary', 'fax', 'phone')
super(ResPartner, rec).write(vals)
return True
else:
return super(ResPartner, self).write(vals)

View File

@@ -14,10 +14,11 @@
<field name="name">res.partner.phone.tree</field>
<field name="model">res.partner.phone</field>
<field name="arch" type="xml">
<tree string="Phones" editable="bottom">
<tree string="Phones and E-mail" editable="bottom">
<field name="partner_id" invisible="not context.get('partner_phone_main_view')"/>
<field name="type"/>
<field name="phone" widget="phone"/>
<field name="phone" widget="phone" attrs="{'required': [('type', 'not in', ('1_email_primary', '2_email_secondary'))], 'readonly': [('type', 'in', ('1_email_primary', '2_email_secondary'))]}"/>
<field name="email" widget="email" attrs="{'readonly': [('type', 'not in', ('1_email_primary', '2_email_secondary'))], 'required': [('type', 'in', ('1_email_primary', '2_email_secondary'))]}"/>
<field name="note"/>
</tree>
</field>
@@ -27,14 +28,18 @@
<field name="name">res.partner.phone.search</field>
<field name="model">res.partner.phone</field>
<field name="arch" type="xml">
<search string="Search Phones">
<search string="Search Phones/E-mail">
<field name="phone" />
<field name="email" />
<group name="groupby">
<filter name="type_groupby" string="Type" context="{'group_by': 'type'}"/>
</group>
</search>
</field>
</record>
<record id="res_partner_phone_action" model="ir.actions.act_window">
<field name="name">Phones</field>
<field name="name">Phones/E-mails</field>
<field name="res_model">res.partner.phone</field>
<field name="view_mode">tree</field>
<field name="context">{'partner_phone_main_view': True}</field>
@@ -43,6 +48,7 @@
<menuitem id="res_partner_phone_menu" action="res_partner_phone_action"
parent="sales_team.menu_sales" sequence="10"/>
<!-- PARTNER views -->
<record id="view_partner_form" model="ir.ui.view">
<field name="name">add.phone_ids.on.partner.form</field>
<field name="model">res.partner</field>
@@ -60,6 +66,9 @@
<field name="fax" position="attributes">
<attribute name="invisible">1</attribute>
</field>
<field name="email" position="attributes">
<attribute name="invisible">1</attribute>
</field>
<xpath expr="//field[@name='child_ids']/form//field[@name='phone']" position="after">
<field name="phone_ids" nolabel="1" colspan="2"/>
</xpath>
@@ -86,8 +95,21 @@
<field name="mobile" position="attributes">
<attribute name="invisible">1</attribute>
</field>
<field name="email" position="attributes">
<attribute name="invisible">1</attribute>
</field>
</field>
</record>
<record id="view_res_partner_filter" model="ir.ui.view">
<field name="name">phone.one2many.res.partner.search</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="base.view_res_partner_filter"/>
<field name="arch" type="xml">
<field name="name" position="attributes">
<attribute name="filter_domain">['|', '|', ('display_name', 'ilike', self), ('ref', '=', self), ('phone_ids.email', 'ilike', self)]</attribute>
</field>
</field>
</record>
</odoo>

View File

@@ -21,26 +21,33 @@ def create_partner_phone(cr, phone_field, phone_type):
return to_create
def create_partner_email(cr):
cr.execute('SELECT id, email FROM res_partner WHERE email IS NOT null')
to_create = []
for partner in cr.fetchall():
to_create.append({
'partner_id': partner[0],
'type': '1_email_primary',
'email': partner[1],
})
return to_create
def migrate_to_partner_phone(cr, registry):
"""This post_install script is required because, when the module
is installed, Odoo creates the column in the DB and compute the field
and THEN it loads the file data/res_country_department_data.yml...
So, when it computes the field on module installation, the
departments are not available in the DB, so the department_id field
on res.partner stays null. This post_install script fixes this."""
logger.info('start data migration for one2many_phone')
with api.Environment.manage():
env = api.Environment(cr, SUPERUSER_ID, {})
rppo = env['res.partner.phone']
to_create = []
to_create += create_partner_phone(cr, 'phone', '1_home')
to_create += create_partner_phone(cr, 'mobile', '2_mobile')
to_create += create_partner_phone(cr, 'fax', '5_office_fax')
to_create += create_partner_phone(cr, 'phone', '3_phone_primary')
to_create += create_partner_phone(cr, 'mobile', '5_mobile_primary')
to_create += create_partner_phone(cr, 'fax', '7_fax_primary')
to_create += create_partner_email(cr)
# I need to create all at the end for invalidation purposes
for vals in to_create:
rppo.create(vals)
logger.info(
'partner_phone type %s phone %s created for partner ID %d',
vals['type'], vals['phone'], vals['partner_id'])
'partner_phone type %s phone %s email %s created for partner ID %d',
vals['type'], vals.get('phone'), vals.get('mail'), vals['partner_id'])
logger.info('end data migration for one2many_phone')
return

View File

@@ -0,0 +1 @@
from . import test_partner_phone

View File

@@ -0,0 +1,146 @@
# -*- coding: utf-8 -*-
# Copyright 2019 Barroux Abbey
# @author: Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo.tests.common import TransactionCase
class TestPartnerPhone(TransactionCase):
def setUp(self):
super(TestPartnerPhone, self).setUp()
def _check_result(self, partner, result):
rppo = self.env['res.partner.phone']
pphone_email = rppo.search(
[('type', '=', '1_email_primary'), ('partner_id', '=', partner.id)])
if result['email']:
self.assertEquals(partner.email, result['email'])
self.assertEquals(len(pphone_email), 1)
self.assertEquals(pphone_email.email, result['email'])
else:
self.assertFalse(partner.email)
self.assertFalse(pphone_email)
if result['phone']:
self.assertEquals(partner.phone.replace(u'\xa0', ''), result['phone'])
else:
self.assertFalse(partner.phone)
if result['mobile']:
self.assertEquals(partner.mobile.replace(u'\xa0', ''), result['mobile'])
else:
self.assertFalse(partner.mobile)
if result['fax']:
self.assertEquals(partner.fax.replace(u'\xa0', ''), result['fax'])
else:
self.assertFalse(partner.fax)
field2type = {
'phone': '3_phone_primary',
'mobile': '5_mobile_primary',
'fax': '7_fax_primary',
}
for field, value in result.items():
if field in field2type:
type = field2type[field]
pphone = rppo.search(
[('type', '=', type), ('partner_id', '=', partner.id)])
if value:
self.assertEquals(len(pphone), 1)
self.assertEquals(pphone.phone.replace(u'\xa0', ''), value)
else:
self.assertFalse(pphone)
def test_create_partner(self):
rpo = self.env['res.partner']
p = rpo.create({
'name': 'Test Me',
'email': 'testme@example.com',
'phone': '0198089246',
'mobile': '0198089247',
'fax': '0198089248',
})
result = {
'email': 'testme@example.com',
'phone': '+33198089246',
'mobile': '+33198089247',
'fax': '+33198089248',
}
self._check_result(p, result)
p2 = rpo.create({
'name': 'Test me now',
'email': 'testmenow@example.com',
'phone': '0972727272',
})
result = {
'email': 'testmenow@example.com',
'phone': '+33972727272',
'mobile': False,
'fax': False,
}
self._check_result(p2, result)
p3 = rpo.create({
'name': 'Test me now',
'phone_ids': [
(0, 0, {'type': '3_phone_primary', 'phone': '0972727272'}),
(0, 0, {'type': '1_email_primary', 'email': 'tutu@example.fr'})],
})
result = {
'email': 'tutu@example.fr',
'phone': '+33972727272',
'mobile': False,
'fax': False,
}
self._check_result(p3, result)
def test_write_partner(self):
p1 = self.env['res.partner'].create({
'name': 'test me now',
'country_id': self.env.ref('base.fr').id,
})
result_none = {
'email': False,
'phone': False,
'mobile': False,
'fax': False,
}
self._check_result(p1, result_none)
p1.write({
'mobile': '0198089247',
'email': 'testmenow@example.com',
})
result = {
'email': 'testmenow@example.com',
'phone': False,
'mobile': '+33198089247',
'fax': False,
}
self._check_result(p1, result)
p1.write({
'email': 'testmenow2@example.com',
'phone': False,
'mobile': '04.72.72.72.72',
})
result = {
'email': 'testmenow2@example.com',
'phone': False,
'mobile': '+33472727272',
'fax': False,
}
self._check_result(p1, result)
p1.write({
'phone': False,
'mobile': False,
'email': False,
})
self._check_result(p1, result_none)
p2 = self.env['res.partner'].create({'name': 'Toto', 'email': 'toto@example.com'})
p_multi = p1 + p2
p_multi.write({'email': 'all@example.com', 'phone': '05.60.60.60.70'})
result = {
'email': 'all@example.com',
'phone': '+33560606070',
'mobile': False,
'fax': False,
}
self._check_result(p1, result)
self._check_result(p2, result)