diff --git a/tryton/modules/country/__init__.py b/tryton/modules/country/__init__.py
index 80dff9d0ce..dcc3549d5f 100644
--- a/tryton/modules/country/__init__.py
+++ b/tryton/modules/country/__init__.py
@@ -8,6 +8,9 @@ def register():
# Prevent to import backend when importing scripts
from . import country
Pool.register(
+ country.Organization,
+ country.OrganizationMember,
+ country.Region,
country.Country,
country.Subdivision,
country.PostalCode,
diff --git a/tryton/modules/country/country.py b/tryton/modules/country/country.py
index 735606d5a6..1d681b5184 100644
--- a/tryton/modules/country/country.py
+++ b/tryton/modules/country/country.py
@@ -1,26 +1,173 @@
# 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 datetime as dt
+
+from sql import Literal
+from sql.conditionals import Coalesce
+
from trytond import backend
-from trytond.model import DeactivableMixin, ModelSQL, ModelView, fields
+from trytond.model import DeactivableMixin, ModelSQL, ModelView, fields, tree
from trytond.pool import Pool
-from trytond.pyson import Eval
-from trytond.tools import lstrip_wildcard
+from trytond.pyson import Eval, If
+from trytond.tools import is_full_text, lstrip_wildcard
from trytond.transaction import Transaction
+class Organization(ModelSQL, ModelView):
+ "Organization"
+ __name__ = 'country.organization'
+
+ name = fields.Char("Name", required=True, translate=True)
+ code = fields.Char("Code")
+ members = fields.One2Many(
+ 'country.organization.member', 'organization', "Members",
+ filter=[
+ ('active', 'in', [True, False]),
+ ])
+ countries = fields.Many2Many(
+ 'country.organization.member', 'organization', 'country', "Countries",
+ readonly=True)
+
+
+class OrganizationMember(ModelSQL, ModelView):
+ "Organization Member"
+ __name__ = 'country.organization.member'
+
+ organization = fields.Many2One(
+ 'country.organization', "Organization", required=True)
+ country = fields.Many2One(
+ 'country.country', "Country", required=True)
+ from_date = fields.Date(
+ "From Date",
+ domain=[
+ If(Eval('from_date') & Eval('to_date'),
+ ('from_date', '<=', Eval('to_date')),
+ ()),
+ ])
+ to_date = fields.Date(
+ "To Date",
+ domain=[
+ If(Eval('from_date') & Eval('to_date'),
+ ('to_date', '>=', Eval('from_date')),
+ ()),
+ ])
+ active = fields.Function(fields.Boolean("Active"), 'on_change_with_active')
+
+ @classmethod
+ def __setup__(cls):
+ super().__setup__()
+ cls._order.insert(0, ('country', None))
+ cls._order.insert(1, ('from_date', 'ASC NULLS FIRST'))
+ cls.__access__.add('organization')
+
+ @classmethod
+ def default_active(cls):
+ return True
+
+ @fields.depends('from_date', 'to_date')
+ def on_change_with_active(self, name=None):
+ pool = Pool()
+ Date = pool.get('ir.date')
+ context = Transaction().context
+ date = context.get('date', Date.today())
+
+ from_date = self.from_date or dt.date.min
+ to_date = self.to_date or dt.date.max
+ return from_date <= date <= to_date
+
+ @classmethod
+ def domain_active(cls, domain, tables):
+ pool = Pool()
+ Date = pool.get('ir.date')
+ context = Transaction().context
+ table, _ = tables[None]
+ _, operator, operand = domain
+ date = context.get('date', Date.today())
+
+ from_date = Coalesce(table.from_date, dt.date.min)
+ to_date = Coalesce(table.to_date, dt.date.max)
+
+ expression = (from_date <= date) & (to_date >= date)
+
+ if operator in {'=', '!='}:
+ if (operator == '=') != operand:
+ expression = ~expression
+ elif operator in {'in', 'not in'}:
+ if True in operand and False not in operand:
+ pass
+ elif False in operand and True not in operand:
+ expression = ~expression
+ else:
+ expression = Literal(True)
+ else:
+ expression = Literal(True)
+ return expression
+
+
+class Region(tree(), ModelSQL, ModelView):
+ "Region"
+ __name__ = 'country.region'
+
+ name = fields.Char("Name", required=True, translate=True)
+ code_numeric = fields.Char(
+ "Numeric Code", size=3,
+ help="UN M49 region code.")
+ parent = fields.Many2One('country.region', "Parent")
+ subregions = fields.One2Many('country.region', 'parent', "Subregions")
+ countries = fields.One2Many(
+ 'country.country', 'region', "Countries",
+ add_remove=[
+ ('region', '=', None),
+ ])
+
+ @classmethod
+ def __setup__(cls):
+ super().__setup__()
+ cls._order.insert(0, ('name', 'ASC'))
+
+ @classmethod
+ def search_rec_name(cls, name, clause):
+ if clause[1].startswith('!') or clause[1].startswith('not '):
+ bool_op = 'AND'
+ else:
+ bool_op = 'OR'
+ code_value = clause[2]
+ if clause[1].endswith('like'):
+ code_value = lstrip_wildcard(clause[2])
+ return [bool_op,
+ ('name',) + tuple(clause[1:]),
+ ('code_numeric', clause[1], code_value) + tuple(clause[3:]),
+ ]
+
+
class Country(DeactivableMixin, ModelSQL, ModelView):
'Country'
__name__ = 'country.country'
- name = fields.Char('Name', required=True, translate=True,
- help="The main identifier of the country.", select=True)
- code = fields.Char('Code', size=2, select=True,
- help="The 2 chars ISO country code.")
- code3 = fields.Char('3-letters Code', size=3, select=True,
+ name = fields.Char(
+ "Name", required=True, translate=True,
+ help="The main identifier of the country.")
+ code = fields.Char(
+ "Code", size=2,select=True,
+ help="The 2 chars ISO country code.")
+ code3 = fields.Char(
+ "3-letters Code", size=3, select=True,
help="The 3 chars ISO country code.")
- code_numeric = fields.Char('Numeric Code', select=True,
+ code_numeric = fields.Char(
+ "Numeric Code", select=True,
help="The ISO numeric country code.")
+ flag = fields.Function(fields.Char("Flag"), 'on_change_with_flag')
+ region = fields.Many2One(
+ 'country.region', "Region", ondelete='SET NULL')
subdivisions = fields.One2Many('country.subdivision',
'country', 'Subdivisions')
+ members = fields.One2Many(
+ 'country.organization.member', 'country', "Members",
+ filter=[
+ ('active', 'in', [True, False]),
+ ])
+ organizations = fields.Many2Many(
+ 'country.organization.member', 'country', 'organization',
+ "Organizations", readonly=True)
@classmethod
def __setup__(cls):
@@ -36,35 +183,53 @@ class Country(DeactivableMixin, ModelSQL, ModelView):
super(Country, cls).__register__(module_name)
- table = cls.__table_handler__(module_name)
-
- # Migration from 3.4: drop unique constraints from name and code
- table.drop_constraint('name_uniq')
- table.drop_constraint('code_uniq')
-
- # Migration from 3.8: remove required on code
- table.not_null_action('code', 'remove')
-
# Migration from 5.2: remove country data
cursor.execute(*data.delete(where=(data.module == 'country')
& (data.model == cls.__name__)))
+ @fields.depends('code')
+ def on_change_with_flag(self, name=None):
+ if self.code:
+ return ''.join(map(chr, map(lambda c: 127397 + ord(c), self.code)))
+
+ def get_rec_name(self, name):
+ name = self.name
+ if self.flag:
+ name = ' '.join([self.flag, self.name])
+ return name
+
@classmethod
def search_rec_name(cls, name, clause):
- if clause[1].startswith('!') or clause[1].startswith('not '):
+ _, operator, operand, *extra = clause
+ if operator.startswith('!') or operator.startswith('not '):
bool_op = 'AND'
else:
bool_op = 'OR'
- code_value = clause[2]
- if clause[1].endswith('like'):
- code_value = lstrip_wildcard(clause[2])
+ code_value = operand
+ if operator.endswith('like') and is_full_text(operand):
+ code_value = lstrip_wildcard(operand)
return [bool_op,
- ('name',) + tuple(clause[1:]),
- ('code', clause[1], code_value) + tuple(clause[3:]),
- ('code3', clause[1], code_value) + tuple(clause[3:]),
- ('code_numeric', clause[1], code_value) + tuple(clause[3:]),
+ ('name', operator, operand, *extra),
+ ('code', operator, code_value, *extra),
+ ('code3', operator, code_value, *extra),
+ ('code_numeric', operator, code_value, *extra),
]
+ def is_member(self, organization, date=None):
+ """Return if the country is in the organization at the date
+ organization can be an XML id"""
+ pool = Pool()
+ Date = pool.get('ir.date')
+ ModelData = pool.get('ir.model.data')
+ Organization = pool.get('country.organization')
+ if date is None:
+ date = Date.today()
+ if isinstance(organization, str):
+ organization = ModelData.get_id(organization)
+ with Transaction().set_context(date=date):
+ organization = Organization(organization)
+ return self in organization.countries
+
@classmethod
def create(cls, vlist):
vlist = [x.copy() for x in vlist]
@@ -90,13 +255,16 @@ class Country(DeactivableMixin, ModelSQL, ModelView):
class Subdivision(DeactivableMixin, ModelSQL, ModelView):
"Subdivision"
__name__ = 'country.subdivision'
- country = fields.Many2One('country.country', 'Country',
- required=True, select=True,
+ country = fields.Many2One(
+ 'country.country', "Country", required=True,
help="The country where this subdivision is.")
- name = fields.Char('Name', required=True, select=True, translate=True,
+ name = fields.Char(
+ "Name", required=True, translate=True,
help="The main identifier of the subdivision.")
- code = fields.Char('Code', required=True, select=True,
+ code = fields.Char(
+ "Code",
help="The ISO code of the subdivision.")
+ flag = fields.Function(fields.Char("Flag"), 'on_change_with_flag')
type = fields.Selection([
(None, ""),
('administration', 'Administration'),
@@ -270,18 +438,29 @@ class Subdivision(DeactivableMixin, ModelSQL, ModelView):
# Migration from 6.2: remove type required
table_h.not_null_action('type', action='remove')
+ # Migration from 6.4: remove required on code
+ table_h.not_null_action('code', action='remove')
+
+ @fields.depends('code')
+ def on_change_with_flag(self, name=None):
+ if self.code:
+ return '🏴' + ''.join(map(chr, map(
+ lambda c: 917504 + ord(c),
+ self.code.replace('-', '').lower()))) + '\U000e007f'
+
@classmethod
def search_rec_name(cls, name, clause):
- if clause[1].startswith('!') or clause[1].startswith('not '):
+ _, operator, operand, *extra = clause
+ if operator.startswith('!') or operator.startswith('not '):
bool_op = 'AND'
else:
bool_op = 'OR'
- code_value = clause[2]
- if clause[1].endswith('like'):
- code_value = lstrip_wildcard(clause[2])
+ code_value = operand
+ if operator.endswith('like') and is_full_text(operand):
+ code_value = lstrip_wildcard(operand)
return [bool_op,
- ('name',) + tuple(clause[1:]),
- ('code', clause[1], code_value) + tuple(clause[3:]),
+ ('name', operator, operand, *extra),
+ ('code', operator, code_value, *extra),
]
@classmethod
@@ -307,11 +486,11 @@ class Subdivision(DeactivableMixin, ModelSQL, ModelView):
class PostalCode(ModelSQL, ModelView):
"Postal Code"
__name__ = 'country.postal_code'
- country = fields.Many2One('country.country', 'Country', required=True,
- select=True, ondelete='CASCADE',
+ country = fields.Many2One(
+ 'country.country', "Country", required=True, ondelete='CASCADE',
help="The country that contains the postal code.")
- subdivision = fields.Many2One('country.subdivision', 'Subdivision',
- select=True, ondelete='CASCADE',
+ subdivision = fields.Many2One(
+ 'country.subdivision', "Subdivision", ondelete='CASCADE',
domain=[('country', '=', Eval('country', -1))],
help="The subdivision where the postal code is.")
postal_code = fields.Char('Postal Code')
@@ -341,14 +520,15 @@ class PostalCode(ModelSQL, ModelView):
@classmethod
def search_rec_name(cls, name, clause):
- if clause[1].startswith('!') or clause[1].startswith('not '):
+ _, operator, operand, *extra = clause
+ if operator.startswith('!') or operator.startswith('not '):
bool_op = 'AND'
else:
bool_op = 'OR'
- code_value = clause[2]
- if clause[1].endswith('like'):
- code_value = lstrip_wildcard(clause[2])
+ code_value = operand
+ if operator.endswith('like') and is_full_text(operand):
+ code_value = lstrip_wildcard(operand)
return [bool_op,
- ('postal_code', clause[1], code_value) + tuple(clause[3:]),
- ('city',) + tuple(clause[1:]),
+ ('postal_code', operator, code_value, *extra),
+ ('city', operator, operand, *extra),
]
diff --git a/tryton/modules/country/country.xml b/tryton/modules/country/country.xml
index b41f30de04..97b5906142 100644
--- a/tryton/modules/country/country.xml
+++ b/tryton/modules/country/country.xml
@@ -8,6 +8,156 @@ this repository contains the full copyright notices and license terms. -->
icons/tryton-country.svg
+
+
+
+
+ country.organization
+ form
+ organization_form
+
+
+
+ country.organization
+ tree
+ organization_list
+
+
+
+ Organizations
+ country.organization
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ country.organization.member
+ form
+ organization_member_form
+
+
+
+ country.organization.member
+ tree
+ organization_member_list
+
+
+
+ country.region
+ form
+ region_form
+
+
+
+ country.region
+ tree
+
+ region_list
+
+
+
+ country.region
+ tree
+
+ subregions
+ region_tree
+
+
+
+ Areas
+ country.region
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Regions
+ country.region
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
country.country
form
@@ -33,15 +183,39 @@ this repository contains the full copyright notices and license terms. -->
-