Files
tryton/modules/party/address.py
T

462 lines
16 KiB
Python
Raw Normal View History

2014-12-02 12:55:24 +01:00
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
2008-04-08 11:26:36 +02:00
'Address'
2016-09-06 09:18:55 +02:00
from string import Template
2020-03-01 12:33:52 +01:00
from sql import Literal
2019-07-30 23:02:46 +02:00
from sql.functions import Substring
2023-01-19 11:25:25 +01:00
from sql.operators import Equal
2021-12-11 17:58:42 +01:00
from trytond.cache import Cache
2018-12-29 14:20:29 +01:00
from trytond.i18n import gettext
2018-03-26 14:10:40 +02:00
from trytond.model import (
2021-12-11 17:58:42 +01:00
DeactivableMixin, Exclude, MatchMixin, ModelSQL, ModelView, fields,
sequence_ordered)
2018-12-29 14:20:29 +01:00
from trytond.model.exceptions import AccessError
2014-08-04 16:42:22 +02:00
from trytond.pool import Pool
2021-12-11 17:58:42 +01:00
from trytond.pyson import Eval, If
2019-04-08 10:10:40 +02:00
from trytond.rpc import RPC
2016-09-06 09:18:55 +02:00
from trytond.transaction import Transaction
2021-12-11 17:58:42 +01:00
2022-10-01 14:52:06 +02:00
from .contact_mechanism import _ContactMechanismMixin
2018-12-29 14:20:29 +01:00
from .exceptions import InvalidFormat
2008-04-08 11:26:36 +02:00
2022-10-01 14:52:06 +02:00
class Address(
DeactivableMixin, sequence_ordered(), _ContactMechanismMixin,
ModelSQL, ModelView):
2008-05-21 18:12:20 +02:00
"Address"
2012-09-24 22:10:20 +02:00
__name__ = 'party.address'
party = fields.Many2One(
'party.party', "Party", required=True, ondelete='CASCADE',
states={
'readonly': Eval('id', 0) > 0,
})
party_name = fields.Char(
"Party Name",
help="If filled, replace the name of the party for address formatting")
name = fields.Char("Building Name")
street = fields.Text("Street")
street_single_line = fields.Function(
fields.Char("Street"),
'on_change_with_street_single_line',
searcher='search_street_single_line')
2021-03-31 23:59:33 +02:00
postal_code = fields.Char("Postal Code")
city = fields.Char("City")
country = fields.Many2One('country.country', "Country")
2019-10-08 18:57:22 +02:00
subdivision_types = fields.Function(
fields.MultiSelection(
'get_subdivision_types', "Subdivision Types"),
'on_change_with_subdivision_types')
2008-10-22 20:07:32 +02:00
subdivision = fields.Many2One("country.subdivision",
2019-10-08 18:57:22 +02:00
'Subdivision',
domain=[
2016-06-14 16:22:35 +02:00
('country', '=', Eval('country', -1)),
2019-10-08 18:57:22 +02:00
If(Eval('subdivision_types', []),
('type', 'in', Eval('subdivision_types', [])),
()
),
])
2010-03-11 16:48:37 +01:00
full_address = fields.Function(fields.Text('Full Address'),
'get_full_address')
identifiers = fields.One2Many(
'party.identifier', 'address', "Identifiers",
domain=[
('party', '=', Eval('party')),
('type', 'in', ['fr_siret']),
])
2022-10-01 14:52:06 +02:00
contact_mechanisms = fields.One2Many(
'party.contact_mechanism', 'address', "Contact Mechanisms",
domain=[
('party', '=', Eval('party', -1)),
])
2012-09-24 22:10:20 +02:00
@classmethod
def __setup__(cls):
super(Address, cls).__setup__()
cls._order.insert(0, ('party', 'ASC'))
2019-04-08 10:10:40 +02:00
cls.__rpc__.update(
2021-03-31 23:59:33 +02:00
autocomplete_postal_code=RPC(instantiate=0, cache=dict(days=1)),
2019-04-08 10:10:40 +02:00
autocomplete_city=RPC(instantiate=0, cache=dict(days=1)),
)
2008-04-22 17:48:57 +02:00
2012-09-24 22:10:20 +02:00
@classmethod
def __register__(cls, module_name):
2021-03-31 23:59:33 +02:00
table = cls.__table_handler__(module_name)
# Migration from 5.8: rename zip to postal code
table.column_rename('zip', 'postal_code')
2012-05-14 15:36:38 +02:00
2012-09-24 22:10:20 +02:00
super(Address, cls).__register__(module_name)
2012-05-14 15:36:38 +02:00
@fields.depends('street')
def on_change_with_street_single_line(self, name=None):
if self.street:
return " ".join(self.street.splitlines())
@classmethod
def search_street_single_line(cls, name, domain):
return [('street',) + tuple(domain[1:])]
2014-08-04 16:42:22 +02:00
_autocomplete_limit = 100
2019-05-20 15:45:29 +02:00
@fields.depends('country', 'subdivision')
2014-08-04 16:42:22 +02:00
def _autocomplete_domain(self):
domain = []
if self.country:
domain.append(('country', '=', self.country.id))
if self.subdivision:
domain.append(['OR',
2016-06-14 16:22:35 +02:00
('subdivision', 'child_of',
[self.subdivision.id], 'parent'),
2014-08-04 16:42:22 +02:00
('subdivision', '=', None),
])
return domain
def _autocomplete_search(self, domain, name):
pool = Pool()
2021-03-31 23:59:33 +02:00
PostalCode = pool.get('country.postal_code')
2014-08-04 16:42:22 +02:00
if domain:
2021-03-31 23:59:33 +02:00
records = PostalCode.search(domain, limit=self._autocomplete_limit)
2014-08-04 16:42:22 +02:00
if len(records) < self._autocomplete_limit:
2014-09-20 10:44:39 +02:00
return sorted({getattr(z, name) for z in records})
2014-08-04 16:42:22 +02:00
return []
2019-05-20 15:45:29 +02:00
@fields.depends('city', methods=['_autocomplete_domain'])
2021-03-31 23:59:33 +02:00
def autocomplete_postal_code(self):
2014-08-04 16:42:22 +02:00
domain = self._autocomplete_domain()
if self.city:
domain.append(('city', 'ilike', '%%%s%%' % self.city))
2021-03-31 23:59:33 +02:00
return self._autocomplete_search(domain, 'postal_code')
2014-08-04 16:42:22 +02:00
2021-03-31 23:59:33 +02:00
@fields.depends('postal_code', methods=['_autocomplete_domain'])
2014-08-04 16:42:22 +02:00
def autocomplete_city(self):
domain = self._autocomplete_domain()
2021-03-31 23:59:33 +02:00
if self.postal_code:
domain.append(('postal_code', 'ilike', '%s%%' % self.postal_code))
2014-08-04 16:42:22 +02:00
return self._autocomplete_search(domain, 'city')
2012-09-24 22:10:20 +02:00
def get_full_address(self, name):
2016-09-06 09:18:55 +02:00
pool = Pool()
AddressFormat = pool.get('party.address.format')
full_address = Template(AddressFormat.get_format(self)).substitute(
**self._get_address_substitutions())
return '\n'.join(
filter(None, (x.strip() for x in full_address.splitlines())))
def _get_address_substitutions(self):
pool = Pool()
Country = pool.get('country.country')
2016-09-06 09:18:55 +02:00
context = Transaction().context
subdivision_code = ''
if getattr(self, 'subdivision', None):
subdivision_code = self.subdivision.code or ''
if '-' in subdivision_code:
subdivision_code = subdivision_code.split('-', 1)[1]
country_name = ''
if getattr(self, 'country', None):
with Transaction().set_context(language='en'):
country_name = Country(self.country.id).name
2016-09-06 09:18:55 +02:00
substitutions = {
'party_name': '',
'attn': '',
2016-12-29 13:42:24 +01:00
'name': getattr(self, 'name', None) or '',
'street': getattr(self, 'street', None) or '',
2021-03-31 23:59:33 +02:00
'postal_code': getattr(self, 'postal_code', None) or '',
2016-12-29 13:42:24 +01:00
'city': getattr(self, 'city', None) or '',
'subdivision': (self.subdivision.name
if getattr(self, 'subdivision', None) else ''),
'subdivision_code': subdivision_code,
'country': country_name,
'country_code': (self.country.code or ''
2016-12-29 13:42:24 +01:00
if getattr(self, 'country', None) else ''),
2016-09-06 09:18:55 +02:00
}
2021-03-31 23:59:33 +02:00
# Keep zip for backward compatibility
substitutions['zip'] = substitutions['postal_code']
2016-12-29 13:42:24 +01:00
if context.get('address_from_country') == getattr(self, 'country', ''):
2016-09-06 09:18:55 +02:00
substitutions['country'] = ''
if context.get('address_with_party', False):
substitutions['party_name'] = self.party_full_name
if context.get('address_attention_party', False):
substitutions['attn'] = (
context['address_attention_party'].full_name)
2018-06-26 19:11:48 +02:00
for key, value in list(substitutions.items()):
2016-09-06 09:18:55 +02:00
substitutions[key.upper()] = value.upper()
return substitutions
2008-10-22 20:07:32 +02:00
@property
def party_full_name(self):
name = ''
if self.party_name:
name = self.party_name
elif self.party:
name = self.party.full_name
return name
2012-09-24 22:10:20 +02:00
def get_rec_name(self, name):
party = self.party_full_name
if self.street_single_line:
street = self.street_single_line
2017-11-15 00:31:05 +01:00
else:
street = None
if self.country:
country = self.country.code
else:
country = None
2016-09-06 09:18:55 +02:00
return ', '.join(
2017-11-15 00:31:05 +01:00
filter(None, [
party,
self.name,
street,
2021-03-31 23:59:33 +02:00
self.postal_code,
2017-11-15 00:31:05 +01:00
self.city,
country]))
2008-03-05 03:12:45 +01:00
2012-09-24 22:10:20 +02:00
@classmethod
def search_rec_name(cls, name, clause):
2015-04-05 23:32:49 +02:00
if clause[1].startswith('!') or clause[1].startswith('not '):
bool_op = 'AND'
else:
bool_op = 'OR'
return [bool_op,
2017-11-15 00:31:05 +01:00
('party',) + tuple(clause[1:]),
('name',) + tuple(clause[1:]),
('street',) + tuple(clause[1:]),
2021-03-31 23:59:33 +02:00
('postal_code',) + tuple(clause[1:]),
('city',) + tuple(clause[1:]),
2017-11-15 00:31:05 +01:00
('country',) + tuple(clause[1:]),
]
2012-09-24 22:10:20 +02:00
@classmethod
2013-12-19 14:45:04 +01:00
def write(cls, *args):
actions = iter(args)
for addresses, values in zip(actions, actions):
if 'party' in values:
for address in addresses:
if address.party.id != values['party']:
2018-12-29 14:20:29 +01:00
raise AccessError(
gettext('party.msg_address_change_party',
address=address.rec_name))
2013-12-19 14:45:04 +01:00
super(Address, cls).write(*args)
2008-06-28 20:46:14 +02:00
2014-01-26 14:29:16 +01:00
@fields.depends('subdivision', 'country')
2012-09-24 22:10:20 +02:00
def on_change_country(self):
if (self.subdivision
and self.subdivision.country != self.country):
2014-10-29 20:39:49 +01:00
self.subdivision = None
2016-09-06 09:18:55 +02:00
2019-10-08 18:57:22 +02:00
@classmethod
def get_subdivision_types(cls):
pool = Pool()
Subdivision = pool.get('country.subdivision')
selection = Subdivision.fields_get(['type'])['type']['selection']
return [(k, v) for k, v in selection if k is not None]
2019-10-08 18:57:22 +02:00
@fields.depends('country')
def on_change_with_subdivision_types(self, name=None):
pool = Pool()
Types = pool.get('party.address.subdivision_type')
return Types.get_types(self.country)
2022-10-01 14:52:06 +02:00
def contact_mechanism_get(self, types=None, usage=None):
mechanism = super().contact_mechanism_get(types=types, usage=usage)
if mechanism is None:
mechanism = self.party.contact_mechanism_get(
types=types, usage=usage)
return mechanism
2016-09-06 09:18:55 +02:00
2018-03-26 14:10:40 +02:00
class AddressFormat(DeactivableMixin, MatchMixin, ModelSQL, ModelView):
2016-09-06 09:18:55 +02:00
"Address Format"
__name__ = 'party.address.format'
2019-07-30 23:02:46 +02:00
country_code = fields.Char("Country Code", size=2)
language_code = fields.Char("Language Code", size=2)
2016-09-06 09:18:55 +02:00
format_ = fields.Text("Format", required=True,
help="Available variables (also in upper case):\n"
"- ${party_name}\n"
"- ${name}\n"
"- ${attn}\n"
2016-09-06 09:18:55 +02:00
"- ${street}\n"
2021-03-31 23:59:33 +02:00
"- ${postal_code}\n"
2016-09-06 09:18:55 +02:00
"- ${city}\n"
"- ${subdivision}\n"
"- ${subdivision_code}\n"
"- ${country}\n"
"- ${country_code}")
_get_format_cache = Cache('party.address.format.get_format')
@classmethod
def __setup__(cls):
super(AddressFormat, cls).__setup__()
2019-07-30 23:02:46 +02:00
cls._order.insert(0, ('country_code', 'ASC NULLS LAST'))
cls._order.insert(1, ('language_code', 'ASC NULLS LAST'))
@classmethod
def __register__(cls, module_name):
pool = Pool()
Country = pool.get('country.country')
Language = pool.get('ir.lang')
country = Country.__table__()
language = Language.__table__()
table = cls.__table__()
cursor = Transaction().connection.cursor()
super().__register__(module_name)
table_h = cls.__table_handler__()
# Migration from 5.2: replace country by country_code
if table_h.column_exist('country'):
query = table.update(
[table.country_code],
country.select(
country.code,
where=country.id == table.country))
cursor.execute(*query)
table_h.drop_column('country')
# Migration from 5.2: replace language by language_code
if table_h.column_exist('language'):
query = table.update(
[table.language_code],
language.select(
Substring(language.code, 0, 2),
where=language.id == table.language))
cursor.execute(*query)
table_h.drop_column('language')
2016-09-06 09:18:55 +02:00
@classmethod
def default_format_(cls):
return """${attn}
${party_name}
2016-09-06 09:18:55 +02:00
${name}
${street}
2021-03-31 23:59:33 +02:00
${postal_code} ${city}
2016-09-06 09:18:55 +02:00
${subdivision}
${COUNTRY}"""
@classmethod
def create(cls, *args, **kwargs):
records = super(AddressFormat, cls).create(*args, **kwargs)
cls._get_format_cache.clear()
return records
@classmethod
def write(cls, *args, **kwargs):
super(AddressFormat, cls).write(*args, **kwargs)
cls._get_format_cache.clear()
@classmethod
def delete(cls, *args, **kwargs):
super(AddressFormat, cls).delete(*args, **kwargs)
cls._get_format_cache.clear()
2016-12-29 13:42:24 +01:00
@classmethod
2022-04-11 23:24:21 +02:00
def validate_fields(cls, formats, field_names):
super().validate_fields(formats, field_names)
cls.check_format(formats, field_names)
2016-12-29 13:42:24 +01:00
2022-04-11 23:24:21 +02:00
@classmethod
def check_format(cls, formats, field_names=None):
2016-12-29 13:42:24 +01:00
pool = Pool()
Address = pool.get('party.address')
2022-04-11 23:24:21 +02:00
if field_names and 'format_' not in field_names:
return
2016-12-29 13:42:24 +01:00
address = Address()
2022-04-11 23:24:21 +02:00
substitutions = address._get_address_substitutions()
for format_ in formats:
try:
Template(format_.format_).substitute(**substitutions)
except Exception as exception:
raise InvalidFormat(gettext('party.invalid_format',
format=format_.format_,
exception=exception)) from exception
2016-12-29 13:42:24 +01:00
2016-09-06 09:18:55 +02:00
@classmethod
def get_format(cls, address, pattern=None):
if pattern is None:
pattern = {}
else:
pattern = pattern.copy()
pattern.setdefault(
2019-07-30 23:02:46 +02:00
'country_code', address.country.code if address.country else None)
2019-07-30 23:30:33 +02:00
pattern.setdefault('language_code', Transaction().language[:2])
2016-09-06 09:18:55 +02:00
2018-06-26 19:11:48 +02:00
key = tuple(sorted(pattern.items()))
2016-09-06 09:18:55 +02:00
format_ = cls._get_format_cache.get(key)
if format_ is not None:
return format_
for record in cls.search([]):
if record.match(pattern):
format_ = record.format_
break
else:
format_ = cls.default_format_()
cls._get_format_cache.set(key, format_)
return format_
2019-10-08 18:57:22 +02:00
class SubdivisionType(DeactivableMixin, ModelSQL, ModelView):
"Address Subdivision Type"
__name__ = 'party.address.subdivision_type'
country_code = fields.Char("Country Code", size=2, required=True)
types = fields.MultiSelection('get_subdivision_types', "Subdivision Types")
_get_types_cache = Cache('party.address.subdivision_type.get_types')
@classmethod
def __setup__(cls):
super().__setup__()
t = cls.__table__()
cls._sql_constraints = [
('country_code_unique',
Exclude(t, (t.country_code, Equal),
2020-03-01 12:33:52 +01:00
where=t.active == Literal(True)),
2019-10-08 18:57:22 +02:00
'party.msg_address_subdivision_country_code_unique')
]
cls._order.insert(0, ('country_code', 'ASC NULLS LAST'))
@classmethod
def get_subdivision_types(cls):
pool = Pool()
Subdivision = pool.get('country.subdivision')
selection = Subdivision.fields_get(['type'])['type']['selection']
return [(k, v) for k, v in selection if k is not None]
2019-10-08 18:57:22 +02:00
@classmethod
def create(cls, *args, **kwargs):
records = super().create(*args, **kwargs)
cls._get_types_cache.clear()
return records
@classmethod
def write(cls, *args, **kwargs):
super().write(*args, **kwargs)
cls._get_types_cache.clear()
@classmethod
def delete(cls, *args, **kwargs):
super().delete(*args, **kwargs)
cls._get_types_cache.clear()
@classmethod
def get_types(cls, country):
key = country.code if country else None
types = cls._get_types_cache.get(key)
if types is not None:
return list(types)
records = cls.search([
('country_code', '=', country.code if country else None),
])
if records:
record, = records
types = record.types
else:
types = []
cls._get_types_cache.set(key, types)
return types