Format phone numbers

It format for human the value of contact mechanisms and add a compact value
format for search.

issue4329
review26831002
This commit is contained in:
Sebastián Marró committed 2016-11-01 09:27:28 +01:00
1 parent 7d4ef33559
commit 8971db3194
5 files changed
+130 -10

No files matched your search

+1
View File
@@ -1,3 +1,4 @@
* Add format to phone numbers
* Rename VAT Code into Tax Identifier
* Add sequence order on Identifier
* Add Replace Party wizard
+1
View File
@@ -8,6 +8,7 @@ Prerequisites
* trytond (http://www.tryton.org/)
* trytond_country (http://www.tryton.org/)
* python-stdnum (http://arthurdejong.org/python-stdnum/)
* Optional: phonenumbers (http://pypi.python.org/pypi/phonenumbers)
Installation
------------
+89 -9
View File
@@ -1,5 +1,11 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
try:
import phonenumbers
from phonenumbers import PhoneNumberFormat, NumberParseException
except ImportError:
phonenumbers = None
from trytond.model import ModelView, ModelSQL, fields, sequence_ordered
from trytond.pyson import Eval
from trytond import backend
@@ -34,7 +40,8 @@ class ContactMechanism(sequence_ordered(), ModelSQL, ModelView):
sort=False, depends=DEPENDS)
value = fields.Char('Value', select=True, states=STATES, depends=DEPENDS
# Add all function fields to ensure to always fill them via on_change
+ ['email', 'website', 'skype', 'sip', 'other_value'])
+ ['email', 'website', 'skype', 'sip', 'other_value', 'value_compact'])
value_compact = fields.Char('Value Compact', readonly=True)
comment = fields.Text('Comment', states=STATES, depends=DEPENDS)
party = fields.Many2One('party.party', 'Party', required=True,
ondelete='CASCADE', states=STATES, select=True, depends=DEPENDS)
@@ -83,6 +90,8 @@ class ContactMechanism(sequence_ordered(), ModelSQL, ModelView):
cls._error_messages.update({
'write_party': ('You can not modify the party of contact '
'mechanism "%s".'),
'invalid_phonenumber': ('The phone number "%(phone)s" of '
'party "%(party)s" is not valid .'),
})
@classmethod
@@ -125,13 +134,39 @@ class ContactMechanism(sequence_ordered(), ModelSQL, ModelView):
return 'fax:%s' % value
return None
@classmethod
def format_value(cls, value=None, type_=None):
if phonenumbers and type_ == 'phone':
try:
phonenumber = phonenumbers.parse(value)
except NumberParseException:
pass
else:
value = phonenumbers.format_number(
phonenumber, PhoneNumberFormat.INTERNATIONAL)
return value
@classmethod
def format_value_compact(cls, value=None, type_=None):
if phonenumbers and type_ == 'phone':
try:
phonenumber = phonenumbers.parse(value)
except NumberParseException:
pass
else:
value = phonenumbers.format_number(
phonenumber, PhoneNumberFormat.E164)
return value
@classmethod
def set_value(cls, mechanisms, name, value):
# Setting value is done by on_changes
pass
def _change_value(self, value):
self.value = value
def _change_value(self, value, type_):
self.value = self.format_value(value=value, type_=type_)
self.value_compact = self.format_value_compact(
value=value, type_=type_)
self.website = value
self.email = value
self.skype = value
@@ -145,27 +180,53 @@ class ContactMechanism(sequence_ordered(), ModelSQL, ModelView):
@fields.depends('value', 'type')
def on_change_value(self):
return self._change_value(self.value)
return self._change_value(self.value, self.type)
@fields.depends('website', 'type')
def on_change_website(self):
return self._change_value(self.website)
return self._change_value(self.website, self.type)
@fields.depends('email', 'type')
def on_change_email(self):
return self._change_value(self.email)
return self._change_value(self.email, self.type)
@fields.depends('skype', 'type')
def on_change_skype(self):
return self._change_value(self.skype)
return self._change_value(self.skype, self.type)
@fields.depends('sip', 'type')
def on_change_sip(self):
return self._change_value(self.sip)
return self._change_value(self.sip, self.type)
@fields.depends('other_value', 'type')
def on_change_other_value(self):
return self._change_value(self.other_value)
return self._change_value(self.other_value, self.type)
@classmethod
def search_rec_name(cls, name, clause):
return ['OR',
('value',) + tuple(clause[1:]),
('value_compact',) + tuple(clause[1:]),
]
@classmethod
def _format_values(cls, mechanisms):
for mechanism in mechanisms:
value = mechanism.format_value(
value=mechanism.value, type_=mechanism.type)
if value != mechanism.value:
mechanism.value = value
value_compact = mechanism.format_value_compact(
value=mechanism.value, type_=mechanism.type)
if value_compact != mechanism.value_compact:
mechanism.value_compact = value_compact
cls.save(mechanisms)
@classmethod
def create(cls, vlist):
mechanisms = super(ContactMechanism, cls).create(vlist)
cls._format_values(mechanisms)
return mechanisms
@classmethod
def write(cls, *args):
@@ -177,3 +238,22 @@ class ContactMechanism(sequence_ordered(), ModelSQL, ModelView):
cls.raise_user_error(
'write_party', (mechanism.rec_name,))
super(ContactMechanism, cls).write(*args)
cls._format_values(mechanisms)
@classmethod
def validate(cls, mechanisms):
super(ContactMechanism, cls).validate(mechanisms)
for mechanism in mechanisms:
mechanism.check_valid_phonenumber()
def check_valid_phonenumber(self):
if not phonenumbers:
return
try:
phonenumbers.parse(self.value)
except NumberParseException:
self.raise_user_error(
'invalid_phonenumber', {
'phone': self.value,
'party': self.party.rec_name
})
+2 -1
View File
@@ -53,7 +53,7 @@ for dep in info.get('depends', []):
requires.append(get_require_version('trytond_%s' % dep))
requires.append(get_require_version('trytond'))
tests_require = [get_require_version('proteus')]
tests_require = [get_require_version('proteus'), 'phonenumbers']
dependency_links = []
if minor_version % 2:
# Add development index for testing with proteus
@@ -114,6 +114,7 @@ setup(name=name,
install_requires=requires,
extras_require={
'VAT': ['python-stdnum'],
'phonenumbers': ['phonenumbers'],
},
dependency_links=dependency_links,
zip_safe=False,
+37
View File
@@ -2,6 +2,10 @@
# this repository contains the full copyright notices and license terms.
import unittest
import doctest
try:
import phonenumbers
except ImportError:
phonenumbers = None
import trytond.tests.test_tryton
from trytond.tests.test_tryton import ModuleTestCase, with_transaction
@@ -121,6 +125,39 @@ class PartyTestCase(ModuleTestCase):
code = party2.code
self.assertEqual(party2.rec_name, '[' + code + ']')
@unittest.skipIf(phonenumbers is None, 'requires phonenumbers')
@with_transaction()
def test_phone_number_format(self):
'Test phone number format'
pool = Pool()
Party = pool.get('party.party')
ContactMechanism = pool.get('party.contact_mechanism')
party1, = Party.create([{
'name': 'Party 1',
}])
mechanism, = ContactMechanism.create([{
'party': party1.id,
'type': 'phone',
'value': '+442083661177',
}])
# Test format on create
self.assertEqual(mechanism.value, '+44 20 8366 1177')
self.assertEqual(mechanism.value_compact, '+442083661177')
# Test format on write
mechanism.value = '+442083661178'
mechanism.save()
self.assertEqual(mechanism.value, '+44 20 8366 1178')
self.assertEqual(mechanism.value_compact, '+442083661178')
ContactMechanism.write([mechanism], {
'value': '+442083661179',
})
self.assertEqual(mechanism.value, '+44 20 8366 1179')
self.assertEqual(mechanism.value_compact, '+442083661179')
def suite():
suite = trytond.tests.test_tryton.suite()