first commit
This commit is contained in:
372
modules/party/contact_mechanism.py
Normal file
372
modules/party/contact_mechanism.py
Normal file
@@ -0,0 +1,372 @@
|
||||
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
# this repository contains the full copyright notices and license terms.
|
||||
from itertools import chain
|
||||
|
||||
try:
|
||||
import phonenumbers
|
||||
from phonenumbers import NumberParseException, PhoneNumberFormat
|
||||
except ImportError:
|
||||
phonenumbers = None
|
||||
|
||||
from trytond.i18n import gettext
|
||||
from trytond.model import (
|
||||
DeactivableMixin, Index, ModelSQL, ModelView, MultiValueMixin, ValueMixin,
|
||||
fields, sequence_ordered)
|
||||
from trytond.model.exceptions import AccessError
|
||||
from trytond.pyson import Eval
|
||||
from trytond.tools.email_ import (
|
||||
EmailNotValidError, normalize_email, validate_email)
|
||||
from trytond.transaction import Transaction
|
||||
|
||||
from .exceptions import InvalidEMail, InvalidPhoneNumber
|
||||
|
||||
_TYPES = [
|
||||
('phone', 'Phone'),
|
||||
('mobile', 'Mobile'),
|
||||
('fax', 'Fax'),
|
||||
('email', 'Email'),
|
||||
('website', 'Website'),
|
||||
('skype', 'Skype'),
|
||||
('sip', 'SIP'),
|
||||
('irc', 'IRC'),
|
||||
('jabber', 'Jabber'),
|
||||
('other', 'Other'),
|
||||
]
|
||||
|
||||
_PHONE_TYPES = {
|
||||
'phone',
|
||||
'mobile',
|
||||
'fax',
|
||||
}
|
||||
|
||||
|
||||
class _ContactMechanismMixin:
|
||||
__slots__ = ()
|
||||
|
||||
def contact_mechanism_get(self, types=None, usage=None):
|
||||
"""
|
||||
Try to find a contact mechanism for the given types and usage, if no
|
||||
usage matches the first mechanism of the given types is returned.
|
||||
"""
|
||||
default_mechanism = None
|
||||
if types:
|
||||
if isinstance(types, str):
|
||||
types = {types}
|
||||
mechanisms = [m for m in self.contact_mechanisms
|
||||
if m.type in types]
|
||||
else:
|
||||
mechanisms = self.contact_mechanisms
|
||||
if mechanisms:
|
||||
default_mechanism = mechanisms[0]
|
||||
if usage:
|
||||
for mechanism in mechanisms:
|
||||
if getattr(mechanism, usage):
|
||||
return mechanism
|
||||
return default_mechanism
|
||||
|
||||
|
||||
class ContactMechanism(
|
||||
DeactivableMixin, sequence_ordered(), ModelSQL, ModelView,
|
||||
MultiValueMixin):
|
||||
__name__ = 'party.contact_mechanism'
|
||||
_rec_name = 'value'
|
||||
|
||||
type = fields.Selection(_TYPES, "Type", required=True, sort=False)
|
||||
type_string = type.translated('type')
|
||||
value = fields.Char(
|
||||
"Value",
|
||||
# Add all function fields to ensure to always fill them via on_change
|
||||
depends={
|
||||
'email', 'website', 'skype', 'sip', 'other_value',
|
||||
'value_compact'})
|
||||
value_compact = fields.Char('Value Compact', readonly=True)
|
||||
name = fields.Char("Name")
|
||||
comment = fields.Text("Comment")
|
||||
party = fields.Many2One(
|
||||
'party.party', "Party", required=True, ondelete='CASCADE')
|
||||
address = fields.Many2One(
|
||||
'party.address', "Address", ondelete='CASCADE',
|
||||
domain=[
|
||||
('party', '=', Eval('party', -1)),
|
||||
])
|
||||
language = fields.MultiValue(
|
||||
fields.Many2One('ir.lang', "Language",
|
||||
help="Used to translate communication made "
|
||||
"using the contact mechanism.\n"
|
||||
"Leave empty for the party language."))
|
||||
languages = fields.One2Many(
|
||||
'party.contact_mechanism.language', 'contact_mechanism', "Languages")
|
||||
email = fields.Function(fields.Char('Email', states={
|
||||
'invisible': Eval('type') != 'email',
|
||||
'required': Eval('type') == 'email',
|
||||
}, depends={'value'}),
|
||||
'get_value', setter='set_value')
|
||||
website = fields.Function(fields.Char('Website', states={
|
||||
'invisible': Eval('type') != 'website',
|
||||
'required': Eval('type') == 'website',
|
||||
}, depends={'value'}),
|
||||
'get_value', setter='set_value')
|
||||
skype = fields.Function(fields.Char('Skype', states={
|
||||
'invisible': Eval('type') != 'skype',
|
||||
'required': Eval('type') == 'skype',
|
||||
}, depends={'value'}),
|
||||
'get_value', setter='set_value')
|
||||
sip = fields.Function(fields.Char('SIP', states={
|
||||
'invisible': Eval('type') != 'sip',
|
||||
'required': Eval('type') == 'sip',
|
||||
}, depends={'value'}),
|
||||
'get_value', setter='set_value')
|
||||
other_value = fields.Function(fields.Char('Value', states={
|
||||
'invisible': Eval('type').in_(['email', 'website', 'skype', 'sip']),
|
||||
'required': ~Eval('type').in_(['email', 'website']),
|
||||
}, depends={'value'}),
|
||||
'get_value', setter='set_value')
|
||||
url = fields.Function(fields.Char('URL', states={
|
||||
'invisible': ~Eval('url'),
|
||||
}),
|
||||
'on_change_with_url')
|
||||
|
||||
@classmethod
|
||||
def __setup__(cls):
|
||||
cls.value.search_unaccented = False
|
||||
cls.value_compact.search_unaccented = False
|
||||
super().__setup__()
|
||||
cls.__access__.add('party')
|
||||
t = cls.__table__()
|
||||
cls._sql_indexes.add(
|
||||
Index(t, (Index.Unaccent(t.value_compact), Index.Similarity())))
|
||||
cls._order.insert(0, ('party.distance', 'ASC NULLS LAST'))
|
||||
cls._order.insert(1, ('party', 'ASC'))
|
||||
|
||||
@staticmethod
|
||||
def default_type():
|
||||
return 'phone'
|
||||
|
||||
@classmethod
|
||||
def default_party(cls):
|
||||
return Transaction().context.get('related_party')
|
||||
|
||||
@fields.depends('address', '_parent_address.party')
|
||||
def on_change_address(self):
|
||||
if self.address:
|
||||
self.party = self.address.party
|
||||
|
||||
@classmethod
|
||||
def get_value(cls, mechanisms, names):
|
||||
return dict((name, dict((m.id, m.value) for m in mechanisms))
|
||||
for name in names)
|
||||
|
||||
@fields.depends('type', 'value')
|
||||
def on_change_with_url(self, name=None, value=None):
|
||||
if value is None:
|
||||
value = self.value
|
||||
if self.type == 'email':
|
||||
return 'mailto:%s' % value
|
||||
elif self.type == 'website':
|
||||
return value
|
||||
elif self.type == 'skype':
|
||||
return 'callto:%s' % value
|
||||
elif self.type == 'sip':
|
||||
return 'sip:%s' % value
|
||||
elif self.type in {'phone', 'mobile'}:
|
||||
return 'tel:%s' % value
|
||||
elif self.type == 'fax':
|
||||
return 'fax:%s' % value
|
||||
return None
|
||||
|
||||
@fields.depends('party', '_parent_party.addresses')
|
||||
def _phone_country_codes(self):
|
||||
if self.party:
|
||||
for address in self.party.addresses:
|
||||
if address.country:
|
||||
yield address.country.code
|
||||
|
||||
@fields.depends(methods=['_phone_country_codes'])
|
||||
def _parse_phonenumber(self, value):
|
||||
for country_code in chain(self._phone_country_codes(), [None]):
|
||||
try:
|
||||
# Country code is ignored if value has an international prefix
|
||||
phonenumber = phonenumbers.parse(value, country_code)
|
||||
except NumberParseException:
|
||||
pass
|
||||
else:
|
||||
if phonenumbers.is_valid_number(phonenumber):
|
||||
return phonenumber
|
||||
return None
|
||||
|
||||
@fields.depends(methods=['_parse_phonenumber'])
|
||||
def format_value(self, value=None, type_=None):
|
||||
if phonenumbers and type_ in _PHONE_TYPES:
|
||||
phonenumber = self._parse_phonenumber(value)
|
||||
if phonenumber:
|
||||
value = phonenumbers.format_number(
|
||||
phonenumber, PhoneNumberFormat.INTERNATIONAL)
|
||||
if type_ == 'email' and value:
|
||||
value = normalize_email(value)
|
||||
return value
|
||||
|
||||
@fields.depends(methods=['_parse_phonenumber'])
|
||||
def format_value_compact(self, value=None, type_=None):
|
||||
if phonenumbers and type_ in _PHONE_TYPES:
|
||||
phonenumber = self._parse_phonenumber(value)
|
||||
if phonenumber:
|
||||
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
|
||||
|
||||
@fields.depends(
|
||||
methods=['on_change_with_url', 'format_value', 'format_value_compact'])
|
||||
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
|
||||
self.sip = value
|
||||
self.other_value = value
|
||||
self.url = self.on_change_with_url(value=value)
|
||||
|
||||
@fields.depends('value', 'type', methods=['_change_value'])
|
||||
def on_change_value(self):
|
||||
return self._change_value(self.value, self.type)
|
||||
|
||||
@fields.depends('website', 'type', methods=['_change_value'])
|
||||
def on_change_website(self):
|
||||
return self._change_value(self.website, self.type)
|
||||
|
||||
@fields.depends('email', 'type', methods=['_change_value'])
|
||||
def on_change_email(self):
|
||||
return self._change_value(self.email, self.type)
|
||||
|
||||
@fields.depends('skype', 'type', methods=['_change_value'])
|
||||
def on_change_skype(self):
|
||||
return self._change_value(self.skype, self.type)
|
||||
|
||||
@fields.depends('sip', 'type', methods=['_change_value'])
|
||||
def on_change_sip(self):
|
||||
return self._change_value(self.sip, self.type)
|
||||
|
||||
@fields.depends('other_value', 'type', methods=['_change_value'])
|
||||
def on_change_other_value(self):
|
||||
return self._change_value(self.other_value, self.type)
|
||||
|
||||
def get_rec_name(self, name):
|
||||
name = self.name or self.party.rec_name
|
||||
return '%s <%s>' % (name, self.value_compact or self.value)
|
||||
|
||||
@classmethod
|
||||
def search_rec_name(cls, name, clause):
|
||||
if clause[1].startswith('!') or clause[1].startswith('not '):
|
||||
bool_op = 'AND'
|
||||
else:
|
||||
bool_op = 'OR'
|
||||
return [bool_op,
|
||||
('value',) + tuple(clause[1:]),
|
||||
('value_compact',) + tuple(clause[1:]),
|
||||
]
|
||||
|
||||
def compute_fields(self, field_names=None):
|
||||
values = super().compute_fields(field_names=field_names)
|
||||
if field_names is None or {'value', 'type'} & field_names:
|
||||
if getattr(self, 'value', None) and getattr(self, 'type', None):
|
||||
value = self.format_value(value=self.value, type_=self.type)
|
||||
if self.value != value:
|
||||
values['value'] = value
|
||||
value_compact = self.format_value_compact(
|
||||
value=self.value, type_=self.type)
|
||||
if getattr(self, 'value_compact', None) != value_compact:
|
||||
values['value_compact'] = value_compact
|
||||
return values
|
||||
|
||||
@classmethod
|
||||
def check_modification(cls, mode, mechanisms, values=None, external=False):
|
||||
super().check_modification(
|
||||
mode, mechanisms, values=values, external=external)
|
||||
if mode == 'write' and 'party' in values:
|
||||
for mechanism in mechanisms:
|
||||
if mechanism.party.id != values['party']:
|
||||
raise AccessError(gettext(
|
||||
'party.msg_contact_mechanism_change_party',
|
||||
contact=mechanism.rec_name))
|
||||
|
||||
@classmethod
|
||||
def validate_fields(cls, mechanisms, field_names):
|
||||
super().validate_fields(mechanisms, field_names)
|
||||
cls.check_valid_phonenumber(mechanisms, field_names)
|
||||
cls.check_valid_email(mechanisms, field_names)
|
||||
|
||||
@classmethod
|
||||
def check_valid_phonenumber(cls, mechanisms, field_names=None):
|
||||
if field_names and not (field_names & {'type', 'value'}):
|
||||
return
|
||||
if not phonenumbers:
|
||||
return
|
||||
for mechanism in mechanisms:
|
||||
if mechanism.type not in _PHONE_TYPES:
|
||||
continue
|
||||
if not mechanism._parse_phonenumber(mechanism.value):
|
||||
raise InvalidPhoneNumber(
|
||||
gettext('party.msg_invalid_phone_number',
|
||||
phone=mechanism.value, party=mechanism.party.rec_name))
|
||||
|
||||
@classmethod
|
||||
def check_valid_email(cls, mechanisms, field_names=None):
|
||||
if field_names and not (field_names & {'type', 'value'}):
|
||||
return
|
||||
for mechanism in mechanisms:
|
||||
if mechanism.type == 'email' and mechanism.value:
|
||||
try:
|
||||
validate_email(mechanism.value)
|
||||
except EmailNotValidError as e:
|
||||
raise InvalidEMail(gettext(
|
||||
'party.msg_email_invalid',
|
||||
email=mechanism.value,
|
||||
party=mechanism.party.rec_name),
|
||||
str(e)) from e
|
||||
|
||||
@classmethod
|
||||
def usages(cls, _fields=None):
|
||||
"Returns the selection list of usage"
|
||||
usages = [(None, "")]
|
||||
if _fields:
|
||||
for name, desc in cls.fields_get(_fields).items():
|
||||
usages.append((name, desc['string']))
|
||||
return usages
|
||||
|
||||
@fields.depends(methods=['_notify_duplicate'])
|
||||
def on_change_notify(self):
|
||||
notifications = super().on_change_notify()
|
||||
notifications.extend(self._notify_duplicate())
|
||||
return notifications
|
||||
|
||||
@fields.depends('value', 'type', methods=['format_value_compact'])
|
||||
def _notify_duplicate(self):
|
||||
cls = self.__class__
|
||||
if self.value and self.type:
|
||||
value_compact = self.format_value_compact(self.value, self.type)
|
||||
others = cls.search([
|
||||
('id', '!=', self.id),
|
||||
('type', '=', self.type),
|
||||
('value_compact', '=', value_compact),
|
||||
], limit=1)
|
||||
if others:
|
||||
other, = others
|
||||
yield ('warning', gettext(
|
||||
'party.msg_party_contact_mechanism_duplicate',
|
||||
party=other.party.rec_name,
|
||||
type=other.type_string,
|
||||
value=other.value))
|
||||
|
||||
|
||||
class ContactMechanismLanguage(ModelSQL, ValueMixin):
|
||||
__name__ = 'party.contact_mechanism.language'
|
||||
contact_mechanism = fields.Many2One(
|
||||
'party.contact_mechanism', "Contact Mechanism",
|
||||
ondelete='CASCADE')
|
||||
language = fields.Many2One('ir.lang', "Language")
|
||||
Reference in New Issue
Block a user