first commit
This commit is contained in:
2
modules/party/__init__.py
Normal file
2
modules/party/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
# this repository contains the full copyright notices and license terms.
|
||||
BIN
modules/party/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
modules/party/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
modules/party/__pycache__/address.cpython-311.pyc
Normal file
BIN
modules/party/__pycache__/address.cpython-311.pyc
Normal file
Binary file not shown.
BIN
modules/party/__pycache__/category.cpython-311.pyc
Normal file
BIN
modules/party/__pycache__/category.cpython-311.pyc
Normal file
Binary file not shown.
BIN
modules/party/__pycache__/configuration.cpython-311.pyc
Normal file
BIN
modules/party/__pycache__/configuration.cpython-311.pyc
Normal file
Binary file not shown.
BIN
modules/party/__pycache__/contact_mechanism.cpython-311.pyc
Normal file
BIN
modules/party/__pycache__/contact_mechanism.cpython-311.pyc
Normal file
Binary file not shown.
BIN
modules/party/__pycache__/country.cpython-311.pyc
Normal file
BIN
modules/party/__pycache__/country.cpython-311.pyc
Normal file
Binary file not shown.
BIN
modules/party/__pycache__/exceptions.cpython-311.pyc
Normal file
BIN
modules/party/__pycache__/exceptions.cpython-311.pyc
Normal file
Binary file not shown.
BIN
modules/party/__pycache__/ir.cpython-311.pyc
Normal file
BIN
modules/party/__pycache__/ir.cpython-311.pyc
Normal file
Binary file not shown.
BIN
modules/party/__pycache__/party.cpython-311.pyc
Normal file
BIN
modules/party/__pycache__/party.cpython-311.pyc
Normal file
Binary file not shown.
679
modules/party/address.py
Normal file
679
modules/party/address.py
Normal file
@@ -0,0 +1,679 @@
|
||||
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
# this repository contains the full copyright notices and license terms.
|
||||
'Address'
|
||||
import re
|
||||
from string import Template
|
||||
|
||||
from sql import Literal
|
||||
from sql.operators import Equal
|
||||
|
||||
from trytond.cache import Cache
|
||||
from trytond.i18n import gettext
|
||||
from trytond.model import (
|
||||
DeactivableMixin, Exclude, MatchMixin, ModelSQL, ModelView, fields,
|
||||
sequence_ordered)
|
||||
from trytond.model.exceptions import AccessError
|
||||
from trytond.pool import Pool
|
||||
from trytond.pyson import Eval, If
|
||||
from trytond.rpc import RPC
|
||||
from trytond.transaction import Transaction
|
||||
|
||||
from .contact_mechanism import _ContactMechanismMixin
|
||||
from .exceptions import InvalidFormat
|
||||
|
||||
|
||||
class Address(
|
||||
DeactivableMixin, sequence_ordered(), _ContactMechanismMixin,
|
||||
ModelSQL, ModelView):
|
||||
__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")
|
||||
|
||||
street = fields.Function(fields.Text(
|
||||
"Street",
|
||||
states={
|
||||
'readonly': (
|
||||
Eval('street_name')
|
||||
| Eval('building_number')
|
||||
| Eval('unit_number')
|
||||
| Eval('floor_number')
|
||||
| Eval('room_number')
|
||||
| Eval('post_box')
|
||||
| Eval('post_office')
|
||||
| Eval('private_bag')),
|
||||
}),
|
||||
'on_change_with_street', setter='set_street', searcher='search_street')
|
||||
street_unstructured = fields.Text(
|
||||
"Street",
|
||||
states={
|
||||
'invisible': (
|
||||
(Eval('street_name')
|
||||
| Eval('building_number')
|
||||
| Eval('unit_number')
|
||||
| Eval('floor_number')
|
||||
| Eval('room_number')
|
||||
| Eval('post_box')
|
||||
| Eval('post_office')
|
||||
| Eval('private_bag'))
|
||||
& ~Eval('street_unstructured')),
|
||||
})
|
||||
|
||||
street_name = fields.Char(
|
||||
"Street Name",
|
||||
states={
|
||||
'invisible': Eval('street_unstructured') & ~Eval('street_name'),
|
||||
})
|
||||
building_name = fields.Char(
|
||||
"Building Name",
|
||||
states={
|
||||
'invisible': Eval('street_unstructured') & ~Eval('building_name'),
|
||||
})
|
||||
building_number = fields.Char(
|
||||
"Building Number",
|
||||
states={
|
||||
'invisible': (
|
||||
Eval('street_unstructured') & ~Eval('building_number')),
|
||||
})
|
||||
unit_number = fields.Char(
|
||||
"Unit Number",
|
||||
states={
|
||||
'invisible': Eval('street_unstructured') & ~Eval('unit_number'),
|
||||
})
|
||||
floor_number = fields.Char(
|
||||
"Floor Number",
|
||||
states={
|
||||
'invisible': Eval('street_unstructured') & ~Eval('floor_number'),
|
||||
})
|
||||
room_number = fields.Char(
|
||||
"Room Number",
|
||||
states={
|
||||
'invisible': Eval('street_unstructured') & ~Eval('room_number'),
|
||||
})
|
||||
|
||||
post_box = fields.Char(
|
||||
"Post Box",
|
||||
states={
|
||||
'invisible': Eval('street_unstructured') & ~Eval('post_box'),
|
||||
})
|
||||
private_bag = fields.Char(
|
||||
"Private Bag",
|
||||
states={
|
||||
'invisible': Eval('street_unstructured') & ~Eval('private_bag'),
|
||||
})
|
||||
post_office = fields.Char(
|
||||
"Post Office",
|
||||
states={
|
||||
'invisible': Eval('street_unstructured') & ~Eval('post_office'),
|
||||
})
|
||||
|
||||
street_single_line = fields.Function(
|
||||
fields.Char("Street"),
|
||||
'on_change_with_street_single_line',
|
||||
searcher='search_street_single_line')
|
||||
postal_code = fields.Char("Postal Code")
|
||||
city = fields.Char("City")
|
||||
country = fields.Many2One('country.country', "Country")
|
||||
subdivision_types = fields.Function(
|
||||
fields.MultiSelection(
|
||||
'get_subdivision_types', "Subdivision Types"),
|
||||
'on_change_with_subdivision_types')
|
||||
subdivision = fields.Many2One("country.subdivision",
|
||||
'Subdivision',
|
||||
domain=[
|
||||
('country', '=', Eval('country', -1)),
|
||||
If(Eval('subdivision_types', []),
|
||||
('type', 'in', Eval('subdivision_types', [])),
|
||||
()
|
||||
),
|
||||
])
|
||||
full_address = fields.Function(fields.Text('Full Address'),
|
||||
'get_full_address')
|
||||
identifiers = fields.One2Many(
|
||||
'party.identifier', 'address', "Identifiers")
|
||||
contact_mechanisms = fields.One2Many(
|
||||
'party.contact_mechanism', 'address', "Contact Mechanisms",
|
||||
domain=[
|
||||
('party', '=', Eval('party', -1)),
|
||||
])
|
||||
|
||||
@classmethod
|
||||
def __setup__(cls):
|
||||
super().__setup__()
|
||||
cls.__access__.add('party')
|
||||
cls._order.insert(0, ('party', 'ASC'))
|
||||
cls.__rpc__.update(
|
||||
autocomplete_postal_code=RPC(instantiate=0, cache=dict(days=1)),
|
||||
autocomplete_city=RPC(instantiate=0, cache=dict(days=1)),
|
||||
)
|
||||
cls.identifiers.domain = [
|
||||
('party', '=', Eval('party', -1)),
|
||||
('type', 'in', list(cls._type_identifiers())),
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def __register__(cls, module_name):
|
||||
table = cls.__table_handler__(module_name)
|
||||
|
||||
# Migration from 7.4: rename street to street_unstructured
|
||||
# and name to building_name
|
||||
table.column_rename('street', 'street_unstructured')
|
||||
table.column_rename('name', 'building_name')
|
||||
|
||||
super().__register__(module_name)
|
||||
|
||||
@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:])]
|
||||
|
||||
_autocomplete_limit = 100
|
||||
|
||||
@fields.depends('country', 'subdivision')
|
||||
def _autocomplete_domain(self):
|
||||
domain = []
|
||||
if self.country:
|
||||
domain.append(('country', '=', self.country.id))
|
||||
if self.subdivision:
|
||||
domain.append(['OR',
|
||||
('subdivision', 'child_of',
|
||||
[self.subdivision.id], 'parent'),
|
||||
('subdivision', '=', None),
|
||||
])
|
||||
return domain
|
||||
|
||||
def _autocomplete_search(self, domain, name):
|
||||
pool = Pool()
|
||||
PostalCode = pool.get('country.postal_code')
|
||||
if domain:
|
||||
records = PostalCode.search(domain, limit=self._autocomplete_limit)
|
||||
if len(records) < self._autocomplete_limit:
|
||||
return sorted({getattr(z, name) for z in records})
|
||||
return []
|
||||
|
||||
@fields.depends('city', methods=['_autocomplete_domain'])
|
||||
def autocomplete_postal_code(self):
|
||||
domain = [
|
||||
self._autocomplete_domain(),
|
||||
('postal_code', 'not in', [None, '']),
|
||||
]
|
||||
if self.city:
|
||||
domain.append(('city', 'ilike', '%%%s%%' % self.city))
|
||||
return self._autocomplete_search(domain, 'postal_code')
|
||||
|
||||
@fields.depends('postal_code', methods=['_autocomplete_domain'])
|
||||
def autocomplete_city(self):
|
||||
domain = [
|
||||
self._autocomplete_domain(),
|
||||
('city', 'not in', [None, '']),
|
||||
]
|
||||
if self.postal_code:
|
||||
domain.append(('postal_code', 'ilike', '%s%%' % self.postal_code))
|
||||
return self._autocomplete_search(domain, 'city')
|
||||
|
||||
def get_full_address(self, name):
|
||||
pool = Pool()
|
||||
AddressFormat = pool.get('party.address.format')
|
||||
full_address = Template(AddressFormat.get_format(self)).substitute(
|
||||
**self._get_address_substitutions())
|
||||
return self._strip(full_address)
|
||||
|
||||
def _get_address_substitutions(self):
|
||||
pool = Pool()
|
||||
Country = pool.get('country.country')
|
||||
|
||||
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
|
||||
substitutions = {
|
||||
'party_name': '',
|
||||
'attn': '',
|
||||
'street': getattr(self, 'street', None) or '',
|
||||
'postal_code': getattr(self, 'postal_code', None) or '',
|
||||
'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 ''
|
||||
if getattr(self, 'country', None) else ''),
|
||||
}
|
||||
# Keep zip for backward compatibility
|
||||
substitutions['zip'] = substitutions['postal_code']
|
||||
if context.get('address_from_country') == getattr(self, 'country', ''):
|
||||
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)
|
||||
for key, value in list(substitutions.items()):
|
||||
substitutions[key.upper()] = value.upper()
|
||||
substitutions.update(self._get_street_substitutions())
|
||||
return substitutions
|
||||
|
||||
@fields.depends('street', 'street_unstructured')
|
||||
def on_change_street(self):
|
||||
self.street_unstructured = self.street
|
||||
|
||||
@fields.depends(
|
||||
'street_unstructured', 'country',
|
||||
methods=['_get_street_substitutions'])
|
||||
def on_change_with_street(self, name=None):
|
||||
pool = Pool()
|
||||
AddressFormat = pool.get('party.address.format')
|
||||
|
||||
format_ = AddressFormat.get_street_format(self)
|
||||
street = Template(format_).substitute(
|
||||
**self._get_street_substitutions())
|
||||
if not (street := self._strip(street, doublespace=True)):
|
||||
street = self.street_unstructured
|
||||
return street
|
||||
|
||||
@classmethod
|
||||
def set_street(cls, addresses, name, value):
|
||||
addresses = [a for a in addresses if a.street != value]
|
||||
cls.write(addresses, {
|
||||
'street_unstructured': value,
|
||||
'street_name': None,
|
||||
'building_name': None,
|
||||
'building_number': None,
|
||||
'unit_number': None,
|
||||
'floor_number': None,
|
||||
'room_number': None,
|
||||
})
|
||||
|
||||
@classmethod
|
||||
def search_street(cls, name, clause):
|
||||
if clause[1].startswith('!') or clause[1].startswith('not '):
|
||||
bool_op = 'AND'
|
||||
else:
|
||||
bool_op = 'OR'
|
||||
return [bool_op,
|
||||
('street_unstructured', *clause[1:]),
|
||||
('street_name', *clause[1:]),
|
||||
('building_name', *clause[1:]),
|
||||
('building_number', *clause[1:]),
|
||||
('unit_number', *clause[1:]),
|
||||
('floor_number', *clause[1:]),
|
||||
('room_number', *clause[1:]),
|
||||
]
|
||||
|
||||
@property
|
||||
def numbers(self):
|
||||
pool = Pool()
|
||||
AddressFormat = pool.get('party.address.format')
|
||||
|
||||
format_ = AddressFormat.get_street_format(self)
|
||||
substitutions = {
|
||||
k: v if k.lower().endswith('_number') else ''
|
||||
for k, v in self._get_street_substitutions().items()}
|
||||
numbers = Template(format_).substitute(**substitutions)
|
||||
return self._strip(numbers, doublespace=True)
|
||||
|
||||
@fields.depends(
|
||||
'country', 'street_name', 'building_number', 'unit_number',
|
||||
'floor_number', 'room_number', 'post_box', 'private_bag',
|
||||
'post_office')
|
||||
def _get_street_substitutions(self):
|
||||
pool = Pool()
|
||||
AddressFormat = pool.get('party.address.format')
|
||||
|
||||
substitutions = {
|
||||
'street_name': getattr(self, 'street_name', None) or '',
|
||||
'building_name': getattr(self, 'building_name', None) or '',
|
||||
'building_number': getattr(self, 'building_number', None) or '',
|
||||
'unit_number': getattr(self, 'unit_number', None) or '',
|
||||
'floor_number': getattr(self, 'floor_number', None) or '',
|
||||
'room_number': getattr(self, 'room_number', None) or '',
|
||||
'post_box': getattr(self, 'post_box', None) or '',
|
||||
'private_bag': getattr(self, 'private_bag', None) or '',
|
||||
'post_office': getattr(self, 'post_office', None) or '',
|
||||
}
|
||||
for number in [
|
||||
'building_number',
|
||||
'unit_number',
|
||||
'floor_number',
|
||||
'room_number',
|
||||
'post_box',
|
||||
'private_bag',
|
||||
'post_office',
|
||||
]:
|
||||
if (substitutions[number]
|
||||
and (format_ := AddressFormat.get_number_format(
|
||||
number, self))):
|
||||
substitutions[number] = format_.format(substitutions[number])
|
||||
for key, value in list(substitutions.items()):
|
||||
substitutions[key.upper()] = value.upper()
|
||||
return substitutions
|
||||
|
||||
@classmethod
|
||||
def _type_identifiers(cls):
|
||||
return {'fr_siret'}
|
||||
|
||||
@classmethod
|
||||
def _strip(cls, value, doublespace=False):
|
||||
value = re.sub(
|
||||
r'[\,\/,–][\s,\,\/]*([\,\/,–])', r'\1', value, flags=re.MULTILINE)
|
||||
if doublespace:
|
||||
value = re.sub(r' {1,}', r' ', value, flags=re.MULTILINE)
|
||||
value = value.splitlines()
|
||||
value = map(lambda x: x.strip(' ,/–'), value)
|
||||
return '\n'.join(filter(None, value))
|
||||
|
||||
@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
|
||||
|
||||
def get_rec_name(self, name):
|
||||
party = self.party_full_name
|
||||
if self.street_single_line:
|
||||
street = self.street_single_line
|
||||
else:
|
||||
street = None
|
||||
if self.country:
|
||||
country = self.country.code
|
||||
else:
|
||||
country = None
|
||||
return ', '.join(
|
||||
filter(None, [
|
||||
party,
|
||||
street,
|
||||
self.postal_code,
|
||||
self.city,
|
||||
country]))
|
||||
|
||||
@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,
|
||||
('party',) + tuple(clause[1:]),
|
||||
('street',) + tuple(clause[1:]),
|
||||
('postal_code',) + tuple(clause[1:]),
|
||||
('city',) + tuple(clause[1:]),
|
||||
('country',) + tuple(clause[1:]),
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def check_modification(cls, mode, addresses, values=None, external=False):
|
||||
super().check_modification(
|
||||
mode, addresses, values=values, external=external)
|
||||
if mode == 'write' and 'party' in values:
|
||||
for address in addresses:
|
||||
if address.party.id != values['party']:
|
||||
raise AccessError(gettext(
|
||||
'party.msg_address_change_party',
|
||||
address=address.rec_name))
|
||||
|
||||
@fields.depends('subdivision', 'country')
|
||||
def on_change_country(self):
|
||||
if (self.subdivision
|
||||
and self.subdivision.country != self.country):
|
||||
self.subdivision = None
|
||||
|
||||
@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]
|
||||
|
||||
@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)
|
||||
|
||||
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
|
||||
|
||||
|
||||
class AddressFormat(DeactivableMixin, MatchMixin, ModelSQL, ModelView):
|
||||
__name__ = 'party.address.format'
|
||||
country_code = fields.Char("Country Code", size=2)
|
||||
language_code = fields.Char("Language Code", size=2)
|
||||
format_ = fields.Text("Format", required=True,
|
||||
help="Available variables (also in upper case and street variables):\n"
|
||||
"- ${party_name}\n"
|
||||
"- ${attn}\n"
|
||||
"- ${street}\n"
|
||||
"- ${postal_code}\n"
|
||||
"- ${city}\n"
|
||||
"- ${subdivision}\n"
|
||||
"- ${subdivision_code}\n"
|
||||
"- ${country}\n"
|
||||
"- ${country_code}")
|
||||
street_format = fields.Text("Street Format", required=True,
|
||||
help="Available variables (also in upper case):\n"
|
||||
"- ${street_name}\n"
|
||||
"- ${building_name}\n"
|
||||
"- ${building_number}\n"
|
||||
"- ${unit_number}\n"
|
||||
"- ${floor_number}\n"
|
||||
"- ${room_number}\n"
|
||||
"- ${post_box}\n"
|
||||
"- ${private_bag}\n"
|
||||
"- ${post_office}\n")
|
||||
building_number_format = fields.Char(
|
||||
"Building Number Format",
|
||||
help="Use {} as placeholder for the building number.")
|
||||
unit_number_format = fields.Char(
|
||||
"Unit Number Format",
|
||||
help="Use {} as placeholder for the unit number.")
|
||||
floor_number_format = fields.Char(
|
||||
"Floor Number Format",
|
||||
help="Use {} as placeholder for the floor number.")
|
||||
room_number_format = fields.Char(
|
||||
"Room Number Format",
|
||||
help="Use {} as placeholder for the room number.")
|
||||
|
||||
post_box_format = fields.Char(
|
||||
"Post Box Format",
|
||||
help="Use {} as placeholder for the post box.")
|
||||
private_bag_format = fields.Char(
|
||||
"Private Bag Format",
|
||||
help="Use {} as placeholder for the private bag.")
|
||||
post_office_format = fields.Char(
|
||||
"Post Office Format",
|
||||
help="Use {} as placeholder for the post office.")
|
||||
|
||||
_get_format_cache = Cache('party.address.format.get_format')
|
||||
|
||||
@classmethod
|
||||
def __setup__(cls):
|
||||
super().__setup__()
|
||||
cls._order.insert(0, ('country_code', 'ASC NULLS LAST'))
|
||||
cls._order.insert(1, ('language_code', 'ASC NULLS LAST'))
|
||||
|
||||
@classmethod
|
||||
def default_format_(cls):
|
||||
return """${attn}
|
||||
${party_name}
|
||||
${street}
|
||||
${postal_code} ${city}
|
||||
${subdivision}
|
||||
${COUNTRY}"""
|
||||
|
||||
@classmethod
|
||||
def on_modification(cls, mode, formats, field_names=None):
|
||||
super().on_modification(mode, formats, field_names=field_names)
|
||||
cls._get_format_cache.clear()
|
||||
|
||||
@classmethod
|
||||
def default_street_format(cls):
|
||||
return (
|
||||
"${street_name} ${building_name} ${building_number}"
|
||||
"/${unit_number}/${floor_number}/${room_number}\n"
|
||||
"${post_box} ${private_bag} ${post_office}")
|
||||
|
||||
@classmethod
|
||||
def validate_fields(cls, formats, field_names):
|
||||
super().validate_fields(formats, field_names)
|
||||
cls.check_format(formats, field_names)
|
||||
cls.check_street_format(formats, field_names)
|
||||
cls.check_number_format(formats, field_names)
|
||||
|
||||
@classmethod
|
||||
def check_format(cls, formats, field_names=None):
|
||||
pool = Pool()
|
||||
Address = pool.get('party.address')
|
||||
if field_names and 'format_' not in field_names:
|
||||
return
|
||||
address = Address()
|
||||
substitutions = address._get_address_substitutions()
|
||||
for format_ in formats:
|
||||
try:
|
||||
Template(format_.format_).substitute(**substitutions)
|
||||
except Exception as exception:
|
||||
raise InvalidFormat(gettext('party.msg_invalid_format',
|
||||
format=format_.format_,
|
||||
exception=exception)) from exception
|
||||
|
||||
@classmethod
|
||||
def check_street_format(cls, formats, field_names=None):
|
||||
pool = Pool()
|
||||
Address = pool.get('party.address')
|
||||
if field_names and 'street_format' not in field_names:
|
||||
return
|
||||
address = Address()
|
||||
substitutions = address._get_street_substitutions()
|
||||
for format_ in formats:
|
||||
try:
|
||||
Template(format_.street_format).substitute(**substitutions)
|
||||
except Exception as exception:
|
||||
raise InvalidFormat(gettext('party.msg_invalid_format',
|
||||
format=format_.street_format,
|
||||
exception=exception)) from exception
|
||||
|
||||
@classmethod
|
||||
def check_number_format(cls, formats, field_names=None):
|
||||
fields = [
|
||||
'building_number_format', 'floor_number_format',
|
||||
'unit_number_format', 'room_number_format',
|
||||
'post_box_format', 'private_bag_format', 'post_office_format']
|
||||
if field_names and not (field_names & set(fields)):
|
||||
return
|
||||
for format_ in formats:
|
||||
for field in fields:
|
||||
if number_format := getattr(format_, field):
|
||||
try:
|
||||
number_format.format('')
|
||||
except Exception as exception:
|
||||
raise InvalidFormat(gettext('party.msg_invalid_format',
|
||||
format=number_format,
|
||||
exception=exception)) from exception
|
||||
|
||||
@classmethod
|
||||
def get_format(cls, address, pattern=None):
|
||||
return cls._get_format('format_', address, pattern=pattern)
|
||||
|
||||
@classmethod
|
||||
def get_street_format(cls, address, pattern=None):
|
||||
return cls._get_format('street_format', address, pattern=pattern)
|
||||
|
||||
@classmethod
|
||||
def get_number_format(cls, number, address, pattern=None):
|
||||
return cls._get_format(f'{number}_format', address, pattern=pattern)
|
||||
|
||||
@classmethod
|
||||
def _get_format(cls, field, address, pattern=None):
|
||||
if pattern is None:
|
||||
pattern = {}
|
||||
else:
|
||||
pattern = pattern.copy()
|
||||
pattern.setdefault(
|
||||
'country_code', address.country.code if address.country else None)
|
||||
pattern.setdefault('language_code', Transaction().language[:2])
|
||||
|
||||
key = (field, *sorted(pattern.items()))
|
||||
format_ = cls._get_format_cache.get(key)
|
||||
if format_ is not None:
|
||||
return format_
|
||||
|
||||
for record in cls.search([]):
|
||||
if record.match(pattern):
|
||||
format_ = getattr(record, field)
|
||||
break
|
||||
else:
|
||||
format_ = getattr(cls, f'default_{field}', lambda: '')()
|
||||
|
||||
cls._get_format_cache.set(key, format_)
|
||||
return format_
|
||||
|
||||
|
||||
class SubdivisionType(DeactivableMixin, ModelSQL, ModelView):
|
||||
__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),
|
||||
where=t.active == Literal(True)),
|
||||
'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]
|
||||
|
||||
@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
|
||||
|
||||
@classmethod
|
||||
def on_modification(cls, mode, types, field_names=None):
|
||||
super().on_modification(mode, types, field_names=field_names)
|
||||
cls._get_types_cache.clear()
|
||||
2833
modules/party/address.xml
Normal file
2833
modules/party/address.xml
Normal file
File diff suppressed because it is too large
Load Diff
31
modules/party/category.py
Normal file
31
modules/party/category.py
Normal file
@@ -0,0 +1,31 @@
|
||||
# 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 sql.conditionals import Coalesce
|
||||
from sql.operators import Equal
|
||||
|
||||
from trytond.model import (
|
||||
DeactivableMixin, Exclude, ModelSQL, ModelView, fields, tree)
|
||||
|
||||
|
||||
class Category(DeactivableMixin, tree(separator=' / '), ModelSQL, ModelView):
|
||||
__name__ = 'party.category'
|
||||
name = fields.Char(
|
||||
"Name", required=True, translate=True,
|
||||
help="The main identifier of the category.")
|
||||
parent = fields.Many2One(
|
||||
'party.category', "Parent",
|
||||
help="Add the category below the parent.")
|
||||
childs = fields.One2Many(
|
||||
'party.category', 'parent', "Children",
|
||||
help="Add children below the category.")
|
||||
|
||||
@classmethod
|
||||
def __setup__(cls):
|
||||
super().__setup__()
|
||||
t = cls.__table__()
|
||||
cls._sql_constraints = [
|
||||
('name_parent_exclude',
|
||||
Exclude(t, (t.name, Equal), (Coalesce(t.parent, -1), Equal)),
|
||||
'party.msg_category_name_unique'),
|
||||
]
|
||||
cls._order.insert(0, ('name', 'ASC'))
|
||||
81
modules/party/category.xml
Normal file
81
modules/party/category.xml
Normal file
@@ -0,0 +1,81 @@
|
||||
<?xml version="1.0"?>
|
||||
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
this repository contains the full copyright notices and license terms. -->
|
||||
<tryton>
|
||||
<data>
|
||||
<record model="ir.ui.view" id="category_view_form">
|
||||
<field name="model">party.category</field>
|
||||
<field name="type">form</field>
|
||||
<field name="name">category_form</field>
|
||||
</record>
|
||||
<record model="ir.ui.view" id="category_view_tree">
|
||||
<field name="model">party.category</field>
|
||||
<field name="type">tree</field>
|
||||
<field name="field_childs">childs</field>
|
||||
<field name="name">category_tree</field>
|
||||
</record>
|
||||
<record model="ir.ui.view" id="category_view_list">
|
||||
<field name="model">party.category</field>
|
||||
<field name="type">tree</field>
|
||||
<field name="priority" eval="10"/>
|
||||
<field name="name">category_list</field>
|
||||
</record>
|
||||
<record model="ir.action.act_window" id="act_category_tree">
|
||||
<field name="name">Categories</field>
|
||||
<field name="res_model">party.category</field>
|
||||
<field name="domain" eval="[('parent', '=', None)]" pyson="1"/>
|
||||
</record>
|
||||
<record model="ir.action.act_window.view" id="act_category_tree_view1">
|
||||
<field name="sequence" eval="10"/>
|
||||
<field name="view" ref="category_view_tree"/>
|
||||
<field name="act_window" ref="act_category_tree"/>
|
||||
</record>
|
||||
<record model="ir.action.act_window.view" id="act_category_tree_view2">
|
||||
<field name="sequence" eval="20"/>
|
||||
<field name="view" ref="category_view_form"/>
|
||||
<field name="act_window" ref="act_category_tree"/>
|
||||
</record>
|
||||
|
||||
<menuitem
|
||||
parent="menu_party"
|
||||
action="act_category_tree"
|
||||
sequence="50"
|
||||
id="menu_category_tree"/>
|
||||
|
||||
<record model="ir.action.act_window" id="act_category_list">
|
||||
<field name="name">Categories</field>
|
||||
<field name="res_model">party.category</field>
|
||||
</record>
|
||||
<record model="ir.action.act_window.view" id="act_category_list_view1">
|
||||
<field name="sequence" eval="10"/>
|
||||
<field name="view" ref="category_view_list"/>
|
||||
<field name="act_window" ref="act_category_list"/>
|
||||
</record>
|
||||
<record model="ir.action.act_window.view" id="act_category_list_view2">
|
||||
<field name="sequence" eval="20"/>
|
||||
<field name="view" ref="category_view_form"/>
|
||||
<field name="act_window" ref="act_category_list"/>
|
||||
</record>
|
||||
<menuitem
|
||||
parent="menu_category_tree"
|
||||
sequence="10"
|
||||
action="act_category_list"
|
||||
id="menu_category_list"/>
|
||||
|
||||
<record model="ir.model.access" id="access_party_category">
|
||||
<field name="model">party.category</field>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="False"/>
|
||||
<field name="perm_create" eval="False"/>
|
||||
<field name="perm_delete" eval="False"/>
|
||||
</record>
|
||||
<record model="ir.model.access" id="access_party_category_admin">
|
||||
<field name="model">party.category</field>
|
||||
<field name="group" ref="group_party_admin"/>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="True"/>
|
||||
<field name="perm_create" eval="True"/>
|
||||
<field name="perm_delete" eval="True"/>
|
||||
</record>
|
||||
</data>
|
||||
</tryton>
|
||||
105
modules/party/configuration.py
Normal file
105
modules/party/configuration.py
Normal file
@@ -0,0 +1,105 @@
|
||||
# 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 trytond.i18n import gettext
|
||||
from trytond.model import (
|
||||
ModelSingleton, ModelSQL, ModelView, MultiValueMixin, ValueMixin, fields)
|
||||
from trytond.model.exceptions import AccessError
|
||||
from trytond.pool import Pool
|
||||
from trytond.pyson import Id
|
||||
from trytond.transaction import Transaction
|
||||
|
||||
from .party import IDENTIFIER_TYPES, replace_vat
|
||||
|
||||
party_sequence = fields.Many2One('ir.sequence', 'Party Sequence',
|
||||
domain=[
|
||||
('sequence_type', '=', Id('party', 'sequence_type_party')),
|
||||
],
|
||||
help="Used to generate the party code.")
|
||||
party_lang = fields.Many2One("ir.lang", 'Party Language',
|
||||
help="The default language for new parties.")
|
||||
|
||||
|
||||
class Configuration(ModelSingleton, ModelSQL, ModelView, MultiValueMixin):
|
||||
__name__ = 'party.configuration'
|
||||
|
||||
party_sequence = fields.MultiValue(party_sequence)
|
||||
party_lang = fields.MultiValue(party_lang)
|
||||
identifier_types = fields.MultiSelection(
|
||||
IDENTIFIER_TYPES, "Identifier Types",
|
||||
help="Defines which identifier types are available.\n"
|
||||
"Leave empty for all of them.")
|
||||
|
||||
@classmethod
|
||||
def __register__(cls, module):
|
||||
cursor = Transaction().connection.cursor()
|
||||
super().__register__(module)
|
||||
|
||||
# Migration from 6.8: Use vat alias
|
||||
configuration = cls(1)
|
||||
if configuration.identifier_types:
|
||||
identifier_types = tuple(map(
|
||||
replace_vat, configuration.identifier_types))
|
||||
if configuration.identifier_types != identifier_types:
|
||||
table = cls.__table__()
|
||||
cursor.execute(*table.update(
|
||||
[table.identifier_types],
|
||||
[cls.identifier_types.sql_format(identifier_types)]
|
||||
))
|
||||
|
||||
@classmethod
|
||||
def default_party_sequence(cls, **pattern):
|
||||
pool = Pool()
|
||||
ModelData = pool.get('ir.model.data')
|
||||
try:
|
||||
return ModelData.get_id('party', 'sequence_party')
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
def get_identifier_types(self):
|
||||
selection = self.fields_get(
|
||||
['identifier_types'])['identifier_types']['selection']
|
||||
if self.identifier_types:
|
||||
selection = [
|
||||
(k, v) for k, v in selection if k in self.identifier_types]
|
||||
return selection
|
||||
|
||||
@classmethod
|
||||
def on_modification(cls, mode, configurations, field_names=None):
|
||||
super().on_modification(mode, configurations, field_names=field_names)
|
||||
ModelView._fields_view_get_cache.clear()
|
||||
|
||||
@classmethod
|
||||
def validate_fields(cls, records, field_names):
|
||||
super().validate_fields(records, field_names)
|
||||
cls(1).check_identifier_types(field_names)
|
||||
|
||||
def check_identifier_types(self, field_names=None):
|
||||
pool = Pool()
|
||||
Identifier = pool.get('party.identifier')
|
||||
if field_names and 'identifier_types' not in field_names:
|
||||
return
|
||||
if self.identifier_types:
|
||||
identifier_types = [None, ''] + list(self.identifier_types)
|
||||
identifiers = Identifier.search([
|
||||
('type', 'not in', identifier_types),
|
||||
], limit=1, order=[])
|
||||
if identifiers:
|
||||
identifier, = identifiers
|
||||
selection = self.fields_get(
|
||||
['identifier_types'])['identifier_types']['selection']
|
||||
selection = dict(selection)
|
||||
raise AccessError(gettext(
|
||||
'party.msg_identifier_type_remove',
|
||||
type=selection.get(identifier.type, identifier.type),
|
||||
identifier=identifier.rec_name,
|
||||
))
|
||||
|
||||
|
||||
class ConfigurationSequence(ModelSQL, ValueMixin):
|
||||
__name__ = 'party.configuration.party_sequence'
|
||||
party_sequence = party_sequence
|
||||
|
||||
|
||||
class ConfigurationLang(ModelSQL, ValueMixin):
|
||||
__name__ = 'party.configuration.party_lang'
|
||||
party_lang = party_lang
|
||||
50
modules/party/configuration.xml
Normal file
50
modules/party/configuration.xml
Normal file
@@ -0,0 +1,50 @@
|
||||
<?xml version="1.0"?>
|
||||
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
this repository contains the full copyright notices and license terms. -->
|
||||
<tryton>
|
||||
<data>
|
||||
<record model="ir.ui.view" id="party_configuration_view_form">
|
||||
<field name="model">party.configuration</field>
|
||||
<field name="type">form</field>
|
||||
<field name="name">configuration_form</field>
|
||||
</record>
|
||||
<record model="ir.action.act_window" id="act_party_configuration_form">
|
||||
<field name="name">Configuration</field>
|
||||
<field name="res_model">party.configuration</field>
|
||||
</record>
|
||||
<record model="ir.action.act_window.view"
|
||||
id="act_party_configuration_view1">
|
||||
<field name="sequence" eval="1"/>
|
||||
<field name="view" ref="party_configuration_view_form"/>
|
||||
<field name="act_window" ref="act_party_configuration_form"/>
|
||||
</record>
|
||||
<menuitem
|
||||
parent="menu_configuration"
|
||||
action="act_party_configuration_form"
|
||||
sequence="10"
|
||||
id="menu_party_configuration"
|
||||
icon="tryton-list"/>
|
||||
|
||||
<record model="ir.model.access" id="access_party_configuration">
|
||||
<field name="model">party.configuration</field>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="False"/>
|
||||
<field name="perm_create" eval="False"/>
|
||||
<field name="perm_delete" eval="False"/>
|
||||
</record>
|
||||
<record model="ir.model.access" id="access_party_configuration_party_admin">
|
||||
<field name="model">party.configuration</field>
|
||||
<field name="group" ref="group_party_admin"/>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="True"/>
|
||||
<field name="perm_create" eval="False"/>
|
||||
<field name="perm_delete" eval="False"/>
|
||||
</record>
|
||||
</data>
|
||||
<data noupdate="1">
|
||||
<record model="party.configuration.party_sequence"
|
||||
id="configuration_party_sequence">
|
||||
<field name="party_sequence" ref="sequence_party"/>
|
||||
</record>
|
||||
</data>
|
||||
</tryton>
|
||||
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")
|
||||
49
modules/party/contact_mechanism.xml
Normal file
49
modules/party/contact_mechanism.xml
Normal file
@@ -0,0 +1,49 @@
|
||||
<?xml version="1.0"?>
|
||||
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
this repository contains the full copyright notices and license terms. -->
|
||||
<tryton>
|
||||
<data>
|
||||
|
||||
<record model="ir.ui.view" id="contact_mechanism_view_tree">
|
||||
<field name="model">party.contact_mechanism</field>
|
||||
<field name="type">tree</field>
|
||||
<field name="priority" eval="10"/>
|
||||
<field name="name">contact_mechanism_tree</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="contact_mechanism_view_tree_sequence">
|
||||
<field name="model">party.contact_mechanism</field>
|
||||
<field name="type">tree</field>
|
||||
<field name="priority" eval="20"/>
|
||||
<field name="name">contact_mechanism_tree_sequence</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="contact_mechanism_view_form">
|
||||
<field name="model">party.contact_mechanism</field>
|
||||
<field name="type">form</field>
|
||||
<field name="name">contact_mechanism_form</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.action.act_window" id="act_contact_mechanism_form">
|
||||
<field name="name">Contact Mechanisms</field>
|
||||
<field name="res_model">party.contact_mechanism</field>
|
||||
</record>
|
||||
<record model="ir.action.act_window.view"
|
||||
id="act_contact_mechanism_form_view1">
|
||||
<field name="sequence" eval="10"/>
|
||||
<field name="view" ref="contact_mechanism_view_tree"/>
|
||||
<field name="act_window" ref="act_contact_mechanism_form"/>
|
||||
</record>
|
||||
<record model="ir.action.act_window.view"
|
||||
id="act_contact_mechanism_form_view2">
|
||||
<field name="sequence" eval="20"/>
|
||||
<field name="view" ref="contact_mechanism_view_form"/>
|
||||
<field name="act_window" ref="act_contact_mechanism_form"/>
|
||||
</record>
|
||||
<menuitem
|
||||
parent="menu_party"
|
||||
sequence="20"
|
||||
action="act_contact_mechanism_form"
|
||||
id="menu_contact_mechanism_form"/>
|
||||
</data>
|
||||
</tryton>
|
||||
21
modules/party/country.py
Normal file
21
modules/party/country.py
Normal file
@@ -0,0 +1,21 @@
|
||||
# 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 trytond.model import Index
|
||||
from trytond.pool import PoolMeta
|
||||
|
||||
|
||||
class PostalCode(metaclass=PoolMeta):
|
||||
__name__ = 'country.postal_code'
|
||||
|
||||
@classmethod
|
||||
def __setup__(cls):
|
||||
super().__setup__()
|
||||
t = cls.__table__()
|
||||
cls._sql_indexes.update({
|
||||
Index(
|
||||
t,
|
||||
(Index.Unaccent(t.city), Index.Similarity()),
|
||||
(t.country, Index.Range()),
|
||||
(t.subdivision, Index.Range())),
|
||||
})
|
||||
32
modules/party/exceptions.py
Normal file
32
modules/party/exceptions.py
Normal file
@@ -0,0 +1,32 @@
|
||||
# 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 trytond.exceptions import UserError, UserWarning
|
||||
from trytond.model.exceptions import ValidationError
|
||||
|
||||
|
||||
class InvalidIdentifierCode(ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidPhoneNumber(ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidEMail(ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class VIESUnavailable(UserError):
|
||||
pass
|
||||
|
||||
|
||||
class SimilarityWarning(UserWarning):
|
||||
pass
|
||||
|
||||
|
||||
class EraseError(ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidFormat(ValidationError):
|
||||
pass
|
||||
4
modules/party/icons/tryton-party.svg
Normal file
4
modules/party/icons/tryton-party.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/>
|
||||
<path d="M0 0h24v24H0z" fill="none"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 259 B |
103
modules/party/ir.py
Normal file
103
modules/party/ir.py
Normal file
@@ -0,0 +1,103 @@
|
||||
# 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 trytond.model import fields
|
||||
from trytond.pool import Pool, PoolMeta
|
||||
from trytond.tools import escape_wildcard
|
||||
from trytond.transaction import Transaction
|
||||
|
||||
|
||||
class Email(metaclass=PoolMeta):
|
||||
__name__ = 'ir.email'
|
||||
|
||||
@classmethod
|
||||
def _match(cls, name, email):
|
||||
pool = Pool()
|
||||
ContactMechanism = pool.get('party.contact_mechanism')
|
||||
yield from super()._match(name, email)
|
||||
domain = ['OR']
|
||||
for field in ['name', 'party.name', 'value']:
|
||||
for value in [name, email]:
|
||||
if value and len(value) >= 3:
|
||||
domain.append(
|
||||
(field, 'ilike', '%' + escape_wildcard(value) + '%'))
|
||||
for contact in ContactMechanism.search([
|
||||
('type', '=', 'email'),
|
||||
('value', '!=', ''),
|
||||
domain,
|
||||
], order=[]):
|
||||
yield contact.name or contact.party.name, contact.value
|
||||
|
||||
|
||||
class EmailTemplate(metaclass=PoolMeta):
|
||||
__name__ = 'ir.email.template'
|
||||
|
||||
contact_mechanism = fields.Selection(
|
||||
'get_contact_mechanisms', "Contact Mechanism",
|
||||
help="Define which email address to use "
|
||||
"from the party's contact mechanisms.")
|
||||
|
||||
@classmethod
|
||||
def get_contact_mechanisms(cls):
|
||||
pool = Pool()
|
||||
ContactMechanism = pool.get('party.contact_mechanism')
|
||||
return ContactMechanism.usages()
|
||||
|
||||
def get(self, record):
|
||||
with Transaction().set_context(usage=self.contact_mechanism):
|
||||
return super().get(record)
|
||||
|
||||
@classmethod
|
||||
def email_models(cls):
|
||||
return super().email_models() + [
|
||||
'party.party', 'party.contact_mechanism']
|
||||
|
||||
@classmethod
|
||||
def _get_default_exclude(cls, record):
|
||||
pool = Pool()
|
||||
Party = pool.get('party.party')
|
||||
ContactMechanism = pool.get('party.contact_mechanism')
|
||||
exclude = super()._get_default_exclude(record)
|
||||
if isinstance(record, Party):
|
||||
exclude.append('contact_mechanisms')
|
||||
if isinstance(record, ContactMechanism):
|
||||
exclude.append('party')
|
||||
return exclude
|
||||
|
||||
@classmethod
|
||||
def _get_address(cls, record):
|
||||
pool = Pool()
|
||||
Party = pool.get('party.party')
|
||||
ContactMechanism = pool.get('party.contact_mechanism')
|
||||
address = super()._get_address(record)
|
||||
usage = Transaction().context.get('usage')
|
||||
if isinstance(record, ContactMechanism):
|
||||
if record.type == 'email':
|
||||
if not usage or getattr(record, usage):
|
||||
address = (record.name or record.party.name, record.email)
|
||||
else:
|
||||
record = record.party
|
||||
if isinstance(record, Party):
|
||||
contact = record.contact_mechanism_get('email', usage=usage)
|
||||
if contact and contact.email:
|
||||
address = (contact.name or record.name, contact.email)
|
||||
return address
|
||||
|
||||
@classmethod
|
||||
def _get_language(cls, record):
|
||||
pool = Pool()
|
||||
Party = pool.get('party.party')
|
||||
ContactMechanism = pool.get('party.contact_mechanism')
|
||||
language = super()._get_language(record)
|
||||
if isinstance(record, Party):
|
||||
usage = Transaction().context.get('usage')
|
||||
contact = record.contact_mechanism_get('email', usage=usage)
|
||||
if contact and contact.language:
|
||||
language = contact.language
|
||||
elif record.lang:
|
||||
language = record.lang
|
||||
if isinstance(record, ContactMechanism):
|
||||
if record.language:
|
||||
language = record.language
|
||||
elif record.party.lang:
|
||||
language = record.party.lang
|
||||
return language
|
||||
12
modules/party/ir.xml
Normal file
12
modules/party/ir.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0"?>
|
||||
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
this repository contains the full copyright notices and license terms. -->
|
||||
<tryton>
|
||||
<data>
|
||||
<record model="ir.ui.view" id="email_template_view_form">
|
||||
<field name="model">ir.email.template</field>
|
||||
<field name="inherit" ref="ir.email_template_view_form"/>
|
||||
<field name="name">email_template_form</field>
|
||||
</record>
|
||||
</data>
|
||||
</tryton>
|
||||
315
modules/party/label.fodt
Normal file
315
modules/party/label.fodt
Normal file
@@ -0,0 +1,315 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<office:document xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0" xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" xmlns:table="urn:oasis:names:tc:opendocument:xmlns:table:1.0" xmlns:draw="urn:oasis:names:tc:opendocument:xmlns:drawing:1.0" xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:meta="urn:oasis:names:tc:opendocument:xmlns:meta:1.0" xmlns:number="urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0" xmlns:svg="urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0" xmlns:chart="urn:oasis:names:tc:opendocument:xmlns:chart:1.0" xmlns:dr3d="urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0" xmlns:math="http://www.w3.org/1998/Math/MathML" xmlns:form="urn:oasis:names:tc:opendocument:xmlns:form:1.0" xmlns:script="urn:oasis:names:tc:opendocument:xmlns:script:1.0" xmlns:config="urn:oasis:names:tc:opendocument:xmlns:config:1.0" xmlns:ooo="http://openoffice.org/2004/office" xmlns:ooow="http://openoffice.org/2004/writer" xmlns:oooc="http://openoffice.org/2004/calc" xmlns:dom="http://www.w3.org/2001/xml-events" xmlns:xforms="http://www.w3.org/2002/xforms" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:rpt="http://openoffice.org/2005/report" xmlns:of="urn:oasis:names:tc:opendocument:xmlns:of:1.2" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:grddl="http://www.w3.org/2003/g/data-view#" xmlns:officeooo="http://openoffice.org/2009/office" xmlns:tableooo="http://openoffice.org/2009/table" xmlns:drawooo="http://openoffice.org/2010/draw" xmlns:calcext="urn:org:documentfoundation:names:experimental:calc:xmlns:calcext:1.0" xmlns:loext="urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.0" xmlns:field="urn:openoffice:names:experimental:ooo-ms-interop:xmlns:field:1.0" xmlns:formx="urn:openoffice:names:experimental:ooxml-odf-interop:xmlns:form:1.0" xmlns:css3t="http://www.w3.org/TR/css3-text/" office:version="1.2" office:mimetype="application/vnd.oasis.opendocument.text">
|
||||
<office:meta><meta:generator>LibreOffice/5.2.7.2$Linux_X86_64 LibreOffice_project/20m0$Build-2</meta:generator><meta:creation-date>2008-06-07T15:28:45</meta:creation-date><dc:date>2008-10-22T19:21:28</dc:date><meta:editing-cycles>1</meta:editing-cycles><meta:editing-duration>PT0S</meta:editing-duration><meta:document-statistic meta:character-count="278" meta:image-count="0" meta:non-whitespace-character-count="265" meta:object-count="0" meta:page-count="1" meta:paragraph-count="12" meta:table-count="0" meta:word-count="25"/><meta:user-defined meta:name="Info 1"/><meta:user-defined meta:name="Info 2"/><meta:user-defined meta:name="Info 3"/><meta:user-defined meta:name="Info 4"/></office:meta>
|
||||
<office:settings>
|
||||
<config:config-item-set config:name="ooo:view-settings">
|
||||
<config:config-item config:name="ViewAreaTop" config:type="long">0</config:config-item>
|
||||
<config:config-item config:name="ViewAreaLeft" config:type="long">0</config:config-item>
|
||||
<config:config-item config:name="ViewAreaWidth" config:type="long">43760</config:config-item>
|
||||
<config:config-item config:name="ViewAreaHeight" config:type="long">21691</config:config-item>
|
||||
<config:config-item config:name="ShowRedlineChanges" config:type="boolean">true</config:config-item>
|
||||
<config:config-item config:name="InBrowseMode" config:type="boolean">false</config:config-item>
|
||||
<config:config-item-map-indexed config:name="Views">
|
||||
<config:config-item-map-entry>
|
||||
<config:config-item config:name="ViewId" config:type="string">view2</config:config-item>
|
||||
<config:config-item config:name="ViewLeft" config:type="long">2630</config:config-item>
|
||||
<config:config-item config:name="ViewTop" config:type="long">2501</config:config-item>
|
||||
<config:config-item config:name="VisibleLeft" config:type="long">0</config:config-item>
|
||||
<config:config-item config:name="VisibleTop" config:type="long">0</config:config-item>
|
||||
<config:config-item config:name="VisibleRight" config:type="long">43759</config:config-item>
|
||||
<config:config-item config:name="VisibleBottom" config:type="long">21689</config:config-item>
|
||||
<config:config-item config:name="ZoomType" config:type="short">0</config:config-item>
|
||||
<config:config-item config:name="ViewLayoutColumns" config:type="short">0</config:config-item>
|
||||
<config:config-item config:name="ViewLayoutBookMode" config:type="boolean">false</config:config-item>
|
||||
<config:config-item config:name="ZoomFactor" config:type="short">100</config:config-item>
|
||||
<config:config-item config:name="IsSelectedFrame" config:type="boolean">false</config:config-item>
|
||||
</config:config-item-map-entry>
|
||||
</config:config-item-map-indexed>
|
||||
</config:config-item-set>
|
||||
<config:config-item-set config:name="ooo:configuration-settings">
|
||||
<config:config-item config:name="PrintProspect" config:type="boolean">false</config:config-item>
|
||||
<config:config-item config:name="PrintLeftPages" config:type="boolean">true</config:config-item>
|
||||
<config:config-item config:name="PrintPageBackground" config:type="boolean">true</config:config-item>
|
||||
<config:config-item config:name="PrintControls" config:type="boolean">true</config:config-item>
|
||||
<config:config-item config:name="PrintAnnotationMode" config:type="short">0</config:config-item>
|
||||
<config:config-item config:name="PrintGraphics" config:type="boolean">true</config:config-item>
|
||||
<config:config-item config:name="PrintRightPages" config:type="boolean">true</config:config-item>
|
||||
<config:config-item config:name="PrintFaxName" config:type="string"/>
|
||||
<config:config-item config:name="PrintPaperFromSetup" config:type="boolean">false</config:config-item>
|
||||
<config:config-item config:name="PrintTextPlaceholder" config:type="boolean">false</config:config-item>
|
||||
<config:config-item config:name="ApplyParagraphMarkFormatToNumbering" config:type="boolean">false</config:config-item>
|
||||
<config:config-item config:name="PrintReversed" config:type="boolean">false</config:config-item>
|
||||
<config:config-item config:name="TabOverMargin" config:type="boolean">false</config:config-item>
|
||||
<config:config-item config:name="EmbedFonts" config:type="boolean">false</config:config-item>
|
||||
<config:config-item config:name="SurroundTextWrapSmall" config:type="boolean">false</config:config-item>
|
||||
<config:config-item config:name="BackgroundParaOverDrawings" config:type="boolean">false</config:config-item>
|
||||
<config:config-item config:name="ClippedPictures" config:type="boolean">false</config:config-item>
|
||||
<config:config-item config:name="FloattableNomargins" config:type="boolean">false</config:config-item>
|
||||
<config:config-item config:name="UnbreakableNumberings" config:type="boolean">false</config:config-item>
|
||||
<config:config-item config:name="EmbedSystemFonts" config:type="boolean">false</config:config-item>
|
||||
<config:config-item config:name="TabOverflow" config:type="boolean">false</config:config-item>
|
||||
<config:config-item config:name="PrintTables" config:type="boolean">true</config:config-item>
|
||||
<config:config-item config:name="PrintSingleJobs" config:type="boolean">false</config:config-item>
|
||||
<config:config-item config:name="SmallCapsPercentage66" config:type="boolean">true</config:config-item>
|
||||
<config:config-item config:name="CollapseEmptyCellPara" config:type="boolean">true</config:config-item>
|
||||
<config:config-item config:name="TreatSingleColumnBreakAsPageBreak" config:type="boolean">false</config:config-item>
|
||||
<config:config-item config:name="MathBaselineAlignment" config:type="boolean">false</config:config-item>
|
||||
<config:config-item config:name="AddFrameOffsets" config:type="boolean">false</config:config-item>
|
||||
<config:config-item config:name="IsLabelDocument" config:type="boolean">false</config:config-item>
|
||||
<config:config-item config:name="PrinterName" config:type="string"/>
|
||||
<config:config-item config:name="OutlineLevelYieldsNumbering" config:type="boolean">false</config:config-item>
|
||||
<config:config-item config:name="IgnoreFirstLineIndentInNumbering" config:type="boolean">false</config:config-item>
|
||||
<config:config-item config:name="UpdateFromTemplate" config:type="boolean">true</config:config-item>
|
||||
<config:config-item config:name="PrintBlackFonts" config:type="boolean">false</config:config-item>
|
||||
<config:config-item config:name="TableRowKeep" config:type="boolean">false</config:config-item>
|
||||
<config:config-item config:name="EmbeddedDatabaseName" config:type="string"/>
|
||||
<config:config-item config:name="IgnoreTabsAndBlanksForLineCalculation" config:type="boolean">false</config:config-item>
|
||||
<config:config-item config:name="UseOldPrinterMetrics" config:type="boolean">false</config:config-item>
|
||||
<config:config-item config:name="InvertBorderSpacing" config:type="boolean">false</config:config-item>
|
||||
<config:config-item config:name="SaveGlobalDocumentLinks" config:type="boolean">false</config:config-item>
|
||||
<config:config-item config:name="TabsRelativeToIndent" config:type="boolean">true</config:config-item>
|
||||
<config:config-item config:name="Rsid" config:type="int">210213</config:config-item>
|
||||
<config:config-item config:name="PrintProspectRTL" config:type="boolean">false</config:config-item>
|
||||
<config:config-item config:name="PrintEmptyPages" config:type="boolean">true</config:config-item>
|
||||
<config:config-item config:name="ApplyUserData" config:type="boolean">false</config:config-item>
|
||||
<config:config-item config:name="PrintHiddenText" config:type="boolean">false</config:config-item>
|
||||
<config:config-item config:name="AddParaTableSpacingAtStart" config:type="boolean">true</config:config-item>
|
||||
<config:config-item config:name="FieldAutoUpdate" config:type="boolean">true</config:config-item>
|
||||
<config:config-item config:name="UseOldNumbering" config:type="boolean">false</config:config-item>
|
||||
<config:config-item config:name="AddParaTableSpacing" config:type="boolean">true</config:config-item>
|
||||
<config:config-item config:name="CharacterCompressionType" config:type="short">0</config:config-item>
|
||||
<config:config-item config:name="SaveVersionOnClose" config:type="boolean">false</config:config-item>
|
||||
<config:config-item config:name="ChartAutoUpdate" config:type="boolean">true</config:config-item>
|
||||
<config:config-item config:name="PrinterIndependentLayout" config:type="string">high-resolution</config:config-item>
|
||||
<config:config-item config:name="IsKernAsianPunctuation" config:type="boolean">false</config:config-item>
|
||||
<config:config-item config:name="UseFormerObjectPositioning" config:type="boolean">false</config:config-item>
|
||||
<config:config-item config:name="AddVerticalFrameOffsets" config:type="boolean">false</config:config-item>
|
||||
<config:config-item config:name="SubtractFlysAnchoredAtFlys" config:type="boolean">true</config:config-item>
|
||||
<config:config-item config:name="AddParaSpacingToTableCells" config:type="boolean">true</config:config-item>
|
||||
<config:config-item config:name="AddExternalLeading" config:type="boolean">true</config:config-item>
|
||||
<config:config-item config:name="CurrentDatabaseDataSource" config:type="string"/>
|
||||
<config:config-item config:name="AllowPrintJobCancel" config:type="boolean">true</config:config-item>
|
||||
<config:config-item config:name="ProtectForm" config:type="boolean">false</config:config-item>
|
||||
<config:config-item config:name="UseFormerLineSpacing" config:type="boolean">false</config:config-item>
|
||||
<config:config-item config:name="PrintDrawings" config:type="boolean">true</config:config-item>
|
||||
<config:config-item config:name="UseFormerTextWrapping" config:type="boolean">false</config:config-item>
|
||||
<config:config-item config:name="UnxForceZeroExtLeading" config:type="boolean">false</config:config-item>
|
||||
<config:config-item config:name="TabAtLeftIndentForParagraphsInList" config:type="boolean">false</config:config-item>
|
||||
<config:config-item config:name="RedlineProtectionKey" config:type="base64Binary"/>
|
||||
<config:config-item config:name="PropLineSpacingShrinksFirstLine" config:type="boolean">false</config:config-item>
|
||||
<config:config-item config:name="ConsiderTextWrapOnObjPos" config:type="boolean">false</config:config-item>
|
||||
<config:config-item config:name="RsidRoot" config:type="int">197346</config:config-item>
|
||||
<config:config-item config:name="StylesNoDefault" config:type="boolean">false</config:config-item>
|
||||
<config:config-item config:name="LinkUpdateMode" config:type="short">1</config:config-item>
|
||||
<config:config-item config:name="AlignTabStopPosition" config:type="boolean">true</config:config-item>
|
||||
<config:config-item config:name="DoNotJustifyLinesWithManualBreak" config:type="boolean">false</config:config-item>
|
||||
<config:config-item config:name="DoNotResetParaAttrsForNumFont" config:type="boolean">false</config:config-item>
|
||||
<config:config-item config:name="CurrentDatabaseCommandType" config:type="int">0</config:config-item>
|
||||
<config:config-item config:name="LoadReadonly" config:type="boolean">false</config:config-item>
|
||||
<config:config-item config:name="DoNotCaptureDrawObjsOnPage" config:type="boolean">false</config:config-item>
|
||||
<config:config-item config:name="CurrentDatabaseCommand" config:type="string"/>
|
||||
<config:config-item config:name="PrinterSetup" config:type="base64Binary"/>
|
||||
<config:config-item config:name="ClipAsCharacterAnchoredWriterFlyFrames" config:type="boolean">false</config:config-item>
|
||||
</config:config-item-set>
|
||||
</office:settings>
|
||||
<office:scripts>
|
||||
<office:script script:language="ooo:Basic">
|
||||
<ooo:libraries xmlns:ooo="http://openoffice.org/2004/office" xmlns:xlink="http://www.w3.org/1999/xlink"/>
|
||||
</office:script>
|
||||
</office:scripts>
|
||||
<office:font-face-decls>
|
||||
<style:font-face style:name="StarSymbol" svg:font-family="StarSymbol"/>
|
||||
<style:font-face style:name="Liberation Serif1" svg:font-family="'Liberation Serif'" style:font-adornments="Bold" style:font-family-generic="roman" style:font-pitch="variable"/>
|
||||
<style:font-face style:name="Liberation Serif" svg:font-family="'Liberation Serif'" style:font-adornments="Regular" style:font-family-generic="roman" style:font-pitch="variable"/>
|
||||
<style:font-face style:name="Thorndale AMT" svg:font-family="'Thorndale AMT'" style:font-family-generic="roman" style:font-pitch="variable"/>
|
||||
<style:font-face style:name="Liberation Sans" svg:font-family="'Liberation Sans'" style:font-adornments="Regular" style:font-family-generic="swiss" style:font-pitch="variable"/>
|
||||
<style:font-face style:name="Andale Sans UI" svg:font-family="'Andale Sans UI'" style:font-family-generic="system" style:font-pitch="variable"/>
|
||||
<style:font-face style:name="DejaVu Sans" svg:font-family="'DejaVu Sans'" style:font-family-generic="system" style:font-pitch="variable"/>
|
||||
</office:font-face-decls>
|
||||
<office:styles>
|
||||
<style:default-style style:family="graphic">
|
||||
<style:graphic-properties svg:stroke-color="#000000" draw:fill-color="#99ccff" fo:wrap-option="no-wrap" draw:shadow-offset-x="0.1181in" draw:shadow-offset-y="0.1181in" draw:start-line-spacing-horizontal="0.1114in" draw:start-line-spacing-vertical="0.1114in" draw:end-line-spacing-horizontal="0.1114in" draw:end-line-spacing-vertical="0.1114in" style:flow-with-text="false"/>
|
||||
<style:paragraph-properties style:text-autospace="ideograph-alpha" style:line-break="strict" style:writing-mode="lr-tb" style:font-independent-line-spacing="false">
|
||||
<style:tab-stops/>
|
||||
</style:paragraph-properties>
|
||||
<style:text-properties style:use-window-font-color="true" style:font-name="Thorndale AMT" fo:font-size="12pt" fo:language="en" fo:country="US" style:letter-kerning="true" style:font-name-asian="Andale Sans UI" style:font-size-asian="10.5pt" style:language-asian="zxx" style:country-asian="none" style:font-name-complex="Andale Sans UI" style:font-size-complex="12pt" style:language-complex="zxx" style:country-complex="none"/>
|
||||
</style:default-style>
|
||||
<style:default-style style:family="paragraph">
|
||||
<style:paragraph-properties fo:hyphenation-ladder-count="no-limit" style:text-autospace="ideograph-alpha" style:punctuation-wrap="hanging" style:line-break="strict" style:tab-stop-distance="0.4925in" style:writing-mode="lr-tb"/>
|
||||
<style:text-properties style:use-window-font-color="true" style:font-name="Thorndale AMT" fo:font-size="12pt" fo:language="en" fo:country="US" style:letter-kerning="true" style:font-name-asian="Andale Sans UI" style:font-size-asian="10.5pt" style:language-asian="zxx" style:country-asian="none" style:font-name-complex="Andale Sans UI" style:font-size-complex="12pt" style:language-complex="zxx" style:country-complex="none" fo:hyphenate="false" fo:hyphenation-remain-char-count="2" fo:hyphenation-push-char-count="2"/>
|
||||
</style:default-style>
|
||||
<style:default-style style:family="table">
|
||||
<style:table-properties table:border-model="collapsing"/>
|
||||
</style:default-style>
|
||||
<style:default-style style:family="table-row">
|
||||
<style:table-row-properties fo:keep-together="auto"/>
|
||||
</style:default-style>
|
||||
<style:style style:name="Standard" style:family="paragraph" style:class="text">
|
||||
<style:text-properties style:font-name="Liberation Sans" fo:font-family="'Liberation Sans'" style:font-style-name="Regular" style:font-family-generic="swiss" style:font-pitch="variable" style:font-size-asian="10.5pt"/>
|
||||
</style:style>
|
||||
<style:style style:name="Heading" style:family="paragraph" style:parent-style-name="Standard" style:next-style-name="Text_20_body" style:class="text">
|
||||
<style:paragraph-properties fo:margin-top="0.1665in" fo:margin-bottom="0.0835in" loext:contextual-spacing="false" fo:keep-with-next="always"/>
|
||||
<style:text-properties style:font-name="Liberation Serif" fo:font-family="'Liberation Serif'" style:font-style-name="Regular" style:font-family-generic="roman" style:font-pitch="variable" fo:font-size="16pt" style:font-name-asian="DejaVu Sans" style:font-family-asian="'DejaVu Sans'" style:font-family-generic-asian="system" style:font-pitch-asian="variable" style:font-size-asian="14pt" style:font-name-complex="DejaVu Sans" style:font-family-complex="'DejaVu Sans'" style:font-family-generic-complex="system" style:font-pitch-complex="variable" style:font-size-complex="14pt"/>
|
||||
</style:style>
|
||||
<style:style style:name="Text_20_body" style:display-name="Text body" style:family="paragraph" style:parent-style-name="Standard" style:class="text">
|
||||
<style:paragraph-properties fo:margin-top="0in" fo:margin-bottom="0.0835in" loext:contextual-spacing="false"/>
|
||||
<style:text-properties style:font-name="Liberation Sans" fo:font-family="'Liberation Sans'" style:font-style-name="Regular" style:font-family-generic="swiss" style:font-pitch="variable" style:font-size-asian="10.5pt"/>
|
||||
</style:style>
|
||||
<style:style style:name="List" style:family="paragraph" style:parent-style-name="Text_20_body" style:class="list">
|
||||
<style:text-properties style:font-size-asian="12pt"/>
|
||||
</style:style>
|
||||
<style:style style:name="Caption" style:family="paragraph" style:parent-style-name="Standard" style:class="extra">
|
||||
<style:paragraph-properties fo:margin-top="0.0835in" fo:margin-bottom="0.0835in" loext:contextual-spacing="false" text:number-lines="false" text:line-number="0"/>
|
||||
<style:text-properties fo:font-size="12pt" fo:font-style="italic" style:font-size-asian="12pt" style:font-style-asian="italic" style:font-size-complex="12pt" style:font-style-complex="italic"/>
|
||||
</style:style>
|
||||
<style:style style:name="Index" style:family="paragraph" style:parent-style-name="Standard" style:class="index">
|
||||
<style:paragraph-properties text:number-lines="false" text:line-number="0"/>
|
||||
<style:text-properties style:font-size-asian="12pt"/>
|
||||
</style:style>
|
||||
<style:style style:name="Header" style:family="paragraph" style:parent-style-name="Standard" style:class="extra">
|
||||
<style:paragraph-properties text:number-lines="false" text:line-number="0">
|
||||
<style:tab-stops>
|
||||
<style:tab-stop style:position="3.4626in" style:type="center"/>
|
||||
<style:tab-stop style:position="6.9252in" style:type="right"/>
|
||||
</style:tab-stops>
|
||||
</style:paragraph-properties>
|
||||
<style:text-properties fo:font-size="9pt" style:font-size-asian="10.5pt"/>
|
||||
</style:style>
|
||||
<style:style style:name="Heading_20_2" style:display-name="Heading 2" style:family="paragraph" style:parent-style-name="Heading" style:next-style-name="Text_20_body" style:class="text">
|
||||
<style:text-properties fo:font-size="14pt" fo:font-style="italic" fo:font-weight="bold" style:font-size-asian="14pt" style:font-style-asian="italic" style:font-weight-asian="bold" style:font-size-complex="14pt" style:font-style-complex="italic" style:font-weight-complex="bold"/>
|
||||
</style:style>
|
||||
<style:style style:name="Footer" style:family="paragraph" style:parent-style-name="Standard" style:class="extra">
|
||||
<style:paragraph-properties text:number-lines="false" text:line-number="0">
|
||||
<style:tab-stops>
|
||||
<style:tab-stop style:position="3.4626in" style:type="center"/>
|
||||
<style:tab-stop style:position="6.9252in" style:type="right"/>
|
||||
</style:tab-stops>
|
||||
</style:paragraph-properties>
|
||||
<style:text-properties fo:font-size="9pt" style:font-size-asian="10.5pt"/>
|
||||
</style:style>
|
||||
<style:style style:name="Heading_20_1" style:display-name="Heading 1" style:family="paragraph" style:parent-style-name="Heading" style:next-style-name="Text_20_body" style:class="text">
|
||||
<style:text-properties fo:font-size="16pt" fo:font-weight="bold" style:font-size-asian="115%" style:font-weight-asian="bold" style:font-size-complex="115%" style:font-weight-complex="bold"/>
|
||||
</style:style>
|
||||
<style:style style:name="Table_20_Contents" style:display-name="Table Contents" style:family="paragraph" style:parent-style-name="Standard" style:class="extra">
|
||||
<style:paragraph-properties text:number-lines="false" text:line-number="0"/>
|
||||
<style:text-properties style:font-size-asian="10.5pt"/>
|
||||
</style:style>
|
||||
<style:style style:name="Table_20_Heading" style:display-name="Table Heading" style:family="paragraph" style:parent-style-name="Table_20_Contents" style:class="extra" style:master-page-name="">
|
||||
<style:paragraph-properties fo:text-align="center" style:justify-single-word="false" style:page-number="auto" text:number-lines="false" text:line-number="0"/>
|
||||
<style:text-properties style:font-name="Liberation Serif1" fo:font-family="'Liberation Serif'" style:font-style-name="Bold" style:font-family-generic="roman" style:font-pitch="variable" fo:font-weight="bold" style:font-size-asian="10.5pt" style:font-weight-asian="bold" style:font-weight-complex="bold"/>
|
||||
</style:style>
|
||||
<style:style style:name="Heading_20_3" style:display-name="Heading 3" style:family="paragraph" style:parent-style-name="Heading" style:next-style-name="Text_20_body" style:class="text">
|
||||
<style:text-properties fo:font-size="14pt" fo:font-weight="bold" style:font-size-asian="14pt" style:font-weight-asian="bold" style:font-size-complex="14pt" style:font-weight-complex="bold"/>
|
||||
</style:style>
|
||||
<style:style style:name="Text_20_body_20_indent" style:display-name="Text body indent" style:family="paragraph" style:parent-style-name="Text_20_body" style:class="text">
|
||||
<style:paragraph-properties fo:margin-left="0.1965in" fo:margin-right="0in" fo:text-indent="0in" style:auto-text-indent="false"/>
|
||||
</style:style>
|
||||
<style:style style:name="Text" style:family="paragraph" style:parent-style-name="Caption" style:class="extra"/>
|
||||
<style:style style:name="Placeholder" style:family="text">
|
||||
<style:text-properties fo:font-variant="small-caps" fo:color="#008080" style:text-underline-style="dotted" style:text-underline-width="auto" style:text-underline-color="font-color"/>
|
||||
</style:style>
|
||||
<style:style style:name="Bullet_20_Symbols" style:display-name="Bullet Symbols" style:family="text">
|
||||
<style:text-properties style:font-name="StarSymbol" fo:font-family="StarSymbol" fo:font-size="9pt" style:font-name-asian="StarSymbol" style:font-family-asian="StarSymbol" style:font-size-asian="9pt" style:font-name-complex="StarSymbol" style:font-family-complex="StarSymbol" style:font-size-complex="9pt"/>
|
||||
</style:style>
|
||||
<text:outline-style style:name="Outline">
|
||||
<text:outline-level-style text:level="1" style:num-format="">
|
||||
<style:list-level-properties text:min-label-distance="0.15in"/>
|
||||
</text:outline-level-style>
|
||||
<text:outline-level-style text:level="2" style:num-format="">
|
||||
<style:list-level-properties text:min-label-distance="0.15in"/>
|
||||
</text:outline-level-style>
|
||||
<text:outline-level-style text:level="3" style:num-format="">
|
||||
<style:list-level-properties text:min-label-distance="0.15in"/>
|
||||
</text:outline-level-style>
|
||||
<text:outline-level-style text:level="4" style:num-format="">
|
||||
<style:list-level-properties text:min-label-distance="0.15in"/>
|
||||
</text:outline-level-style>
|
||||
<text:outline-level-style text:level="5" style:num-format="">
|
||||
<style:list-level-properties text:min-label-distance="0.15in"/>
|
||||
</text:outline-level-style>
|
||||
<text:outline-level-style text:level="6" style:num-format="">
|
||||
<style:list-level-properties text:min-label-distance="0.15in"/>
|
||||
</text:outline-level-style>
|
||||
<text:outline-level-style text:level="7" style:num-format="">
|
||||
<style:list-level-properties text:min-label-distance="0.15in"/>
|
||||
</text:outline-level-style>
|
||||
<text:outline-level-style text:level="8" style:num-format="">
|
||||
<style:list-level-properties text:min-label-distance="0.15in"/>
|
||||
</text:outline-level-style>
|
||||
<text:outline-level-style text:level="9" style:num-format="">
|
||||
<style:list-level-properties text:min-label-distance="0.15in"/>
|
||||
</text:outline-level-style>
|
||||
<text:outline-level-style text:level="10" style:num-format="">
|
||||
<style:list-level-properties text:min-label-distance="0.15in"/>
|
||||
</text:outline-level-style>
|
||||
</text:outline-style>
|
||||
<text:notes-configuration text:note-class="footnote" style:num-format="1" text:start-value="0" text:footnotes-position="page" text:start-numbering-at="document"/>
|
||||
<text:notes-configuration text:note-class="endnote" style:num-format="i" text:start-value="0"/>
|
||||
<text:linenumbering-configuration text:number-lines="false" text:offset="0.1965in" style:num-format="1" text:number-position="left" text:increment="5"/>
|
||||
</office:styles>
|
||||
<office:automatic-styles>
|
||||
<style:style style:name="P1" style:family="paragraph" style:parent-style-name="Standard">
|
||||
<style:paragraph-properties fo:margin-top="0in" fo:margin-bottom="0.2in" loext:contextual-spacing="false"/>
|
||||
</style:style>
|
||||
<style:style style:name="P2" style:family="paragraph" style:parent-style-name="Standard">
|
||||
<style:paragraph-properties fo:margin-top="0in" fo:margin-bottom="0in" loext:contextual-spacing="false"/>
|
||||
</style:style>
|
||||
<style:style style:name="P3" style:family="paragraph" style:parent-style-name="Standard" style:master-page-name="">
|
||||
<style:paragraph-properties fo:margin-top="0in" fo:margin-bottom="0in" loext:contextual-spacing="false" style:page-number="auto"/>
|
||||
</style:style>
|
||||
<style:style style:name="P4" style:family="paragraph" style:parent-style-name="Standard" style:master-page-name="">
|
||||
<style:paragraph-properties fo:margin-top="0.1965in" fo:margin-bottom="0in" loext:contextual-spacing="false" style:page-number="auto"/>
|
||||
</style:style>
|
||||
<style:style style:name="Sect1" style:family="section">
|
||||
<style:section-properties style:editable="false">
|
||||
<style:columns fo:column-count="1" fo:column-gap="0in"/>
|
||||
</style:section-properties>
|
||||
</style:style>
|
||||
<style:page-layout style:name="pm1">
|
||||
<style:page-layout-properties fo:page-width="8.2673in" fo:page-height="11.6925in" style:num-format="1" style:print-orientation="portrait" fo:margin-top="0.7874in" fo:margin-bottom="0.7874in" fo:margin-left="0.7874in" fo:margin-right="0.7874in" style:shadow="none" fo:background-color="transparent" style:writing-mode="lr-tb" style:layout-grid-color="#c0c0c0" style:layout-grid-lines="44" style:layout-grid-base-height="0.2165in" style:layout-grid-ruby-height="0in" style:layout-grid-mode="none" style:layout-grid-ruby-below="false" style:layout-grid-print="true" style:layout-grid-display="true" draw:fill="none" draw:fill-color="#99ccff" style:footnote-max-height="0in">
|
||||
<style:columns fo:column-count="2" fo:column-gap="0.5in">
|
||||
<style:column style:rel-width="32767*" fo:start-indent="0in" fo:end-indent="0.25in"/>
|
||||
<style:column style:rel-width="32768*" fo:start-indent="0.25in" fo:end-indent="0in"/>
|
||||
</style:columns>
|
||||
<style:footnote-sep style:width="0.0071in" style:distance-before-sep="0.0398in" style:distance-after-sep="0.0398in" style:line-style="none" style:adjustment="left" style:rel-width="25%" style:color="#000000"/>
|
||||
</style:page-layout-properties>
|
||||
<style:header-style/>
|
||||
<style:footer-style/>
|
||||
</style:page-layout>
|
||||
</office:automatic-styles>
|
||||
<office:master-styles>
|
||||
<style:master-page style:name="Standard" style:page-layout-name="pm1"/>
|
||||
</office:master-styles>
|
||||
<office:body>
|
||||
<office:text>
|
||||
<office:forms form:automatic-focus="false" form:apply-design-mode="false"/>
|
||||
<text:sequence-decls>
|
||||
<text:sequence-decl text:display-outline-level="0" text:name="Illustration"/>
|
||||
<text:sequence-decl text:display-outline-level="0" text:name="Table"/>
|
||||
<text:sequence-decl text:display-outline-level="0" text:name="Text"/>
|
||||
<text:sequence-decl text:display-outline-level="0" text:name="Drawing"/>
|
||||
</text:sequence-decls>
|
||||
<text:section text:style-name="Sect1" text:name="Section1">
|
||||
<text:p text:style-name="P3"><text:placeholder text:placeholder-type="text"><for each="party in records"></text:placeholder></text:p>
|
||||
<text:p text:style-name="P4"><text:placeholder text:placeholder-type="text"><party.full_name></text:placeholder></text:p>
|
||||
<text:p text:style-name="P1"><text:placeholder text:placeholder-type="text"><if test="party.addresses"></text:placeholder></text:p>
|
||||
<text:p text:style-name="P1"><text:placeholder text:placeholder-type="text"><for each="line in party.addresses[0].full_address.split('\n')"></text:placeholder></text:p>
|
||||
<text:p text:style-name="P2"><text:placeholder text:placeholder-type="text"><line></text:placeholder></text:p>
|
||||
<text:p text:style-name="P2"><text:placeholder text:placeholder-type="text"></for></text:placeholder></text:p>
|
||||
<text:p text:style-name="P2"><text:placeholder text:placeholder-type="text"><for each="i in range(5 -len(party.addresses[0].full_address.split('\n')))"></text:placeholder></text:p>
|
||||
<text:p text:style-name="P2"/>
|
||||
<text:p text:style-name="P2"><text:placeholder text:placeholder-type="text"></for></text:placeholder></text:p>
|
||||
<text:p text:style-name="P2"><text:placeholder text:placeholder-type="text"></if></text:placeholder></text:p>
|
||||
<text:p text:style-name="P2"><text:placeholder text:placeholder-type="text"><if test="not party.addresses"></text:placeholder></text:p>
|
||||
<text:p text:style-name="P2"/>
|
||||
<text:p text:style-name="P2"/>
|
||||
<text:p text:style-name="P2"/>
|
||||
<text:p text:style-name="P2"/>
|
||||
<text:p text:style-name="P2"/>
|
||||
<text:p text:style-name="P2"><text:placeholder text:placeholder-type="text"></if></text:placeholder></text:p>
|
||||
<text:p text:style-name="P2"><text:placeholder text:placeholder-type="text"></for></text:placeholder></text:p>
|
||||
</text:section>
|
||||
</office:text>
|
||||
</office:body>
|
||||
</office:document>
|
||||
1597
modules/party/locale/bg.po
Normal file
1597
modules/party/locale/bg.po
Normal file
File diff suppressed because it is too large
Load Diff
1562
modules/party/locale/ca.po
Normal file
1562
modules/party/locale/ca.po
Normal file
File diff suppressed because it is too large
Load Diff
1574
modules/party/locale/cs.po
Normal file
1574
modules/party/locale/cs.po
Normal file
File diff suppressed because it is too large
Load Diff
1574
modules/party/locale/de.po
Normal file
1574
modules/party/locale/de.po
Normal file
File diff suppressed because it is too large
Load Diff
1563
modules/party/locale/es.po
Normal file
1563
modules/party/locale/es.po
Normal file
File diff suppressed because it is too large
Load Diff
1561
modules/party/locale/es_419.po
Normal file
1561
modules/party/locale/es_419.po
Normal file
File diff suppressed because it is too large
Load Diff
1716
modules/party/locale/et.po
Normal file
1716
modules/party/locale/et.po
Normal file
File diff suppressed because it is too large
Load Diff
1612
modules/party/locale/fa.po
Normal file
1612
modules/party/locale/fa.po
Normal file
File diff suppressed because it is too large
Load Diff
1560
modules/party/locale/fi.po
Normal file
1560
modules/party/locale/fi.po
Normal file
File diff suppressed because it is too large
Load Diff
1567
modules/party/locale/fr.po
Normal file
1567
modules/party/locale/fr.po
Normal file
File diff suppressed because it is too large
Load Diff
1625
modules/party/locale/hu.po
Normal file
1625
modules/party/locale/hu.po
Normal file
File diff suppressed because it is too large
Load Diff
1591
modules/party/locale/id.po
Normal file
1591
modules/party/locale/id.po
Normal file
File diff suppressed because it is too large
Load Diff
1630
modules/party/locale/it.po
Normal file
1630
modules/party/locale/it.po
Normal file
File diff suppressed because it is too large
Load Diff
1639
modules/party/locale/lo.po
Normal file
1639
modules/party/locale/lo.po
Normal file
File diff suppressed because it is too large
Load Diff
1731
modules/party/locale/lt.po
Normal file
1731
modules/party/locale/lt.po
Normal file
File diff suppressed because it is too large
Load Diff
1563
modules/party/locale/nl.po
Normal file
1563
modules/party/locale/nl.po
Normal file
File diff suppressed because it is too large
Load Diff
1643
modules/party/locale/pl.po
Normal file
1643
modules/party/locale/pl.po
Normal file
File diff suppressed because it is too large
Load Diff
1570
modules/party/locale/pt.po
Normal file
1570
modules/party/locale/pt.po
Normal file
File diff suppressed because it is too large
Load Diff
1597
modules/party/locale/ro.po
Normal file
1597
modules/party/locale/ro.po
Normal file
File diff suppressed because it is too large
Load Diff
1601
modules/party/locale/ru.po
Normal file
1601
modules/party/locale/ru.po
Normal file
File diff suppressed because it is too large
Load Diff
1593
modules/party/locale/sl.po
Normal file
1593
modules/party/locale/sl.po
Normal file
File diff suppressed because it is too large
Load Diff
1560
modules/party/locale/tr.po
Normal file
1560
modules/party/locale/tr.po
Normal file
File diff suppressed because it is too large
Load Diff
1617
modules/party/locale/uk.po
Normal file
1617
modules/party/locale/uk.po
Normal file
File diff suppressed because it is too large
Load Diff
1577
modules/party/locale/zh_CN.po
Normal file
1577
modules/party/locale/zh_CN.po
Normal file
File diff suppressed because it is too large
Load Diff
58
modules/party/message.xml
Normal file
58
modules/party/message.xml
Normal file
@@ -0,0 +1,58 @@
|
||||
<?xml version="1.0"?>
|
||||
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
this repository contains the full copyright notices and license terms. -->
|
||||
<tryton>
|
||||
<data grouped="1">
|
||||
<record model="ir.message" id="msg_party_code_unique">
|
||||
<field name="text">The code on party must be unique.</field>
|
||||
</record>
|
||||
<record model="ir.message" id="msg_party_set_contact_mechanism">
|
||||
<field name="text">To change the "%(field)s" for party "%(party)s", you must edit their contact mechanisms.</field>
|
||||
</record>
|
||||
<record model="ir.message" id="msg_contact_mechanism_change_party">
|
||||
<field name="text">You cannot change the party of contact mechanism "%(contact)s".</field>
|
||||
</record>
|
||||
<record model="ir.message" id="msg_party_contact_mechanism_duplicate">
|
||||
<field name="text">The party "%(party)s" has the same %(type)s "%(value)s".</field>
|
||||
</record>
|
||||
<record model="ir.message" id="msg_invalid_phone_number">
|
||||
<field name="text">The phone number "%(phone)s" for party "%(party)s" is not valid.</field>
|
||||
</record>
|
||||
<record model="ir.message" id="msg_email_invalid">
|
||||
<field name="text">The email address "%(email)s" for party "%(party)s" is not valid.</field>
|
||||
</record>
|
||||
<record model="ir.message" id="msg_invalid_code">
|
||||
<field name="text">The %(type)s "%(code)s" for party "%(party)s" is not valid.</field>
|
||||
</record>
|
||||
<record model="ir.message" id="msg_party_identifier_duplicate">
|
||||
<field name="text">The party "%(party)s" has the same %(type)s "%(code)s".</field>
|
||||
</record>
|
||||
<record model="ir.message" id="msg_vies_unavailable">
|
||||
<field name="text">The VIES service is unavailable, try again later.</field>
|
||||
</record>
|
||||
<record model="ir.message" id="msg_different_name">
|
||||
<field name="text">Parties have different names: "%(source_name)s" vs "%(destination_name)s".</field>
|
||||
</record>
|
||||
<record model="ir.message" id="msg_different_tax_identifier">
|
||||
<field name="text">Parties have different tax identifiers: "%(source_code)s" vs "%(destination_code)s".</field>
|
||||
</record>
|
||||
<record model="ir.message" id="msg_erase_active_party">
|
||||
<field name="text">Party "%(party)s" cannot be erased because they are still active.</field>
|
||||
</record>
|
||||
<record model="ir.message" id="msg_address_change_party">
|
||||
<field name="text">You cannot change the party of address "%(address)s".</field>
|
||||
</record>
|
||||
<record model="ir.message" id="msg_invalid_format">
|
||||
<field name="text">Invalid format "%(format)s" with exception "%(exception)s".</field>
|
||||
</record>
|
||||
<record model="ir.message" id="msg_category_name_unique">
|
||||
<field name="text">The name of party category must be unique by parent.</field>
|
||||
</record>
|
||||
<record model="ir.message" id="msg_address_subdivision_country_code_unique">
|
||||
<field name="text">The country code on subdivision type must be unique.</field>
|
||||
</record>
|
||||
<record model="ir.message" id="msg_identifier_type_remove">
|
||||
<field name="text">To remove the identifier type "%(type)s" from the configuration, you must change it on "%(identifier)s".</field>
|
||||
</record>
|
||||
</data>
|
||||
</tryton>
|
||||
1243
modules/party/party.py
Normal file
1243
modules/party/party.py
Normal file
File diff suppressed because it is too large
Load Diff
190
modules/party/party.xml
Normal file
190
modules/party/party.xml
Normal file
@@ -0,0 +1,190 @@
|
||||
<?xml version="1.0"?>
|
||||
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
this repository contains the full copyright notices and license terms. -->
|
||||
<tryton>
|
||||
<data>
|
||||
<record model="res.group" id="group_party_admin">
|
||||
<field name="name">Party Administration</field>
|
||||
</record>
|
||||
<record model="res.user-res.group"
|
||||
id="user_admin_group_party_admin">
|
||||
<field name="user" ref="res.user_admin"/>
|
||||
<field name="group" ref="group_party_admin"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.icon" id="party_icon">
|
||||
<field name="name">tryton-party</field>
|
||||
<field name="path">icons/tryton-party.svg</field>
|
||||
</record>
|
||||
<menuitem
|
||||
name="Parties"
|
||||
sequence="10"
|
||||
id="menu_party"
|
||||
icon="tryton-party"/>
|
||||
|
||||
<menuitem
|
||||
name="Configuration"
|
||||
parent="menu_party"
|
||||
sequence="0"
|
||||
id="menu_configuration"
|
||||
icon="tryton-settings"/>
|
||||
<record model="ir.ui.menu-res.group"
|
||||
id="menu_party_group_party_admin">
|
||||
<field name="menu" ref="menu_configuration"/>
|
||||
<field name="group" ref="group_party_admin"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="party_view_tree">
|
||||
<field name="model">party.party</field>
|
||||
<field name="type">tree</field>
|
||||
<field name="name">party_tree</field>
|
||||
</record>
|
||||
<record model="ir.ui.view" id="party_view_form">
|
||||
<field name="model">party.party</field>
|
||||
<field name="type">form</field>
|
||||
<field name="name">party_form</field>
|
||||
</record>
|
||||
<record model="ir.action.act_window" id="act_party_form">
|
||||
<field name="name">Parties</field>
|
||||
<field name="res_model">party.party</field>
|
||||
</record>
|
||||
<record model="ir.action.act_window.view" id="act_party_form_view1">
|
||||
<field name="sequence" eval="10"/>
|
||||
<field name="view" ref="party_view_tree"/>
|
||||
<field name="act_window" ref="act_party_form"/>
|
||||
</record>
|
||||
<record model="ir.action.act_window.view" id="act_party_form_view2">
|
||||
<field name="sequence" eval="20"/>
|
||||
<field name="view" ref="party_view_form"/>
|
||||
<field name="act_window" ref="act_party_form"/>
|
||||
</record>
|
||||
<menuitem
|
||||
parent="menu_party"
|
||||
action="act_party_form"
|
||||
sequence="10"
|
||||
id="menu_party_form"/>
|
||||
|
||||
<record model="ir.action.act_window" id="act_party_by_category">
|
||||
<field name="name">Parties by Category</field>
|
||||
<field name="res_model">party.party</field>
|
||||
<field name="context"
|
||||
eval="{'categories': [Eval('active_id')]}" pyson="1"/>
|
||||
<field name="domain"
|
||||
eval="[('categories', 'child_of', [Eval('active_id')], 'parent')]"
|
||||
pyson="1"/>
|
||||
</record>
|
||||
<record model="ir.action.keyword" id="act_party_by_category_keyword1">
|
||||
<field name="keyword">tree_open</field>
|
||||
<field name="model">party.category,-1</field>
|
||||
<field name="action" ref="act_party_by_category"/>
|
||||
</record>
|
||||
<record model="ir.action.report" id="report_label">
|
||||
<field name="name">Labels</field>
|
||||
<field name="model">party.party</field>
|
||||
<field name="report_name">party.label</field>
|
||||
<field name="report">party/label.fodt</field>
|
||||
</record>
|
||||
<record model="ir.action.keyword" id="report_label_party">
|
||||
<field name="keyword">form_print</field>
|
||||
<field name="model">party.party,-1</field>
|
||||
<field name="action" ref="report_label"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.sequence.type" id="sequence_type_party">
|
||||
<field name="name">Party</field>
|
||||
</record>
|
||||
<record model="ir.sequence.type-res.group"
|
||||
id="sequence_type_party_group_admin">
|
||||
<field name="sequence_type" ref="sequence_type_party"/>
|
||||
<field name="group" ref="res.group_admin"/>
|
||||
</record>
|
||||
<record model="ir.sequence.type-res.group"
|
||||
id="sequence_type_party_group_party_admin">
|
||||
<field name="sequence_type" ref="sequence_type_party"/>
|
||||
<field name="group" ref="group_party_admin"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.sequence" id="sequence_party">
|
||||
<field name="name">Party</field>
|
||||
<field name="sequence_type" ref="sequence_type_party"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="identifier_form">
|
||||
<field name="model">party.identifier</field>
|
||||
<field name="type">form</field>
|
||||
<field name="name">identifier_form</field>
|
||||
</record>
|
||||
<record model="ir.ui.view" id="identifier_list">
|
||||
<field name="model">party.identifier</field>
|
||||
<field name="type">tree</field>
|
||||
<field name="priority" eval="10"/>
|
||||
<field name="name">identifier_list</field>
|
||||
</record>
|
||||
<record model="ir.ui.view" id="identifier_list_sequence">
|
||||
<field name="model">party.identifier</field>
|
||||
<field name="type">tree</field>
|
||||
<field name="priority" eval="20"/>
|
||||
<field name="name">identifier_list_sequence</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.action.wizard" id="wizard_check_vies">
|
||||
<field name="name">Check VIES</field>
|
||||
<field name="wiz_name">party.check_vies</field>
|
||||
<field name="model">party.party</field>
|
||||
</record>
|
||||
<record model="ir.action.keyword" id="check_vies_keyword">
|
||||
<field name="keyword">form_action</field>
|
||||
<field name="model">party.party,-1</field>
|
||||
<field name="action" ref="wizard_check_vies"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="check_vies_result">
|
||||
<field name="model">party.check_vies.result</field>
|
||||
<field name="type">form</field>
|
||||
<field name="name">check_vies_result</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.action.wizard" id="wizard_replace">
|
||||
<field name="name">Replace</field>
|
||||
<field name="wiz_name">party.replace</field>
|
||||
<field name="model">party.party</field>
|
||||
</record>
|
||||
<record model="ir.action-res.group"
|
||||
id="wizard_replace-group_party_admin">
|
||||
<field name="action" ref="wizard_replace"/>
|
||||
<field name="group" ref="group_party_admin"/>
|
||||
</record>
|
||||
<record model="ir.action.keyword" id="wizard_replace_keyword1">
|
||||
<field name="keyword">form_action</field>
|
||||
<field name="model">party.party,-1</field>
|
||||
<field name="action" ref="wizard_replace"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="replace_ask_view_form">
|
||||
<field name="model">party.replace.ask</field>
|
||||
<field name="type">form</field>
|
||||
<field name="name">replace_ask_form</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.action.wizard" id="wizard_erase">
|
||||
<field name="name">Erase</field>
|
||||
<field name="wiz_name">party.erase</field>
|
||||
<field name="model">party.party</field>
|
||||
</record>
|
||||
<record model="ir.action-res.group" id="wizard_erase-group_party_admin">
|
||||
<field name="action" ref="wizard_erase"/>
|
||||
<field name="group" ref="group_party_admin"/>
|
||||
</record>
|
||||
<record model="ir.action.keyword" id="wizard_erase_keyword1">
|
||||
<field name="keyword">form_action</field>
|
||||
<field name="model">party.party,-1</field>
|
||||
<field name="action" ref="wizard_erase"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="erase_ask_view_form">
|
||||
<field name="model">party.erase.ask</field>
|
||||
<field name="type">form</field>
|
||||
<field name="name">erase_ask_form</field>
|
||||
</record>
|
||||
</data>
|
||||
</tryton>
|
||||
6
modules/party/tests/__init__.py
Normal file
6
modules/party/tests/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
# 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 .test_module import PartyCheckEraseMixin, PartyCheckReplaceMixin
|
||||
|
||||
__all__ = [PartyCheckEraseMixin, PartyCheckReplaceMixin]
|
||||
BIN
modules/party/tests/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
modules/party/tests/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
modules/party/tests/__pycache__/test_module.cpython-311.pyc
Normal file
BIN
modules/party/tests/__pycache__/test_module.cpython-311.pyc
Normal file
Binary file not shown.
BIN
modules/party/tests/__pycache__/test_scenario.cpython-311.pyc
Normal file
BIN
modules/party/tests/__pycache__/test_scenario.cpython-311.pyc
Normal file
Binary file not shown.
@@ -0,0 +1,51 @@
|
||||
=====================================
|
||||
Party Contact Mechanism Notifications
|
||||
=====================================
|
||||
|
||||
Imports::
|
||||
|
||||
>>> from proteus import Model
|
||||
>>> from trytond.tests.tools import activate_modules
|
||||
|
||||
Activate modules::
|
||||
|
||||
>>> config = activate_modules('party')
|
||||
|
||||
>>> Party = Model.get('party.party')
|
||||
|
||||
Create first party::
|
||||
|
||||
>>> party1 = Party(name="Party 1")
|
||||
>>> contact_mechanism = party1.contact_mechanisms.new(type='email')
|
||||
>>> contact_mechanism.value = "test@example.com"
|
||||
>>> party1.save()
|
||||
|
||||
Create second party::
|
||||
|
||||
>>> party2 = Party(name="Party 2")
|
||||
>>> contact_mechanism = party2.contact_mechanisms.new(type='email')
|
||||
>>> contact_mechanism.value = "test@example.com"
|
||||
|
||||
Check notifications::
|
||||
|
||||
>>> len(contact_mechanism.notifications())
|
||||
1
|
||||
|
||||
Change contact mechanism value::
|
||||
|
||||
>>> contact_mechanism.value = "foo@example.com"
|
||||
|
||||
Check notifications::
|
||||
|
||||
>>> len(contact_mechanism.notifications())
|
||||
0
|
||||
|
||||
Change contact mechanism type::
|
||||
|
||||
>>> contact_mechanism.type = 'other'
|
||||
>>> contact_mechanism.value = "test@example.com"
|
||||
|
||||
Check notifications::
|
||||
|
||||
>>> len(contact_mechanism.notifications())
|
||||
0
|
||||
63
modules/party/tests/scenario_party_erase.rst
Normal file
63
modules/party/tests/scenario_party_erase.rst
Normal file
@@ -0,0 +1,63 @@
|
||||
====================
|
||||
Party Erase Scenario
|
||||
====================
|
||||
|
||||
Imports::
|
||||
|
||||
>>> from proteus import Model, Wizard
|
||||
>>> from trytond.tests.tools import activate_modules, assertEqual
|
||||
|
||||
Activate modules::
|
||||
|
||||
>>> config = activate_modules('party')
|
||||
|
||||
Create a party::
|
||||
|
||||
>>> Party = Model.get('party.party')
|
||||
>>> Attachment = Model.get('ir.attachment')
|
||||
>>> party = Party(name='Pam')
|
||||
>>> _ = party.identifiers.new(code="Identifier")
|
||||
>>> _ = party.contact_mechanisms.new(type='other', value="mechanism")
|
||||
>>> party.save()
|
||||
>>> address, = party.addresses
|
||||
>>> address.street = "St sample, 15"
|
||||
>>> address.city = "City"
|
||||
>>> address.save()
|
||||
>>> identifier, = party.identifiers
|
||||
>>> contact_mechanism, = party.contact_mechanisms
|
||||
>>> attachment = Attachment()
|
||||
>>> attachment.resource = party
|
||||
>>> attachment.name = "Attachment"
|
||||
>>> attachment.save()
|
||||
|
||||
Try erase active party::
|
||||
|
||||
>>> erase = Wizard('party.erase', models=[party])
|
||||
>>> assertEqual(erase.form.party, party)
|
||||
>>> erase.execute('erase')
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
EraseError: ...
|
||||
|
||||
Erase inactive party::
|
||||
|
||||
>>> party.active = False
|
||||
>>> party.save()
|
||||
|
||||
>>> erase = Wizard('party.erase', models=[party])
|
||||
>>> assertEqual(erase.form.party, party)
|
||||
>>> erase.execute('erase')
|
||||
|
||||
Check fields have been erased::
|
||||
|
||||
>>> party.name
|
||||
>>> identifier.reload()
|
||||
>>> identifier.code
|
||||
'****'
|
||||
>>> address.reload()
|
||||
>>> address.street
|
||||
>>> address.city
|
||||
>>> contact_mechanism.reload()
|
||||
>>> contact_mechanism.value
|
||||
>>> Attachment.find()
|
||||
[]
|
||||
@@ -0,0 +1,42 @@
|
||||
==============================
|
||||
Party Identifier Notifications
|
||||
==============================
|
||||
|
||||
Imports::
|
||||
|
||||
>>> from proteus import Model
|
||||
>>> from trytond.tests.tools import activate_modules
|
||||
|
||||
Activate modules::
|
||||
|
||||
>>> config = activate_modules('party')
|
||||
|
||||
>>> Party = Model.get('party.party')
|
||||
|
||||
Create first party::
|
||||
|
||||
>>> party1 = Party(name="Party 1")
|
||||
>>> identifier = party1.identifiers.new(type='be_vat')
|
||||
>>> identifier.code = "500923836"
|
||||
>>> party1.save()
|
||||
|
||||
Create second party::
|
||||
|
||||
>>> party2 = Party(name="Party 2")
|
||||
>>> identifier = party2.identifiers.new(type='be_vat')
|
||||
>>> identifier.code = "500923836"
|
||||
|
||||
Check notifications::
|
||||
|
||||
>>> len(identifier.notifications())
|
||||
1
|
||||
|
||||
Change identifier::
|
||||
|
||||
>>> identifier.type = None
|
||||
>>> identifier.code = "foo"
|
||||
|
||||
Check notifications::
|
||||
|
||||
>>> len(identifier.notifications())
|
||||
0
|
||||
41
modules/party/tests/scenario_party_phone_number.rst
Normal file
41
modules/party/tests/scenario_party_phone_number.rst
Normal file
@@ -0,0 +1,41 @@
|
||||
===========================
|
||||
Party Phone Number Scenario
|
||||
===========================
|
||||
|
||||
Imports::
|
||||
|
||||
>>> from proteus import Model
|
||||
>>> from trytond.tests.tools import activate_modules
|
||||
|
||||
Activate modules::
|
||||
|
||||
>>> config = activate_modules('party')
|
||||
|
||||
Create a country::
|
||||
|
||||
>>> Country = Model.get('country.country')
|
||||
>>> spain = Country(name='Spain', code='ES')
|
||||
>>> spain.save()
|
||||
|
||||
Create a party related to the country::
|
||||
|
||||
>>> Party = Model.get('party.party')
|
||||
>>> party = Party(name='Pam')
|
||||
>>> address, = party.addresses
|
||||
>>> address.country = spain
|
||||
|
||||
The country phone prefix is set when creating a phone of this party::
|
||||
|
||||
>>> local_phone = party.contact_mechanisms.new()
|
||||
>>> local_phone.type = 'phone'
|
||||
>>> local_phone.value = '666666666'
|
||||
>>> local_phone.value
|
||||
'+34 666 66 66 66'
|
||||
|
||||
The phone prefix is respected when using international prefix::
|
||||
|
||||
>>> international_phone = party.contact_mechanisms.new()
|
||||
>>> international_phone.type = 'phone'
|
||||
>>> international_phone.value = '+442083661178'
|
||||
>>> international_phone.value
|
||||
'+44 20 8366 1178'
|
||||
52
modules/party/tests/scenario_party_replace.rst
Normal file
52
modules/party/tests/scenario_party_replace.rst
Normal file
@@ -0,0 +1,52 @@
|
||||
======================
|
||||
Party Replace Scenario
|
||||
======================
|
||||
|
||||
Imports::
|
||||
|
||||
>>> from proteus import Model, Wizard
|
||||
>>> from trytond.tests.tools import activate_modules, assertEqual, assertFalse
|
||||
|
||||
Activate modules::
|
||||
|
||||
>>> config = activate_modules('party')
|
||||
|
||||
Create a party::
|
||||
|
||||
>>> Party = Model.get('party.party')
|
||||
>>> party1 = Party(name='Pam')
|
||||
>>> identifier1 = party1.identifiers.new()
|
||||
>>> identifier1.type = 'eu_vat'
|
||||
>>> identifier1.code = 'BE0897290877'
|
||||
>>> party1.save()
|
||||
>>> address1, = party1.addresses
|
||||
>>> identifier1, = party1.identifiers
|
||||
|
||||
Create a second party similar party::
|
||||
|
||||
>>> party2 = Party(name='Pam')
|
||||
>>> identifier2 = party2.identifiers.new()
|
||||
>>> identifier2.type = 'eu_vat'
|
||||
>>> identifier2.code = 'BE0897290877'
|
||||
>>> party2.save()
|
||||
>>> address2, = party2.addresses
|
||||
>>> identifier2, = party2.identifiers
|
||||
|
||||
Replace the second by the first party::
|
||||
|
||||
>>> replace = Wizard('party.replace', models=[party2])
|
||||
>>> assertEqual(replace.form.source, party2)
|
||||
>>> replace.form.destination = party1
|
||||
>>> replace.execute('replace')
|
||||
|
||||
>>> party2.reload()
|
||||
>>> bool(party2.active)
|
||||
False
|
||||
|
||||
>>> identifier2.reload()
|
||||
>>> assertEqual(identifier2.party, party1)
|
||||
>>> assertFalse(identifier2.active)
|
||||
|
||||
>>> address2.reload()
|
||||
>>> assertEqual(address2.party, party1)
|
||||
>>> assertFalse(address2.active)
|
||||
803
modules/party/tests/test_module.py
Normal file
803
modules/party/tests/test_module.py
Normal file
@@ -0,0 +1,803 @@
|
||||
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
# this repository contains the full copyright notices and license terms.
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from stdnum import get_cc_module
|
||||
|
||||
try:
|
||||
import phonenumbers
|
||||
except ImportError:
|
||||
phonenumbers = None
|
||||
|
||||
from trytond.exceptions import UserError
|
||||
from trytond.model.exceptions import AccessError
|
||||
from trytond.modules.party.party import (
|
||||
IDENTIFIER_TYPES, IDENTIFIER_VAT, replace_vat)
|
||||
from trytond.pool import Pool
|
||||
from trytond.tests.test_tryton import ModuleTestCase, with_transaction
|
||||
from trytond.tools import is_instance_method
|
||||
from trytond.transaction import Transaction
|
||||
|
||||
|
||||
class PartyCheckEraseMixin:
|
||||
|
||||
@with_transaction()
|
||||
def test_check_erase_party(self):
|
||||
"Test check erase of party"
|
||||
pool = Pool()
|
||||
Erase = pool.get('party.erase', type='wizard')
|
||||
Session = pool.get('ir.session.wizard')
|
||||
party = self.setup_check_erase_party()
|
||||
|
||||
session = Session()
|
||||
session.save()
|
||||
|
||||
Erase(session.id).check_erase(party)
|
||||
|
||||
def setup_check_erase_party(self):
|
||||
pool = Pool()
|
||||
Party = pool.get('party.party')
|
||||
|
||||
party = Party(active=False)
|
||||
party.save()
|
||||
return party
|
||||
|
||||
|
||||
class PartyCheckReplaceMixin:
|
||||
|
||||
@with_transaction()
|
||||
def test_check_replace_party(self):
|
||||
"Test fields to replace"
|
||||
pool = Pool()
|
||||
Replace = pool.get('party.replace', type='wizard')
|
||||
|
||||
for model_name, field_name in Replace.fields_to_replace():
|
||||
with self.subTest(model_name=model_name, field_name=field_name):
|
||||
Model = pool.get(model_name)
|
||||
field = getattr(Model, field_name)
|
||||
if field._type == 'reference':
|
||||
if isinstance(field.selection, (tuple, list)):
|
||||
self.assertIn('party.party', dict(field.selection))
|
||||
else:
|
||||
sel_func = getattr(Model, field.selection)
|
||||
instance_sel_func = is_instance_method(
|
||||
Model, field.selection)
|
||||
if not instance_sel_func:
|
||||
self.assertIn('party.party', dict(sel_func()))
|
||||
else:
|
||||
self.assertEqual(field._type, 'many2one')
|
||||
self.assertEqual(field.model_name, 'party.party')
|
||||
|
||||
|
||||
class PartyTestCase(
|
||||
PartyCheckEraseMixin, PartyCheckReplaceMixin, ModuleTestCase):
|
||||
'Test Party module'
|
||||
module = 'party'
|
||||
|
||||
@with_transaction()
|
||||
def test_category(self):
|
||||
'Create category'
|
||||
pool = Pool()
|
||||
Category = pool.get('party.category')
|
||||
category1, = Category.create([{
|
||||
'name': 'Category 1',
|
||||
}])
|
||||
self.assertTrue(category1.id)
|
||||
|
||||
@with_transaction()
|
||||
def test_category_recursion(self):
|
||||
'Test category recursion'
|
||||
pool = Pool()
|
||||
Category = pool.get('party.category')
|
||||
category1, = Category.create([{
|
||||
'name': 'Category 1',
|
||||
}])
|
||||
category2, = Category.create([{
|
||||
'name': 'Category 2',
|
||||
'parent': category1.id,
|
||||
}])
|
||||
self.assertTrue(category2.id)
|
||||
|
||||
self.assertRaises(Exception, Category.write, [category1], {
|
||||
'parent': category2.id,
|
||||
})
|
||||
|
||||
@with_transaction()
|
||||
def test_party(self):
|
||||
'Create party'
|
||||
pool = Pool()
|
||||
Party = pool.get('party.party')
|
||||
party1, = Party.create([{
|
||||
'name': 'Party 1',
|
||||
}])
|
||||
self.assertTrue(party1.id)
|
||||
|
||||
@with_transaction()
|
||||
def test_party_code(self):
|
||||
'Test party code constraint'
|
||||
pool = Pool()
|
||||
Party = pool.get('party.party')
|
||||
party1, = Party.create([{
|
||||
'name': 'Party 1',
|
||||
}])
|
||||
|
||||
code = party1.code
|
||||
self.assertTrue(party1.code_alnum.isalnum())
|
||||
self.assertTrue(party1.code_digit)
|
||||
|
||||
party2, = Party.create([{
|
||||
'name': 'Party 2',
|
||||
}])
|
||||
|
||||
self.assertRaises(Exception, Party.write, [party2], {
|
||||
'code': code,
|
||||
})
|
||||
|
||||
@with_transaction()
|
||||
def test_party_autocomplete_eu_vat(self):
|
||||
"Test party autocomplete eu_vat"
|
||||
pool = Pool()
|
||||
Party = pool.get('party.party')
|
||||
|
||||
self.assertEqual(
|
||||
Party.autocomplete('BE500923836'), [{
|
||||
'id': None,
|
||||
'name': 'BE0500923836',
|
||||
'defaults': {
|
||||
'identifiers': [{
|
||||
'type': 'eu_vat',
|
||||
'code': 'BE0500923836',
|
||||
}],
|
||||
},
|
||||
}])
|
||||
|
||||
@with_transaction()
|
||||
def test_party_autocomplete_eu_vat_without_country(self):
|
||||
"Test party autocomplete eu_vat without country"
|
||||
pool = Pool()
|
||||
Party = pool.get('party.party')
|
||||
|
||||
self.assertIn({
|
||||
'id': None,
|
||||
'name': 'BE0500923836',
|
||||
'defaults': {
|
||||
'identifiers': [{
|
||||
'type': 'eu_vat',
|
||||
'code': 'BE0500923836',
|
||||
}],
|
||||
},
|
||||
},
|
||||
Party.autocomplete('500923836'))
|
||||
|
||||
@with_transaction()
|
||||
def test_party_autocomplete_be_vat(self):
|
||||
"Test party autocomplete be_vat"
|
||||
pool = Pool()
|
||||
Party = pool.get('party.party')
|
||||
Configuration = pool.get('party.configuration')
|
||||
|
||||
configuration = Configuration(1)
|
||||
configuration.identifier_types = ['be_vat']
|
||||
configuration.save()
|
||||
|
||||
self.assertEqual(
|
||||
Party.autocomplete('BE500923836'), [{
|
||||
'id': None,
|
||||
'name': '0500923836',
|
||||
'defaults': {
|
||||
'identifiers': [{
|
||||
'type': 'be_vat',
|
||||
'code': '0500923836',
|
||||
}],
|
||||
},
|
||||
}])
|
||||
|
||||
@with_transaction()
|
||||
def test_party_default_get_eu_vat(self):
|
||||
"Test party default_get eu_vat"
|
||||
pool = Pool()
|
||||
Party = pool.get('party.party')
|
||||
Country = pool.get('country.country')
|
||||
|
||||
belgium = Country(code='BE', name="Belgium")
|
||||
belgium.save()
|
||||
|
||||
eu_vat = get_cc_module('eu', 'vat')
|
||||
with patch.object(eu_vat, 'check_vies') as check_vies:
|
||||
check_vies.return_value = {
|
||||
'valid': True,
|
||||
'name': "Tryton Foundation",
|
||||
'address': "Street",
|
||||
}
|
||||
with Transaction().set_context(default_identifiers=[{
|
||||
'type': 'eu_vat',
|
||||
'code': 'BE0500923836',
|
||||
}]):
|
||||
self.assertEqual(
|
||||
Party.default_get(['name', 'addresses', 'identifiers']), {
|
||||
'name': "Tryton Foundation",
|
||||
'addresses': [{
|
||||
'street': "Street",
|
||||
'country': belgium.id,
|
||||
'country.': {
|
||||
'rec_name': "🇧🇪 Belgium",
|
||||
},
|
||||
}],
|
||||
'identifiers': [{
|
||||
'type': 'eu_vat',
|
||||
'code': 'BE0500923836',
|
||||
}],
|
||||
})
|
||||
|
||||
@with_transaction()
|
||||
def test_address_strip(self):
|
||||
"Test address strip"
|
||||
pool = Pool()
|
||||
Address = pool.get('party.address')
|
||||
for value, result in [
|
||||
('', ''),
|
||||
(' ', ''),
|
||||
('\n', ''),
|
||||
(' \n \n', ''),
|
||||
('foo\n\n', 'foo'),
|
||||
(',foo', 'foo'),
|
||||
(',,foo', 'foo'),
|
||||
(', , foo', 'foo'),
|
||||
('foo, , bar', 'foo, bar'),
|
||||
('foo,,', 'foo'),
|
||||
('foo, , ', 'foo'),
|
||||
(',/–foo,/–', 'foo'),
|
||||
('foo, /bar', 'foo/bar'),
|
||||
('foo, /, bar', 'foo, bar'),
|
||||
('foo,/bar\nfoo, –\n/bar, foo', 'foo/bar\nfoo\nbar, foo'),
|
||||
]:
|
||||
with self.subTest(value=value):
|
||||
self.assertEqual(Address._strip(value), result)
|
||||
|
||||
@with_transaction()
|
||||
def test_address(self):
|
||||
'Create address'
|
||||
pool = Pool()
|
||||
Party = pool.get('party.party')
|
||||
Address = pool.get('party.address')
|
||||
party1, = Party.create([{
|
||||
'name': 'Party 1',
|
||||
}])
|
||||
|
||||
address, = Address.create([{
|
||||
'party': party1.id,
|
||||
'street': 'St sample, 15',
|
||||
'city': 'City',
|
||||
}])
|
||||
self.assertTrue(address.id)
|
||||
self.assertMultiLineEqual(address.full_address,
|
||||
"St sample, 15\n"
|
||||
"City")
|
||||
with Transaction().set_context(address_with_party=True):
|
||||
address = Address(address.id)
|
||||
self.assertMultiLineEqual(address.full_address,
|
||||
"Party 1\n"
|
||||
"St sample, 15\n"
|
||||
"City")
|
||||
|
||||
@with_transaction()
|
||||
def test_address_structured(self):
|
||||
"Test address structured"
|
||||
pool = Pool()
|
||||
Address = pool.get('party.address')
|
||||
Country = pool.get('country.country')
|
||||
|
||||
us = Country(code='US', name="US")
|
||||
us.save()
|
||||
be = Country(code='BE', name="BE")
|
||||
be.save()
|
||||
|
||||
address = Address(
|
||||
street_name="St sample",
|
||||
building_number="15",
|
||||
unit_number="B",
|
||||
floor_number=1,
|
||||
room_number=3,
|
||||
)
|
||||
|
||||
for country, result in [
|
||||
(None, "St sample 15/B/1/3"),
|
||||
(us, "15 St sample B"),
|
||||
(be, "St sample 15 box B"),
|
||||
]:
|
||||
with self.subTest(country=country.code if country else None):
|
||||
address.country = country
|
||||
self.assertEqual(address.street, result)
|
||||
|
||||
@with_transaction()
|
||||
def test_address_numbers(self):
|
||||
"Test address numbers"
|
||||
pool = Pool()
|
||||
Party = pool.get('party.party')
|
||||
Address = pool.get('party.address')
|
||||
|
||||
party = Party(name="Dunder Mifflin")
|
||||
party.save()
|
||||
address = Address(
|
||||
party=party,
|
||||
street_name="St sample",
|
||||
building_number="15",
|
||||
unit_number="B",
|
||||
floor_number=1,
|
||||
room_number=3,
|
||||
)
|
||||
address.save()
|
||||
|
||||
self.assertEqual(address.numbers, "15/B/1/3")
|
||||
|
||||
@with_transaction()
|
||||
def test_address_autocomplete_postal_code(self):
|
||||
"Test autocomplete of postal code"
|
||||
pool = Pool()
|
||||
Country = pool.get('country.country')
|
||||
Subdivision = pool.get('country.subdivision')
|
||||
PostalCode = pool.get('country.postal_code')
|
||||
Address = pool.get('party.address')
|
||||
|
||||
country = Country(name="Country")
|
||||
country.save()
|
||||
subdivision = Subdivision(name="Subdivision", country=country)
|
||||
subdivision.save()
|
||||
postal_code = PostalCode(
|
||||
country=country,
|
||||
subdivision=subdivision,
|
||||
postal_code="12345",
|
||||
city="City",
|
||||
)
|
||||
postal_code.save()
|
||||
|
||||
address = Address(
|
||||
country=country, subdivision=subdivision, city="C")
|
||||
completions = address.autocomplete_postal_code()
|
||||
|
||||
self.assertEqual(completions, ["12345"])
|
||||
|
||||
@with_transaction()
|
||||
def test_address_autocomplete_postal_code_empty(self):
|
||||
"Test autocomplete with empty postal code"
|
||||
pool = Pool()
|
||||
Country = pool.get('country.country')
|
||||
PostalCode = pool.get('country.postal_code')
|
||||
Address = pool.get('party.address')
|
||||
|
||||
country = Country(name="Country")
|
||||
country.save()
|
||||
for postal_code in [None, '12345']:
|
||||
PostalCode(
|
||||
country=country,
|
||||
postal_code=postal_code,
|
||||
city="City",
|
||||
).save()
|
||||
|
||||
completions = Address(city="City").autocomplete_postal_code()
|
||||
|
||||
self.assertEqual(completions, ["12345"])
|
||||
|
||||
@with_transaction()
|
||||
def test_address_autocomplete_city(self):
|
||||
"Test autocomplete of city"
|
||||
pool = Pool()
|
||||
Country = pool.get('country.country')
|
||||
Subdivision = pool.get('country.subdivision')
|
||||
PostalCode = pool.get('country.postal_code')
|
||||
Address = pool.get('party.address')
|
||||
|
||||
country = Country(name="Country")
|
||||
country.save()
|
||||
subdivision = Subdivision(name="Subdivision", country=country)
|
||||
subdivision.save()
|
||||
postal_code = PostalCode(
|
||||
country=country,
|
||||
subdivision=subdivision,
|
||||
postal_code="12345",
|
||||
city="City",
|
||||
)
|
||||
postal_code.save()
|
||||
|
||||
address = Address(
|
||||
country=country, subdivision=subdivision, postal_code="123")
|
||||
completions = address.autocomplete_city()
|
||||
|
||||
self.assertEqual(completions, ["City"])
|
||||
|
||||
@with_transaction()
|
||||
def test_address_autocomplete_city_empty(self):
|
||||
"Test autocomplete with empty city"
|
||||
pool = Pool()
|
||||
Country = pool.get('country.country')
|
||||
PostalCode = pool.get('country.postal_code')
|
||||
Address = pool.get('party.address')
|
||||
|
||||
country = Country(name="Country")
|
||||
country.save()
|
||||
for city in [None, "City"]:
|
||||
PostalCode(
|
||||
country=country,
|
||||
postal_code="12345",
|
||||
city=city,
|
||||
).save()
|
||||
|
||||
completions = Address(postal_code="12345").autocomplete_city()
|
||||
|
||||
self.assertEqual(completions, ["City"])
|
||||
|
||||
@with_transaction()
|
||||
def test_full_address_country_subdivision(self):
|
||||
'Test full address with country and subdivision'
|
||||
pool = Pool()
|
||||
Party = pool.get('party.party')
|
||||
Country = pool.get('country.country')
|
||||
Subdivision = pool.get('country.subdivision')
|
||||
Address = pool.get('party.address')
|
||||
party, = Party.create([{
|
||||
'name': 'Party',
|
||||
}])
|
||||
country = Country(name='Country')
|
||||
country.save()
|
||||
subdivision = Subdivision(
|
||||
name='Subdivision', country=country, code='SUB', type='area')
|
||||
subdivision.save()
|
||||
address, = Address.create([{
|
||||
'party': party.id,
|
||||
'subdivision': subdivision.id,
|
||||
'country': country.id,
|
||||
}])
|
||||
self.assertMultiLineEqual(address.full_address,
|
||||
"Subdivision\n"
|
||||
"COUNTRY")
|
||||
|
||||
@with_transaction()
|
||||
def test_address_get_no_type(self):
|
||||
"Test address_get with no type"
|
||||
pool = Pool()
|
||||
Party = pool.get('party.party')
|
||||
Address = pool.get('party.address')
|
||||
party, = Party.create([{}])
|
||||
address1, address2 = Address.create([{
|
||||
'party': party.id,
|
||||
'sequence': 1,
|
||||
}, {
|
||||
'party': party.id,
|
||||
'sequence': 2,
|
||||
}])
|
||||
|
||||
address = party.address_get()
|
||||
|
||||
self.assertEqual(address, address1)
|
||||
|
||||
@with_transaction()
|
||||
def test_address_get_no_address(self):
|
||||
"Test address_get with no address"
|
||||
pool = Pool()
|
||||
Party = pool.get('party.party')
|
||||
party, = Party.create([{}])
|
||||
|
||||
address = party.address_get()
|
||||
|
||||
self.assertEqual(address, None)
|
||||
|
||||
@with_transaction()
|
||||
def test_address_get_inactive(self):
|
||||
"Test address_get with inactive"
|
||||
pool = Pool()
|
||||
Party = pool.get('party.party')
|
||||
Address = pool.get('party.address')
|
||||
party, = Party.create([{}])
|
||||
address1, address2 = Address.create([{
|
||||
'party': party.id,
|
||||
'sequence': 1,
|
||||
'active': False,
|
||||
}, {
|
||||
'party': party.id,
|
||||
'sequence': 2,
|
||||
'active': True,
|
||||
}])
|
||||
|
||||
address = party.address_get()
|
||||
|
||||
self.assertEqual(address, address2)
|
||||
|
||||
@with_transaction()
|
||||
def test_address_get_type(self):
|
||||
"Test address_get with type"
|
||||
pool = Pool()
|
||||
Party = pool.get('party.party')
|
||||
Address = pool.get('party.address')
|
||||
party, = Party.create([{}])
|
||||
address1, address2 = Address.create([{
|
||||
'party': party.id,
|
||||
'sequence': 1,
|
||||
'postal_code': None,
|
||||
}, {
|
||||
'party': party.id,
|
||||
'sequence': 2,
|
||||
'postal_code': '1000',
|
||||
}])
|
||||
|
||||
address = party.address_get(type='postal_code')
|
||||
|
||||
self.assertEqual(address, address2)
|
||||
|
||||
@with_transaction()
|
||||
def test_party_label_report(self):
|
||||
'Test party label report'
|
||||
pool = Pool()
|
||||
Party = pool.get('party.party')
|
||||
Label = pool.get('party.label', type='report')
|
||||
party1, = Party.create([{
|
||||
'name': 'Party 1',
|
||||
}])
|
||||
oext, content, _, _ = Label.execute([party1.id], {})
|
||||
self.assertEqual(oext, 'odt')
|
||||
self.assertTrue(content)
|
||||
|
||||
@with_transaction()
|
||||
def test_party_without_name(self):
|
||||
'Create party without name'
|
||||
pool = Pool()
|
||||
Party = pool.get('party.party')
|
||||
party2, = Party.create([{}])
|
||||
self.assertTrue(party2.id)
|
||||
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')
|
||||
transaction = Transaction()
|
||||
|
||||
def create(mtype, mvalue):
|
||||
party1, = Party.create([{
|
||||
'name': 'Party 1',
|
||||
}])
|
||||
return ContactMechanism.create([{
|
||||
'party': party1.id,
|
||||
'type': mtype,
|
||||
'value': mvalue,
|
||||
}])[0]
|
||||
|
||||
# Test format on create
|
||||
mechanism = create('phone', '+442083661177')
|
||||
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')
|
||||
|
||||
# Test rejection of a phone type mechanism to non-phone value
|
||||
with self.assertRaises(UserError):
|
||||
mechanism.value = 'notaphone@example.com'
|
||||
mechanism.save()
|
||||
transaction.rollback()
|
||||
|
||||
# Test rejection of invalid phone number creation
|
||||
with self.assertRaises(UserError):
|
||||
mechanism = create('phone', 'alsonotaphone@example.com')
|
||||
transaction.rollback()
|
||||
|
||||
# Test acceptance of a non-phone value when type is non-phone
|
||||
mechanism = create('email', 'name@example.com')
|
||||
|
||||
@with_transaction()
|
||||
def test_set_contact_mechanism(self):
|
||||
"Test set_contact_mechanism"
|
||||
pool = Pool()
|
||||
Party = pool.get('party.party')
|
||||
|
||||
party = Party(email='test@example.com')
|
||||
party.save()
|
||||
|
||||
self.assertEqual(party.email, 'test@example.com')
|
||||
|
||||
@with_transaction()
|
||||
def test_set_contact_mechanism_with_value(self):
|
||||
"Test set_contact_mechanism"
|
||||
pool = Pool()
|
||||
Party = pool.get('party.party')
|
||||
|
||||
party = Party(email='foo@example.com')
|
||||
party.save()
|
||||
|
||||
party.email = 'bar@example.com'
|
||||
with self.assertRaises(AccessError):
|
||||
party.save()
|
||||
|
||||
@with_transaction()
|
||||
def test_contact_mechanism_get_no_usage(self):
|
||||
"Test contact_mechanism_get with no usage"
|
||||
pool = Pool()
|
||||
Party = pool.get('party.party')
|
||||
ContactMechanism = pool.get('party.contact_mechanism')
|
||||
party, = Party.create([{}])
|
||||
contact1, contact2 = ContactMechanism.create([{
|
||||
'party': party.id,
|
||||
'sequence': 1,
|
||||
'type': 'email',
|
||||
'value': 'test1@example.com',
|
||||
}, {
|
||||
'party': party.id,
|
||||
'sequence': 2,
|
||||
'type': 'email',
|
||||
'value': 'test2@example.com',
|
||||
}])
|
||||
|
||||
contact = party.contact_mechanism_get('email')
|
||||
|
||||
self.assertEqual(contact, contact1)
|
||||
|
||||
@with_transaction()
|
||||
def test_contact_mechanism_get_many_types(self):
|
||||
"Test contact_mechanism_get with many types"
|
||||
pool = Pool()
|
||||
Party = pool.get('party.party')
|
||||
ContactMechanism = pool.get('party.contact_mechanism')
|
||||
party, = Party.create([{}])
|
||||
contact1, contact2 = ContactMechanism.create([{
|
||||
'party': party.id,
|
||||
'sequence': 1,
|
||||
'type': 'other',
|
||||
'value': 'test',
|
||||
}, {
|
||||
'party': party.id,
|
||||
'sequence': 2,
|
||||
'type': 'email',
|
||||
'value': 'test2@example.com',
|
||||
}])
|
||||
|
||||
contact = party.contact_mechanism_get({'email', 'phone'})
|
||||
|
||||
self.assertEqual(contact, contact2)
|
||||
|
||||
@with_transaction()
|
||||
def test_contact_mechanism_get_no_contact_mechanism(self):
|
||||
"Test contact_mechanism_get with no contact mechanism"
|
||||
pool = Pool()
|
||||
Party = pool.get('party.party')
|
||||
party, = Party.create([{}])
|
||||
|
||||
contact = party.contact_mechanism_get()
|
||||
|
||||
self.assertEqual(contact, None)
|
||||
|
||||
@with_transaction()
|
||||
def test_contact_mechanism_get_no_type(self):
|
||||
"Test contact_mechanism_get with no type"
|
||||
pool = Pool()
|
||||
Party = pool.get('party.party')
|
||||
ContactMechanism = pool.get('party.contact_mechanism')
|
||||
party, = Party.create([{}])
|
||||
ContactMechanism.create([{
|
||||
'party': party.id,
|
||||
'type': 'email',
|
||||
'value': 'test1@example.com',
|
||||
}])
|
||||
|
||||
contact = party.contact_mechanism_get('phone')
|
||||
|
||||
self.assertEqual(contact, None)
|
||||
|
||||
@with_transaction()
|
||||
def test_contact_mechanism_get_any_type(self):
|
||||
"Test contact_mechanism_get with any type"
|
||||
pool = Pool()
|
||||
Party = pool.get('party.party')
|
||||
ContactMechanism = pool.get('party.contact_mechanism')
|
||||
party, = Party.create([{}])
|
||||
email1, = ContactMechanism.create([{
|
||||
'party': party.id,
|
||||
'type': 'email',
|
||||
'value': 'test1@example.com',
|
||||
}])
|
||||
|
||||
contact = party.contact_mechanism_get()
|
||||
|
||||
self.assertEqual(contact, email1)
|
||||
|
||||
@with_transaction()
|
||||
def test_contact_mechanism_get_inactive(self):
|
||||
"Test contact_mechanism_get with inactive"
|
||||
pool = Pool()
|
||||
Party = pool.get('party.party')
|
||||
ContactMechanism = pool.get('party.contact_mechanism')
|
||||
party, = Party.create([{}])
|
||||
contact1, contact2 = ContactMechanism.create([{
|
||||
'party': party.id,
|
||||
'sequence': 1,
|
||||
'type': 'email',
|
||||
'value': 'test1@example.com',
|
||||
'active': False,
|
||||
}, {
|
||||
'party': party.id,
|
||||
'sequence': 2,
|
||||
'type': 'email',
|
||||
'value': 'test2@example.com',
|
||||
'active': True,
|
||||
}])
|
||||
|
||||
contact = party.contact_mechanism_get()
|
||||
|
||||
self.assertEqual(contact, contact2)
|
||||
|
||||
@with_transaction()
|
||||
def test_contact_mechanism_get_usage(self):
|
||||
"Test contact_mechanism_get with usage"
|
||||
pool = Pool()
|
||||
Party = pool.get('party.party')
|
||||
ContactMechanism = pool.get('party.contact_mechanism')
|
||||
party, = Party.create([{}])
|
||||
contact1, contact2 = ContactMechanism.create([{
|
||||
'party': party.id,
|
||||
'sequence': 1,
|
||||
'type': 'email',
|
||||
'value': 'test1@example.com',
|
||||
'name': None,
|
||||
}, {
|
||||
'party': party.id,
|
||||
'sequence': 2,
|
||||
'type': 'email',
|
||||
'value': 'test2@example.com',
|
||||
'name': 'email',
|
||||
}])
|
||||
|
||||
contact = party.contact_mechanism_get(usage='name')
|
||||
|
||||
self.assertEqual(contact, contact2)
|
||||
|
||||
@with_transaction()
|
||||
def test_tax_identifier_types(self):
|
||||
"Ensure tax identifier types are in identifier types"
|
||||
pool = Pool()
|
||||
Party = pool.get('party.party')
|
||||
identifiers = dict(IDENTIFIER_TYPES).keys()
|
||||
tax_identifiers = set(Party.tax_identifier_types())
|
||||
self.assertLessEqual(tax_identifiers, identifiers)
|
||||
|
||||
@with_transaction()
|
||||
def test_identifier_vat_types(self):
|
||||
"Ensure VAT identifiers are identifier types"
|
||||
identifiers = dict(IDENTIFIER_TYPES).keys()
|
||||
vat_identifiers = set(map(replace_vat, IDENTIFIER_VAT))
|
||||
self.assertLessEqual(vat_identifiers, identifiers)
|
||||
|
||||
@with_transaction()
|
||||
def test_party_distance(self):
|
||||
"Test party distance"
|
||||
pool = Pool()
|
||||
Party = pool.get('party.party')
|
||||
|
||||
A, B, = Party.create([{
|
||||
'name': 'A',
|
||||
}, {
|
||||
'name': 'B',
|
||||
}])
|
||||
|
||||
parties = Party.search([])
|
||||
self.assertEqual([p.distance for p in parties], [None] * 2)
|
||||
|
||||
with Transaction().set_context(related_party=A.id):
|
||||
parties = Party.search([])
|
||||
self.assertEqual(
|
||||
[(p.name, p.distance) for p in parties],
|
||||
[('A', 0), ('B', None)])
|
||||
|
||||
|
||||
del ModuleTestCase
|
||||
8
modules/party/tests/test_scenario.py
Normal file
8
modules/party/tests/test_scenario.py
Normal file
@@ -0,0 +1,8 @@
|
||||
# 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 trytond.tests.test_tryton import load_doc_tests
|
||||
|
||||
|
||||
def load_tests(*args, **kwargs):
|
||||
return load_doc_tests(__name__, __file__, *args, **kwargs)
|
||||
40
modules/party/tryton.cfg
Normal file
40
modules/party/tryton.cfg
Normal file
@@ -0,0 +1,40 @@
|
||||
[tryton]
|
||||
version=7.8.2
|
||||
depends:
|
||||
country
|
||||
ir
|
||||
res
|
||||
xml:
|
||||
party.xml
|
||||
category.xml
|
||||
address.xml
|
||||
contact_mechanism.xml
|
||||
configuration.xml
|
||||
ir.xml
|
||||
message.xml
|
||||
|
||||
[register]
|
||||
model:
|
||||
country.PostalCode
|
||||
category.Category
|
||||
party.Party
|
||||
party.PartyLang
|
||||
party.PartyCategory
|
||||
party.Identifier
|
||||
party.CheckVIESResult
|
||||
party.ReplaceAsk
|
||||
party.EraseAsk
|
||||
address.Address
|
||||
address.AddressFormat
|
||||
address.SubdivisionType
|
||||
contact_mechanism.ContactMechanism
|
||||
contact_mechanism.ContactMechanismLanguage
|
||||
configuration.Configuration
|
||||
configuration.ConfigurationSequence
|
||||
configuration.ConfigurationLang
|
||||
ir.Email
|
||||
ir.EmailTemplate
|
||||
wizard:
|
||||
party.CheckVIES
|
||||
party.Replace
|
||||
party.Erase
|
||||
65
modules/party/view/address_form.xml
Normal file
65
modules/party/view/address_form.xml
Normal file
@@ -0,0 +1,65 @@
|
||||
<?xml version="1.0"?>
|
||||
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
this repository contains the full copyright notices and license terms. -->
|
||||
<form col="6">
|
||||
<label name="party"/>
|
||||
<field name="party" colspan="5"/>
|
||||
<label name="party_name"/>
|
||||
<field name="party_name"/>
|
||||
<group colspan="2" col="-1" id="checkboxes">
|
||||
<label name="active"/>
|
||||
<field name="active"
|
||||
xexpand="0" width="25"/>
|
||||
<!-- Add here some checkboxes ! -->
|
||||
<label name="sequence"/>
|
||||
<field name="sequence"/>
|
||||
</group>
|
||||
<notebook colspan="6">
|
||||
<page string="General" id="general">
|
||||
<label name="street"/>
|
||||
<group id="street" colspan="3" col="2">
|
||||
<field name="street" yexpand="1"/>
|
||||
<group id="street_structured" yalign="0" yexpand="1">
|
||||
<label name="street_name"/>
|
||||
<field name="street_name" colspan="3"/>
|
||||
|
||||
<label name="building_name"/>
|
||||
<field name="building_name" colspan="3"/>
|
||||
|
||||
<label name="building_number"/>
|
||||
<field name="building_number"/>
|
||||
<label name="unit_number"/>
|
||||
<field name="unit_number"/>
|
||||
|
||||
<label name="floor_number"/>
|
||||
<field name="floor_number"/>
|
||||
<label name="room_number"/>
|
||||
<field name="room_number"/>
|
||||
|
||||
<label name="post_box"/>
|
||||
<field name="post_box"/>
|
||||
<label name="private_bag"/>
|
||||
<field name="private_bag"/>
|
||||
<label name="post_office"/>
|
||||
<field name="post_office" colspan="3"/>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
<label name="postal_code"/>
|
||||
<field name="postal_code"/>
|
||||
<label name="city"/>
|
||||
<field name="city"/>
|
||||
<newline/>
|
||||
<label name="country"/>
|
||||
<field name="country"/>
|
||||
<label name="subdivision"/>
|
||||
<field name="subdivision"/>
|
||||
</page>
|
||||
<page name="identifiers">
|
||||
<field name="identifiers" colspan="4" pre_validate="1" view_ids="party.identifier_list_sequence"/>
|
||||
</page>
|
||||
<page name="contact_mechanisms">
|
||||
<field name="contact_mechanisms" colspan="4" pre_validate="1" view_ids="party.contact_mechanism_view_tree_sequence"/>
|
||||
</page>
|
||||
</notebook>
|
||||
</form>
|
||||
57
modules/party/view/address_form_simple.xml
Normal file
57
modules/party/view/address_form_simple.xml
Normal file
@@ -0,0 +1,57 @@
|
||||
<?xml version="1.0"?>
|
||||
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
this repository contains the full copyright notices and license terms. -->
|
||||
<form col="6">
|
||||
<label name="party"/>
|
||||
<field name="party" colspan="5"/>
|
||||
<label name="party_name"/>
|
||||
<field name="party_name"/>
|
||||
<group colspan="2" col="-1" id="checkboxes">
|
||||
<label name="active"/>
|
||||
<field name="active"
|
||||
xexpand="0" width="25"/>
|
||||
<!-- Add here some checkboxes ! -->
|
||||
<label name="sequence"/>
|
||||
<field name="sequence"/>
|
||||
</group>
|
||||
<newline/>
|
||||
|
||||
<label name="street"/>
|
||||
<group id="street" colspan="5" col="2">
|
||||
<field name="street" yexpand="1"/>
|
||||
<group id="street_structured" yalign="0" yexpand="1">
|
||||
<label name="street_name"/>
|
||||
<field name="street_name" colspan="3"/>
|
||||
|
||||
<label name="building_name"/>
|
||||
<field name="building_name" colspan="3"/>
|
||||
|
||||
<label name="building_number"/>
|
||||
<field name="building_number"/>
|
||||
<label name="unit_number"/>
|
||||
<field name="unit_number"/>
|
||||
|
||||
<label name="floor_number"/>
|
||||
<field name="floor_number"/>
|
||||
<label name="room_number"/>
|
||||
<field name="room_number"/>
|
||||
|
||||
<label name="post_box"/>
|
||||
<field name="post_box"/>
|
||||
<label name="private_bag"/>
|
||||
<field name="private_bag"/>
|
||||
<label name="post_office"/>
|
||||
<field name="post_office" colspan="3"/>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
<label name="postal_code"/>
|
||||
<field name="postal_code"/>
|
||||
<label name="city"/>
|
||||
<field name="city"/>
|
||||
<newline/>
|
||||
<label name="country"/>
|
||||
<field name="country"/>
|
||||
<label name="subdivision"/>
|
||||
<field name="subdivision"/>
|
||||
</form>
|
||||
27
modules/party/view/address_format_form.xml
Normal file
27
modules/party/view/address_format_form.xml
Normal file
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0"?>
|
||||
<form col="6">
|
||||
<label name="country_code"/>
|
||||
<field name="country_code"/>
|
||||
<label name="language_code"/>
|
||||
<field name="language_code"/>
|
||||
<label name="active"/>
|
||||
<field name="active"/>
|
||||
<separator name="format_" colspan="6"/>
|
||||
<field name="format_" colspan="6"/>
|
||||
<separator name="street_format" colspan="6"/>
|
||||
<field name="street_format" colspan="6"/>
|
||||
<label name="building_number_format"/>
|
||||
<field name="building_number_format" colspan="5"/>
|
||||
<label name="unit_number_format"/>
|
||||
<field name="unit_number_format" colspan="5"/>
|
||||
<label name="floor_number_format"/>
|
||||
<field name="floor_number_format" colspan="5"/>
|
||||
<label name="room_number_format"/>
|
||||
<field name="room_number_format" colspan="5"/>
|
||||
<label name="post_box_format"/>
|
||||
<field name="post_box_format" colspan="5"/>
|
||||
<label name="private_bag_format"/>
|
||||
<field name="private_bag_format" colspan="5"/>
|
||||
<label name="post_office_format"/>
|
||||
<field name="post_office_format" colspan="5"/>
|
||||
</form>
|
||||
5
modules/party/view/address_format_list.xml
Normal file
5
modules/party/view/address_format_list.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0"?>
|
||||
<tree>
|
||||
<field name="country_code"/>
|
||||
<field name="language_code"/>
|
||||
</tree>
|
||||
11
modules/party/view/address_subdivision_type_form.xml
Normal file
11
modules/party/view/address_subdivision_type_form.xml
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0"?>
|
||||
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
this repository contains the full copyright notices and license terms. -->
|
||||
<form>
|
||||
<label name="country_code"/>
|
||||
<field name="country_code"/>
|
||||
<label name="active"/>
|
||||
<field name="active"/>
|
||||
<separator name="types" colspan="4"/>
|
||||
<field name="types" colspan="4"/>
|
||||
</form>
|
||||
7
modules/party/view/address_subdivision_type_list.xml
Normal file
7
modules/party/view/address_subdivision_type_list.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0"?>
|
||||
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
this repository contains the full copyright notices and license terms. -->
|
||||
<tree>
|
||||
<field name="country_code"/>
|
||||
<field name="types" expand="1"/>
|
||||
</tree>
|
||||
12
modules/party/view/address_tree.xml
Normal file
12
modules/party/view/address_tree.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0"?>
|
||||
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
this repository contains the full copyright notices and license terms. -->
|
||||
<tree>
|
||||
<field name="party" expand="1"/>
|
||||
<field name="party_name" optional="0"/>
|
||||
<field name="street_single_line" expand="2"/>
|
||||
<field name="postal_code"/>
|
||||
<field name="city"/>
|
||||
<field name="country" optional="0"/>
|
||||
<field name="subdivision" optional="0"/>
|
||||
</tree>
|
||||
12
modules/party/view/address_tree_sequence.xml
Normal file
12
modules/party/view/address_tree_sequence.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0"?>
|
||||
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
this repository contains the full copyright notices and license terms. -->
|
||||
<tree sequence="sequence">
|
||||
<field name="party" expand="1"/>
|
||||
<field name="party_name" optional="0"/>
|
||||
<field name="street_single_line" expand="2"/>
|
||||
<field name="postal_code"/>
|
||||
<field name="city"/>
|
||||
<field name="country" optional="0"/>
|
||||
<field name="subdivision" optional="0"/>
|
||||
</tree>
|
||||
12
modules/party/view/category_form.xml
Normal file
12
modules/party/view/category_form.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0"?>
|
||||
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
this repository contains the full copyright notices and license terms. -->
|
||||
<form>
|
||||
<label name="name"/>
|
||||
<field name="name"/>
|
||||
<label name="active"/>
|
||||
<field name="active"/>
|
||||
<label name="parent"/>
|
||||
<field name="parent"/>
|
||||
<field name="childs" colspan="4"/>
|
||||
</form>
|
||||
6
modules/party/view/category_list.xml
Normal file
6
modules/party/view/category_list.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0"?>
|
||||
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
this repository contains the full copyright notices and license terms. -->
|
||||
<tree>
|
||||
<field name="rec_name" expand="1"/>
|
||||
</tree>
|
||||
6
modules/party/view/category_tree.xml
Normal file
6
modules/party/view/category_tree.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0"?>
|
||||
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
this repository contains the full copyright notices and license terms. -->
|
||||
<tree keyword_open="1">
|
||||
<field name="name" expand="1"/>
|
||||
</tree>
|
||||
7
modules/party/view/check_vies_result.xml
Normal file
7
modules/party/view/check_vies_result.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0"?>
|
||||
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
this repository contains the full copyright notices and license terms. -->
|
||||
<form col="1">
|
||||
<field name="parties_succeed"/>
|
||||
<field name="parties_failed"/>
|
||||
</form>
|
||||
12
modules/party/view/configuration_form.xml
Normal file
12
modules/party/view/configuration_form.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0"?>
|
||||
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
this repository contains the full copyright notices and license terms. -->
|
||||
<form>
|
||||
<label name="party_sequence"/>
|
||||
<field name="party_sequence"/>
|
||||
<label name="party_lang"/>
|
||||
<field name="party_lang" widget="selection"/>
|
||||
|
||||
<separator name="identifier_types" colspan="4"/>
|
||||
<field name="identifier_types" colspan="4" height="200"/>
|
||||
</form>
|
||||
36
modules/party/view/contact_mechanism_form.xml
Normal file
36
modules/party/view/contact_mechanism_form.xml
Normal file
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0"?>
|
||||
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
this repository contains the full copyright notices and license terms. -->
|
||||
<form>
|
||||
<label name="party"/>
|
||||
<field name="party"/>
|
||||
<group col="-1" colspan="2" id="checkboxes">
|
||||
<label name="active"/>
|
||||
<field name="active"
|
||||
xexpand="0" width="25"/>
|
||||
<!-- Add here some checkboxes ! -->
|
||||
<label name="sequence"/>
|
||||
<field name="sequence"/>
|
||||
</group>
|
||||
<label name="type"/>
|
||||
<field name="type"/>
|
||||
<group col="2" colspan="2" id="value">
|
||||
<label name="other_value"/>
|
||||
<field name="other_value"/>
|
||||
<label name="website"/>
|
||||
<field name="website" widget="url"/>
|
||||
<label name="email"/>
|
||||
<field name="email" widget="email"/>
|
||||
<label name="skype"/>
|
||||
<field name="skype" widget="callto"/>
|
||||
<label name="sip"/>
|
||||
<field name="sip" widget="sip"/>
|
||||
</group>
|
||||
<label name="name"/>
|
||||
<field name="name"/>
|
||||
<label name="language"/>
|
||||
<field name="language" widget="selection"/>
|
||||
|
||||
<separator name="comment" colspan="6"/>
|
||||
<field name="comment" colspan="6"/>
|
||||
</form>
|
||||
11
modules/party/view/contact_mechanism_tree.xml
Normal file
11
modules/party/view/contact_mechanism_tree.xml
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0"?>
|
||||
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
this repository contains the full copyright notices and license terms. -->
|
||||
<tree>
|
||||
<field name="type"/>
|
||||
<field name="value" expand="1"/>
|
||||
<field name="name" expand="1" optional="0"/>
|
||||
<field name="party" expand="2" optional="0"/>
|
||||
<field name="address" expand="1" optional="0"/>
|
||||
<field name="language" widget="selection" optional="1"/>
|
||||
</tree>
|
||||
12
modules/party/view/contact_mechanism_tree_sequence.xml
Normal file
12
modules/party/view/contact_mechanism_tree_sequence.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0"?>
|
||||
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
this repository contains the full copyright notices and license terms. -->
|
||||
<tree sequence="sequence" editable="1">
|
||||
<field name="type"/>
|
||||
<field name="value" expand="1"/>
|
||||
<field name="name" expand="1" optional="0"/>
|
||||
<field name="party" expand="2" optional="0"/>
|
||||
<field name="address" expand="1" optional="0"/>
|
||||
<field name="language" widget="selection" optional="1"/>
|
||||
<field name="url" widget="url"/>
|
||||
</tree>
|
||||
9
modules/party/view/email_template_form.xml
Normal file
9
modules/party/view/email_template_form.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0"?>
|
||||
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
this repository contains the full copyright notices and license terms. -->
|
||||
<data>
|
||||
<xpath expr="//label[@name='subject']" position="before">
|
||||
<label name="contact_mechanism"/>
|
||||
<field name="contact_mechanism" colspan="3"/>
|
||||
</xpath>
|
||||
</data>
|
||||
7
modules/party/view/erase_ask_form.xml
Normal file
7
modules/party/view/erase_ask_form.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0"?>
|
||||
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
this repository contains the full copyright notices and license terms. -->
|
||||
<form>
|
||||
<label name="party"/>
|
||||
<field name="party"/>
|
||||
</form>
|
||||
20
modules/party/view/identifier_form.xml
Normal file
20
modules/party/view/identifier_form.xml
Normal file
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0"?>
|
||||
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
this repository contains the full copyright notices and license terms. -->
|
||||
<form cursor="type">
|
||||
<label name="party"/>
|
||||
<field name="party"/>
|
||||
<group colspan="2" col="-1" id="checkboxes">
|
||||
<label name="active"/>
|
||||
<field name="active"/>
|
||||
<label name="sequence"/>
|
||||
<field name="sequence"/>
|
||||
</group>
|
||||
<label name="type"/>
|
||||
<field name="type"/>
|
||||
<label name="code"/>
|
||||
<field name="code"/>
|
||||
|
||||
<label name="address"/>
|
||||
<field name="address"/>
|
||||
</form>
|
||||
9
modules/party/view/identifier_list.xml
Normal file
9
modules/party/view/identifier_list.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0"?>
|
||||
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
this repository contains the full copyright notices and license terms. -->
|
||||
<tree>
|
||||
<field name="party" expand="1"/>
|
||||
<field name="address" expand="1"/>
|
||||
<field name="type" expand="1"/>
|
||||
<field name="code" expand="2"/>
|
||||
</tree>
|
||||
9
modules/party/view/identifier_list_sequence.xml
Normal file
9
modules/party/view/identifier_list_sequence.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0"?>
|
||||
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
this repository contains the full copyright notices and license terms. -->
|
||||
<tree sequence="sequence">
|
||||
<field name="party" expand="1"/>
|
||||
<field name="address" expand="1"/>
|
||||
<field name="type" expand="1"/>
|
||||
<field name="code" expand="2"/>
|
||||
</tree>
|
||||
37
modules/party/view/party_form.xml
Normal file
37
modules/party/view/party_form.xml
Normal file
@@ -0,0 +1,37 @@
|
||||
<?xml version="1.0"?>
|
||||
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
this repository contains the full copyright notices and license terms. -->
|
||||
<form col="6">
|
||||
<group col="6" colspan="5" id="header" yalign="0">
|
||||
<label name="name"/>
|
||||
<field name="name" xexpand="1"/>
|
||||
<label name="code"/>
|
||||
<field name="code"/>
|
||||
<group col="-1" colspan="2" id="checkboxes" xexpand="0">
|
||||
<label name="active"/>
|
||||
<field name="active" xexpand="0" width="25"/>
|
||||
<!-- Add here some checkboxes ! -->
|
||||
</group>
|
||||
<label name="replaced_by"/>
|
||||
<field name="replaced_by"/>
|
||||
<newline/>
|
||||
<label name="lang"/>
|
||||
<field name="lang" widget="selection"/>
|
||||
</group>
|
||||
<notebook colspan="6">
|
||||
<page string="General" id="general">
|
||||
<field name="addresses" mode="form,tree" colspan="4"
|
||||
view_ids="party.address_view_form_simple,party.address_view_tree_sequence"/>
|
||||
<field name="contact_mechanisms" colspan="2"
|
||||
view_ids="party.contact_mechanism_view_tree_sequence"/>
|
||||
<field name="categories" colspan="2"
|
||||
view_ids="party.category_view_list"/>
|
||||
</page>
|
||||
<page name="identifiers">
|
||||
<field name="identifiers" colspan="4" pre_validate="1"
|
||||
view_ids="party.identifier_list_sequence"/>
|
||||
</page>
|
||||
</notebook>
|
||||
<group id="links" col="-1" colspan="6">
|
||||
</group>
|
||||
</form>
|
||||
9
modules/party/view/party_tree.xml
Normal file
9
modules/party/view/party_tree.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0"?>
|
||||
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
this repository contains the full copyright notices and license terms. -->
|
||||
<tree>
|
||||
<field name="code"/>
|
||||
<field name="name" expand="1"/>
|
||||
<field name="lang" optional="1"/>
|
||||
<field name="tax_identifier" optional="0"/>
|
||||
</tree>
|
||||
9
modules/party/view/replace_ask_form.xml
Normal file
9
modules/party/view/replace_ask_form.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0"?>
|
||||
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
this repository contains the full copyright notices and license terms. -->
|
||||
<form>
|
||||
<label name="source" string="Party"/>
|
||||
<field name="source"/>
|
||||
<label name="destination" string="Replaced By"/>
|
||||
<field name="destination"/>
|
||||
</form>
|
||||
Reference in New Issue
Block a user