From bb232548302454462aba4aef06be7c7c4a9c35c9 Mon Sep 17 00:00:00 2001 From: Alexis de Lattre Date: Fri, 10 Jan 2020 16:02:03 +0100 Subject: [PATCH] Big update of base_partner_one2many_phone: new types, add email support Migration script provided --- base_partner_one2many_phone/__init__.py | 21 -- base_partner_one2many_phone/__manifest__.py | 10 +- .../migrations/10.0.2.0.0/post-migration.py | 75 +++++++ base_partner_one2many_phone/partner_phone.py | 196 +++++++++++++----- .../partner_phone_view.xml | 30 ++- base_partner_one2many_phone/post_install.py | 29 ++- base_partner_one2many_phone/tests/__init__.py | 1 + .../tests/test_partner_phone.py | 146 +++++++++++++ 8 files changed, 419 insertions(+), 89 deletions(-) create mode 100644 base_partner_one2many_phone/migrations/10.0.2.0.0/post-migration.py create mode 100644 base_partner_one2many_phone/tests/__init__.py create mode 100644 base_partner_one2many_phone/tests/test_partner_phone.py diff --git a/base_partner_one2many_phone/__init__.py b/base_partner_one2many_phone/__init__.py index 7a40fbf..1a8f37a 100644 --- a/base_partner_one2many_phone/__init__.py +++ b/base_partner_one2many_phone/__init__.py @@ -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 . -# -############################################################################## - from . import partner_phone from .post_install import migrate_to_partner_phone diff --git a/base_partner_one2many_phone/__manifest__.py b/base_partner_one2many_phone/__manifest__.py index 7b4df33..ac6ea83 100644 --- a/base_partner_one2many_phone/__manifest__.py +++ b/base_partner_one2many_phone/__manifest__.py @@ -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', diff --git a/base_partner_one2many_phone/migrations/10.0.2.0.0/post-migration.py b/base_partner_one2many_phone/migrations/10.0.2.0.0/post-migration.py new file mode 100644 index 0000000..e110588 --- /dev/null +++ b/base_partner_one2many_phone/migrations/10.0.2.0.0/post-migration.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- +# Copyright 2020 Akretion France (http://www.akretion.com/) +# @author: Alexis de Lattre +# 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'])) diff --git a/base_partner_one2many_phone/partner_phone.py b/base_partner_one2many_phone/partner_phone.py index 6629f6f..ea3244c 100644 --- a/base_partner_one2many_phone/partner_phone.py +++ b/base_partner_one2many_phone/partner_phone.py @@ -5,9 +5,12 @@ # @author: Alexis de Lattre # 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) diff --git a/base_partner_one2many_phone/partner_phone_view.xml b/base_partner_one2many_phone/partner_phone_view.xml index 2dfd8fa..5c31cfa 100644 --- a/base_partner_one2many_phone/partner_phone_view.xml +++ b/base_partner_one2many_phone/partner_phone_view.xml @@ -14,10 +14,11 @@ res.partner.phone.tree res.partner.phone - + - + + @@ -27,14 +28,18 @@ res.partner.phone.search res.partner.phone - + + + + + - Phones + Phones/E-mails res.partner.phone tree {'partner_phone_main_view': True} @@ -43,6 +48,7 @@ + add.phone_ids.on.partner.form res.partner @@ -60,6 +66,9 @@ 1 + + 1 + @@ -86,8 +95,21 @@ 1 + + 1 + + + phone.one2many.res.partner.search + res.partner + + + + ['|', '|', ('display_name', 'ilike', self), ('ref', '=', self), ('phone_ids.email', 'ilike', self)] + + + diff --git a/base_partner_one2many_phone/post_install.py b/base_partner_one2many_phone/post_install.py index a65619d..00d21a7 100644 --- a/base_partner_one2many_phone/post_install.py +++ b/base_partner_one2many_phone/post_install.py @@ -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 diff --git a/base_partner_one2many_phone/tests/__init__.py b/base_partner_one2many_phone/tests/__init__.py new file mode 100644 index 0000000..7a8d4bf --- /dev/null +++ b/base_partner_one2many_phone/tests/__init__.py @@ -0,0 +1 @@ +from . import test_partner_phone diff --git a/base_partner_one2many_phone/tests/test_partner_phone.py b/base_partner_one2many_phone/tests/test_partner_phone.py new file mode 100644 index 0000000..85a2400 --- /dev/null +++ b/base_partner_one2many_phone/tests/test_partner_phone.py @@ -0,0 +1,146 @@ +# -*- coding: utf-8 -*- +# Copyright 2019 Barroux Abbey +# @author: Alexis de Lattre +# 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)