first commit

This commit is contained in:
root
2026-03-14 09:42:12 +00:00
commit 0adbd20c2c
10991 changed files with 1646955 additions and 0 deletions

View 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.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

679
modules/party/address.py Normal file
View 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

File diff suppressed because it is too large Load Diff

31
modules/party/category.py Normal file
View 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'))

View 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>

View 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

View 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>

View 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")

View 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
View 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())),
})

View 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

View 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
View 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
View 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
View 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="&apos;Liberation Serif&apos;" style:font-adornments="Bold" style:font-family-generic="roman" style:font-pitch="variable"/>
<style:font-face style:name="Liberation Serif" svg:font-family="&apos;Liberation Serif&apos;" style:font-adornments="Regular" style:font-family-generic="roman" style:font-pitch="variable"/>
<style:font-face style:name="Thorndale AMT" svg:font-family="&apos;Thorndale AMT&apos;" style:font-family-generic="roman" style:font-pitch="variable"/>
<style:font-face style:name="Liberation Sans" svg:font-family="&apos;Liberation Sans&apos;" style:font-adornments="Regular" style:font-family-generic="swiss" style:font-pitch="variable"/>
<style:font-face style:name="Andale Sans UI" svg:font-family="&apos;Andale Sans UI&apos;" style:font-family-generic="system" style:font-pitch="variable"/>
<style:font-face style:name="DejaVu Sans" svg:font-family="&apos;DejaVu Sans&apos;" 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="&apos;Liberation Sans&apos;" 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="&apos;Liberation Serif&apos;" 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="&apos;DejaVu Sans&apos;" 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="&apos;DejaVu Sans&apos;" 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="&apos;Liberation Sans&apos;" 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="&apos;Liberation Serif&apos;" 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">&lt;for each=&quot;party in records&quot;&gt;</text:placeholder></text:p>
<text:p text:style-name="P4"><text:placeholder text:placeholder-type="text">&lt;party.full_name&gt;</text:placeholder></text:p>
<text:p text:style-name="P1"><text:placeholder text:placeholder-type="text">&lt;if test=&quot;party.addresses&quot;&gt;</text:placeholder></text:p>
<text:p text:style-name="P1"><text:placeholder text:placeholder-type="text">&lt;for each=&quot;line in party.addresses[0].full_address.split(&apos;\n&apos;)&quot;&gt;</text:placeholder></text:p>
<text:p text:style-name="P2"><text:placeholder text:placeholder-type="text">&lt;line&gt;</text:placeholder></text:p>
<text:p text:style-name="P2"><text:placeholder text:placeholder-type="text">&lt;/for&gt;</text:placeholder></text:p>
<text:p text:style-name="P2"><text:placeholder text:placeholder-type="text">&lt;for each=&quot;i in range(5 -len(party.addresses[0].full_address.split(&apos;\n&apos;)))&quot;&gt;</text:placeholder></text:p>
<text:p text:style-name="P2"/>
<text:p text:style-name="P2"><text:placeholder text:placeholder-type="text">&lt;/for&gt;</text:placeholder></text:p>
<text:p text:style-name="P2"><text:placeholder text:placeholder-type="text">&lt;/if&gt;</text:placeholder></text:p>
<text:p text:style-name="P2"><text:placeholder text:placeholder-type="text">&lt;if test=&quot;not party.addresses&quot;&gt;</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">&lt;/if&gt;</text:placeholder></text:p>
<text:p text:style-name="P2"><text:placeholder text:placeholder-type="text">&lt;/for&gt;</text:placeholder></text:p>
</text:section>
</office:text>
</office:body>
</office:document>

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

File diff suppressed because it is too large Load Diff

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

File diff suppressed because it is too large Load Diff

1563
modules/party/locale/es.po Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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

File diff suppressed because it is too large Load Diff

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

File diff suppressed because it is too large Load Diff

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

File diff suppressed because it is too large Load Diff

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

File diff suppressed because it is too large Load Diff

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

File diff suppressed because it is too large Load Diff

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

File diff suppressed because it is too large Load Diff

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

File diff suppressed because it is too large Load Diff

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

File diff suppressed because it is too large Load Diff

1617
modules/party/locale/uk.po Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

58
modules/party/message.xml Normal file
View 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

File diff suppressed because it is too large Load Diff

190
modules/party/party.xml Normal file
View 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>

View 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]

View File

@@ -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

View 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()
[]

View File

@@ -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

View 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'

View 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)

View 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

View 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
View 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

View 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>

View 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>

View 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>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0"?>
<tree>
<field name="country_code"/>
<field name="language_code"/>
</tree>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>