diff --git a/base_partner_one2many_phone/__init__.py b/base_partner_one2many_phone/__init__.py index 361d25d..4e3cd47 100644 --- a/base_partner_one2many_phone/__init__.py +++ b/base_partner_one2many_phone/__init__.py @@ -1,2 +1,2 @@ -from . import partner_phone +from . import models 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 e6cb7f7..65a366c 100644 --- a/base_partner_one2many_phone/__manifest__.py +++ b/base_partner_one2many_phone/__manifest__.py @@ -1,12 +1,12 @@ -# Copyright 2014-2020 Abbaye du Barroux (http://www.barroux.org) -# Copyright 2014-2020 Akretion (http://www.akretion.com>) +# Copyright 2014-2023 Abbaye du Barroux (http://www.barroux.org) +# Copyright 2014-2023 Akretion (http://www.akretion.com>) # @author: Frère Bernard # @author: Alexis de Lattre # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). { 'name': 'Base Partner One2many Phone', - 'version': '14.0.1.0.0', + 'version': '16.0.1.0.0', 'category': 'Phone', 'license': 'AGPL-3', 'summary': 'One2many link between partners and phone numbers/emails', @@ -19,13 +19,14 @@ With this module, one partner can have several phone numbers and several emails. It has been developped by brother Bernard from Barroux Abbey and Alexis de Lattre from Akretion. """, 'author': 'Akretion', - 'website': 'https://akretion.com/', + 'website': 'https://github.com/akretion/odoo-usability', 'depends': ['contacts', 'base_usability', 'phone_validation'], 'excludes': ['sms'], # because sms introduces big changes in partner form view 'data': [ - 'partner_phone_view.xml', + 'views/res_partner_phone.xml', + 'views/res_partner.xml', 'security/ir.model.access.csv', ], - 'installable': False, + 'installable': True, 'post_init_hook': 'migrate_to_partner_phone', } diff --git a/base_partner_one2many_phone/models/__init__.py b/base_partner_one2many_phone/models/__init__.py new file mode 100644 index 0000000..643bea2 --- /dev/null +++ b/base_partner_one2many_phone/models/__init__.py @@ -0,0 +1,2 @@ +from . import res_partner_phone +from . import res_partner diff --git a/base_partner_one2many_phone/models/res_partner.py b/base_partner_one2many_phone/models/res_partner.py new file mode 100644 index 0000000..ab86378 --- /dev/null +++ b/base_partner_one2many_phone/models/res_partner.py @@ -0,0 +1,94 @@ +# Copyright 2014-2023 Abbaye du Barroux (http://www.barroux.org) +# Copyright 2016-2023 Akretion (http://www.akretion.com>) +# @author: Frère Bernard +# @author: Alexis de Lattre +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models, Command + + +class ResPartner(models.Model): + _inherit = 'res.partner' + + # 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/Emails') + phone = fields.Char(compute='_compute_partner_phone', store=True) + mobile = fields.Char(compute='_compute_partner_phone', store=True) + email = fields.Char(compute='_compute_partner_phone', store=True) + + @api.depends('phone_ids.phone', 'phone_ids.type', 'phone_ids.email') + def _compute_partner_phone(self): + for partner in self: + phone = mobile = 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 == '3_phone_primary': + phone = pphone.phone + partner.phone = phone + partner.mobile = mobile + partner.email = email + + def _update_create_vals( + self, vals, type, partner_field, partner_phone_field): + if vals.get(partner_field): + vals['phone_ids'].append( + Command.create({'type': type, partner_phone_field: vals[partner_field]})) + if partner_field in vals: + vals.pop(partner_field) + + @api.model_create_multi + def create(self, vals_list): + for vals in vals_list: + 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') + return super().create(vals_list) + + 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(Command.update(pphone.id, { + partner_phone_field: vals[partner_field]})) + else: + vals['phone_ids'].append(Command.create({ + 'type': type, + partner_phone_field: vals[partner_field], + })) + else: + if pphone: + vals['phone_ids'].append(Command.delete(pphone.id)) + vals.pop(partner_field) + + def write(self, vals): + if 'phone_ids' not in vals: + for rec in self: + cvals = dict(vals, phone_ids=[]) + rec._update_write_vals(cvals, '1_email_primary', 'email', 'email') + rec._update_write_vals(cvals, '3_phone_primary', 'phone', 'phone') + rec._update_write_vals(cvals, '5_mobile_primary', 'mobile', 'phone') + super(ResPartner, rec).write(cvals) + return True + else: + return super().write(vals) diff --git a/base_partner_one2many_phone/models/res_partner_phone.py b/base_partner_one2many_phone/models/res_partner_phone.py new file mode 100644 index 0000000..a79ffc3 --- /dev/null +++ b/base_partner_one2many_phone/models/res_partner_phone.py @@ -0,0 +1,108 @@ +# Copyright 2014-2023 Abbaye du Barroux (http://www.barroux.org) +# Copyright 2016-2023 Akretion (http://www.akretion.com>) +# @author: Frère Bernard +# @author: Alexis de Lattre +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models, _ +from odoo.exceptions import ValidationError +from odoo.addons.phone_validation.tools import phone_validation + +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): + _name = 'res.partner.phone' + _order = 'partner_id, type' + _phone_name_sequence = 8 + _description = 'Multiple emails and phones for partners' + + partner_id = fields.Many2one( + 'res.partner', string='Related Partner', index=True, ondelete='cascade') + type = fields.Selection([ + ('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 = fields.Char(string='Phone') + 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.onchange('phone', 'partner_id') + def _onchange_phone_validation(self): + if self.phone: + country = self.partner_id.country_id + self.phone = phone_validation.phone_format( + self.phone, + country.code or None, + country.phone_code or None, + force_format='INTERNATIONAL', + raise_exception=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.display_name + else: + name = u'%s (%s)' % (pphone.phone, pphone.partner_id.name) + else: + name = pphone.phone + res.append((pphone.id, name)) + return res + + 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') + ''') diff --git a/base_partner_one2many_phone/partner_phone.py b/base_partner_one2many_phone/partner_phone.py deleted file mode 100644 index 3c15728..0000000 --- a/base_partner_one2many_phone/partner_phone.py +++ /dev/null @@ -1,193 +0,0 @@ -# Copyright 2014-2020 Abbaye du Barroux (http://www.barroux.org) -# Copyright 2016-2020 Akretion (http://www.akretion.com>) -# @author: Frère Bernard -# @author: Alexis de Lattre -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). - -from odoo import api, fields, models, _ -from odoo.exceptions import ValidationError - -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): - _name = 'res.partner.phone' - _order = 'partner_id, type' - _phone_name_sequence = 8 - _inherit = ['phone.validation.mixin'] - _description = 'Multiple emails and phones for partners' - - partner_id = fields.Many2one( - 'res.partner', string='Related Partner', index=True, ondelete='cascade') - type = fields.Selection([ - ('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 = fields.Char(string='Phone') - 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.onchange('phone', 'partner_id') - def _onchange_phone_validation(self): - if self.phone: - self.phone = self.phone_format(self.phone, country=self.partner_id.country_id) - - @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.display_name - else: - name = u'%s (%s)' % (pphone.phone, pphone.partner_id.name) - else: - name = pphone.phone - res.append((pphone.id, name)) - return res - - 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' - - # 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/Emails') - phone = fields.Char( - compute='_compute_partner_phone', - store=True, readonly=True, compute_sudo=True) - mobile = fields.Char( - compute='_compute_partner_phone', - store=True, readonly=True, compute_sudo=True) - email = fields.Char( - compute='_compute_partner_phone', - store=True, readonly=True, compute_sudo=True) - - @api.depends('phone_ids.phone', 'phone_ids.type', 'phone_ids.email') - def _compute_partner_phone(self): - for partner in self: - phone = mobile = 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 == '3_phone_primary': - phone = pphone.phone - partner.phone = phone - partner.mobile = mobile - 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().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().write(vals) diff --git a/base_partner_one2many_phone/post_install.py b/base_partner_one2many_phone/post_install.py index 5059913..49cea2f 100644 --- a/base_partner_one2many_phone/post_install.py +++ b/base_partner_one2many_phone/post_install.py @@ -1,4 +1,4 @@ -# Copyright 2017-2020 Akretion France (http://www.akretion.com/) +# Copyright 2017-2023 Akretion France (http://www.akretion.com/) # @author Alexis de Lattre # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). @@ -36,14 +36,12 @@ def create_partner_email(cr): def migrate_to_partner_phone(cr, registry): 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', '3_phone_primary') - to_create += create_partner_phone(cr, 'mobile', '5_mobile_primary') - to_create += create_partner_email(cr) - # I need to create all at the end for invalidation purposes - rppo.create(to_create) + env = api.Environment(cr, SUPERUSER_ID, {}) + rppo = env['res.partner.phone'] + to_create = [] + to_create += create_partner_phone(cr, 'phone', '3_phone_primary') + to_create += create_partner_phone(cr, 'mobile', '5_mobile_primary') + to_create += create_partner_email(cr) + # I need to create all at the end for invalidation purposes + rppo.create(to_create) logger.info('end data migration for one2many_phone') - return diff --git a/base_partner_one2many_phone/tests/test_partner_phone.py b/base_partner_one2many_phone/tests/test_partner_phone.py index 3515d20..cea48b1 100644 --- a/base_partner_one2many_phone/tests/test_partner_phone.py +++ b/base_partner_one2many_phone/tests/test_partner_phone.py @@ -8,8 +8,9 @@ from odoo.tests.common import TransactionCase class TestPartnerPhone(TransactionCase): - def setUp(self): - super(TestPartnerPhone, self).setUp() + @classmethod + def setUpClass(cls): + super().setUpClass() def _check_result(self, partner, result): rppo = self.env['res.partner.phone'] diff --git a/base_partner_one2many_phone/partner_phone_view.xml b/base_partner_one2many_phone/views/res_partner.xml similarity index 58% rename from base_partner_one2many_phone/partner_phone_view.xml rename to base_partner_one2many_phone/views/res_partner.xml index edb4b1e..126c1c9 100644 --- a/base_partner_one2many_phone/partner_phone_view.xml +++ b/base_partner_one2many_phone/views/res_partner.xml @@ -1,7 +1,7 @@ - - res.partner.phone.tree - res.partner.phone - - - - - - - - - - - - - res.partner.phone.form - res.partner.phone - -
- - - - - - - -
-
-
- - - res.partner.phone.search - res.partner.phone - - - - - - - - - - - - - Phones/E-mails - res.partner.phone - tree - {'partner_phone_main_view': True} - - - - - - 20 - - add.phone_ids.on.partner.form @@ -159,7 +100,7 @@ - ['|', '|', ('display_name', 'ilike', self), ('ref', '=ilike', self + '%'), ('phone_ids.email', 'ilike', self)] + ['|', '|', '|', '|', ('display_name', 'ilike', self), ('ref', '=ilike', self + '%'), ('phone_ids.email', 'ilike', self), ('vat', 'ilike', self), ('company_registry', 'ilike', self)] diff --git a/base_partner_one2many_phone/views/res_partner_phone.xml b/base_partner_one2many_phone/views/res_partner_phone.xml new file mode 100644 index 0000000..19e5fc8 --- /dev/null +++ b/base_partner_one2many_phone/views/res_partner_phone.xml @@ -0,0 +1,71 @@ + + + + + + + + res.partner.phone.tree + res.partner.phone + + + + + + + + + + + + + res.partner.phone.form + res.partner.phone + +
+ + + + + + + +
+
+
+ + + res.partner.phone.search + res.partner.phone + + + + + + + + + + + + + Phones/E-mails + res.partner.phone + tree + {'partner_phone_main_view': True} + + + + + + 20 + + +