diff --git a/base_location_nuts/README.rst b/base_location_nuts/README.rst new file mode 100644 index 000000000..18c380523 --- /dev/null +++ b/base_location_nuts/README.rst @@ -0,0 +1,91 @@ +.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg + :alt: License: AGPL-3 + +============ +NUTS Regions +============ + +This module allows to import NUTS locations. + +Creates two new fields in Partner object: + +* Region (res.partner.region): Classification over state, automatically + calculated when state is selected +* Substate (res.partner.substate): Classification above state, user must select + one from available for selected state + + +Installation +============ + +You need to install another addon (one for each country) in order to use +these NUTS, for example: + +* l10n_es_location_nuts : + * Spanish Provinces (NUTS level 4) as Partner State + * Spanish Autonomous communities (NUTS level 3) as Partner Substate + * Spanish Regions (NUTS level 2) as Partner Region +* l10n_de_location_nuts : + * German states (NUTS level 2) as Partner State + * German districts (NUTS level 3) as Partner Substate + * German regions (NUTS level 4) as Partner Region + + +Configuration +============= + +After installation, you must click at import wizard to populate NUTS items +in Odoo database in: +Sales > Configuration > Address Book > Import NUTS 2013 + +This wizard will download from Europe RAMON service the metadata to +build NUTS in Odoo. Each localization addon (l10n_es_location_nuts, +l10n_de_location_nuts, ...) will inherit this wizard and +relate each NUTS item with states. So if you install a new localization addon +you must re-build NUTS clicking this wizard again. + + +Usage +===== + +Only Administrator can manage NUTS list (it is not neccesary because +it is an European convention) but any registered user can read them, +in order to allow to assign them to partner object. + +.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas + :alt: Try me on Runbot + :target: https://runbot.odoo-community.org/runbot/134/{branch} + + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed feedback +`here `_. + + +Credits +======= + +Contributors +------------ + +* Rafael Blasco +* Antonio Espinosa + +Maintainer +---------- + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +This module is maintained by the OCA. + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +To contribute to this module, please visit http://odoo-community.org. \ No newline at end of file diff --git a/base_location_nuts/__init__.py b/base_location_nuts/__init__.py new file mode 100644 index 000000000..7333c0440 --- /dev/null +++ b/base_location_nuts/__init__.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +############################################################################## +# For copyright and license notices, see __openerp__.py file in root directory +############################################################################## + +from . import models +from . import wizard diff --git a/base_location_nuts/__openerp__.py b/base_location_nuts/__openerp__.py new file mode 100644 index 000000000..a1aad7791 --- /dev/null +++ b/base_location_nuts/__openerp__.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +# Python source code encoding : https://www.python.org/dev/peps/pep-0263/ +############################################################################## +# +# OpenERP, Odoo Source Management Solution +# Copyright (c) 2015 Antiun Ingeniería S.L. (http://www.antiun.com) +# Antonio Espinosa +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +{ + 'name': 'NUTS Regions', + 'category': 'Localisation/Europe', + 'version': '8.0.1.0.0', + 'depends': [ + 'base', + ], + 'data': [ + 'views/res_partner_nuts_view.xml', + 'views/res_partner_view.xml', + 'wizard/nuts_import_view.xml', + 'security/ir.model.access.csv', + ], + 'author': 'Antiun Ingeniería S.L., ' + 'Odoo Community Association (OCA)', + 'website': 'http://www.antiun.com', + 'license': 'AGPL-3', + 'installable': True, +} diff --git a/base_location_nuts/i18n/es.po b/base_location_nuts/i18n/es.po new file mode 100644 index 000000000..1bbee771d --- /dev/null +++ b/base_location_nuts/i18n/es.po @@ -0,0 +1,220 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * base_location_nuts +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-06-11 15:26+0000\n" +"PO-Revision-Date: 2015-06-11 15:26+0000\n" +"Last-Translator: <>\n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: base_location_nuts +#: view:nuts.import:base_location_nuts.nuts_import_form +msgid "Cancel" +msgstr "Cancelar" + +#. module: base_location_nuts +#: field:res.partner.nuts,children:0 +msgid "Children" +msgstr "Hijos" + +#. module: base_location_nuts +#: field:res.partner.nuts,code:0 +msgid "Code" +msgstr "Código" + +#. module: base_location_nuts +#: view:res.partner.nuts:base_location_nuts.view_res_partner_nuts_filter +#: field:res.partner.nuts,country_id:0 +msgid "Country" +msgstr "País" + +#. module: base_location_nuts +#: field:nuts.import,create_uid:0 +#: field:res.partner.nuts,create_uid:0 +msgid "Created by" +msgstr "Creado por" + +#. module: base_location_nuts +#: field:nuts.import,create_date:0 +#: field:res.partner.nuts,create_date:0 +msgid "Created on" +msgstr "Creado en" + +#. module: base_location_nuts +#: code:addons/base_location_nuts/wizard/nuts_import.py:149 +#, python-format +msgid "Downloaded file is not a valid XML file" +msgstr "El fichero descargado no es un fichero XML válido" + +#. module: base_location_nuts +#: code:addons/base_location_nuts/wizard/nuts_import.py:141 +#, python-format +msgid "Got an error %d when trying to download the file %s." +msgstr "Error %d al intentar descargar el fichero %s." + +#. module: base_location_nuts +#: code:addons/base_location_nuts/wizard/nuts_import.py:137 +#, python-format +msgid "Got an error when trying to download the file: %s." +msgstr "Error al intentar descargar el fichero: %s." + +#. module: base_location_nuts +#: view:res.partner.nuts:base_location_nuts.view_res_partner_nuts_filter +msgid "Group By" +msgstr "Agrupar por" + +#. module: base_location_nuts +#: field:nuts.import,id:0 +#: field:res.partner.nuts,id:0 +msgid "ID" +msgstr "ID" + +#. module: base_location_nuts +#: view:nuts.import:base_location_nuts.nuts_import_form +msgid "Import" +msgstr "Importar" + +#. module: base_location_nuts +#: model:ir.ui.menu,name:base_location_nuts.nuts_import_menu +msgid "Import NUTS 2013" +msgstr "Importar NUTS 2013" + +#. module: base_location_nuts +#: model:ir.actions.act_window,name:base_location_nuts.nuts_import_action +#: view:nuts.import:base_location_nuts.nuts_import_form +msgid "Import NUTS 2013 from RAMON" +msgstr "Importar NUTS 2013 desde RAMON" + +#. module: base_location_nuts +#: model:ir.model,name:base_location_nuts.model_nuts_import +msgid "Import NUTS items from European RAMON service" +msgstr "Importar regiones NUTS desde el servicio europeo RAMON" + +#. module: base_location_nuts +#: field:nuts.import,write_uid:0 +#: field:res.partner.nuts,write_uid:0 +msgid "Last Updated by" +msgstr "Última actualización por" + +#. module: base_location_nuts +#: field:nuts.import,write_date:0 +#: field:res.partner.nuts,write_date:0 +msgid "Last Updated on" +msgstr "Última actualización en" + +#. module: base_location_nuts +#: view:res.partner.nuts:base_location_nuts.view_res_partner_nuts_filter +#: field:res.partner.nuts,level:0 +msgid "Level" +msgstr "Nivel" + +#. module: base_location_nuts +#: model:ir.model,name:base_location_nuts.model_res_partner_nuts +#: view:res.partner.nuts:base_location_nuts.res_partner_nuts_form +msgid "NUTS Item" +msgstr "Región NUTS" + +#. module: base_location_nuts +#: model:ir.actions.act_window,name:base_location_nuts.res_partner_nuts_action +#: model:ir.ui.menu,name:base_location_nuts.res_partner_nuts_menu +#: view:res.partner.nuts:base_location_nuts.res_partner_nuts_tree +msgid "NUTS Items" +msgstr "Regiones NUTS" + +#. module: base_location_nuts +#: field:res.partner.nuts,name:0 +msgid "Name" +msgstr "Nombre" + +#. module: base_location_nuts +#: field:res.partner.nuts,parent_left:0 +msgid "Parent Left" +msgstr "Padre izquierda" + +#. module: base_location_nuts +#: field:res.partner.nuts,parent_right:0 +msgid "Parent Right" +msgstr "Padre derecha" + +#. module: base_location_nuts +#: field:res.partner.nuts,parent_id:0 +msgid "Parent id" +msgstr "ID del padre" + +#. module: base_location_nuts +#: model:ir.model,name:base_location_nuts.model_res_partner +msgid "Partner" +msgstr "Empresa" + +#. module: base_location_nuts +#: code:addons/base_location_nuts/models/res_partner.py:53 +#: view:res.partner:base_location_nuts.view_res_partner_filter_nuts +#: field:res.partner,region:0 +#, python-format +msgid "Region" +msgstr "Región" + +#. module: base_location_nuts +#: view:res.partner:base_location_nuts.view_res_partner_filter_nuts +msgid "Salesperson" +msgstr "Comercial" + +#. module: base_location_nuts +#: view:res.partner.nuts:base_location_nuts.view_res_partner_nuts_filter +msgid "Search NUTS" +msgstr "Buscar NUTS" + +#. module: base_location_nuts +#: field:res.partner.nuts,state_id:0 +msgid "State" +msgstr "Provincia" + +#. module: base_location_nuts +#: code:addons/base_location_nuts/models/res_partner.py:54 +#: view:res.partner:base_location_nuts.view_res_partner_filter_nuts +#: field:res.partner,substate:0 +#, python-format +msgid "Substate" +msgstr "Estado" + +#. module: base_location_nuts +#: view:nuts.import:base_location_nuts.nuts_import_form +msgid "This wizard will download the lastest version of\n" +" NUTS 2013 from Europe RAMON metadata service.\n" +" Updating or creating new NUTS entries if not\n" +" found already in the system, and DELETING MISSING\n" +" ENTRIES from new downloaded file." +msgstr "Este asistente descargará la última version de\n" +" NUTS 2013 desde el servicio de matadatos europeo RAMON.\n" +" Actualizando o creando nuevas regiones NUTS si no\n" +" las encuentra en el sistemma, y BORRANDO LAS QUE NO ENCUENTRE\n" +" en el nuevo fichero descargado." + +#. module: base_location_nuts +#: code:addons/base_location_nuts/wizard/nuts_import.py:116 +#, python-format +msgid "Value not found for mandatory field %s" +msgstr "El valor no se ha encontrado para el campo obligatorio %s" + +#. module: base_location_nuts +#: model:ir.actions.todo,note:base_location_nuts.config_wizard_nuts +msgid "You can import NUTS from RAMON european service." +msgstr "Usted puede importar NUTS desde el servicion europeo RAMON." + +#. module: base_location_nuts +#: model:ir.actions.act_window,help:base_location_nuts.res_partner_nuts_action +msgid "You must click at import wizard to populate NUTS items\n" +" in Odoo database in:\n" +" Sales > Configuration > Address Book > Localization > Import NUTS 2013" +msgstr "Debes clicar en el asistente de importación para crear las\n" +" las regiones NUTS en la base de datos de Odoo, en el menú:\n" +" Ventas > Configuración > Libreta de direcciones > Localización > Importar NUTS 2013" + diff --git a/base_location_nuts/models/__init__.py b/base_location_nuts/models/__init__.py new file mode 100644 index 000000000..a88f82ad0 --- /dev/null +++ b/base_location_nuts/models/__init__.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +############################################################################## +# For copyright and license notices, see __openerp__.py file in root directory +############################################################################## + +from . import res_partner_nuts +from . import res_partner diff --git a/base_location_nuts/models/res_partner.py b/base_location_nuts/models/res_partner.py new file mode 100644 index 000000000..904580980 --- /dev/null +++ b/base_location_nuts/models/res_partner.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +############################################################################## +# For copyright and license notices, see __openerp__.py file in root directory +############################################################################## + +from openerp import models, fields, api +from openerp.tools.translate import _ +import collections + + +def dict_recursive_update(d, u): + for k, v in u.iteritems(): + if isinstance(v, collections.Mapping): + r = dict_recursive_update(d.get(k, {}), v) + d[k] = r + else: + d[k] = u[k] + return d + + +class ResPartner(models.Model): + _inherit = 'res.partner' + + region = fields.Many2one(comodel_name='res.partner.nuts', + string="Region") + substate = fields.Many2one(comodel_name='res.partner.nuts', + string="Substate") + lbl_region = fields.Char(compute='_labels_get') + lbl_substate = fields.Char(compute='_labels_get') + + @api.one + @api.depends('country_id') + def _labels_get(self): + self.lbl_region = _('Region') + self.lbl_substate = _('Substate') + + @api.multi + def onchange_state(self, state_id): + result = super(ResPartner, self).onchange_state(state_id) + if not state_id: + changes = { + 'domain': { + 'substate': [], + 'region': [], + }, + 'value': { + 'substate': False, + 'region': False, + } + } + dict_recursive_update(result, changes) + return result + + @api.onchange('substate', 'region') + def onchange_substate_or_region(self): + result = {'domain': {}} + if not self.substate: + result['domain']['substate'] = [] + if not self.region: + result['domain']['region'] = [] + return result diff --git a/base_location_nuts/models/res_partner_nuts.py b/base_location_nuts/models/res_partner_nuts.py new file mode 100644 index 000000000..f8e62f20b --- /dev/null +++ b/base_location_nuts/models/res_partner_nuts.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +############################################################################## +# For copyright and license notices, see __openerp__.py file in root directory +############################################################################## + +from openerp import models, fields + + +class ResPartnerNuts(models.Model): + _name = 'res.partner.nuts' + _order = "parent_left" + _parent_order = "name" + _parent_store = True + _description = "NUTS Item" + + # NUTS fields + level = fields.Integer(required=True) + code = fields.Char(required=True) + name = fields.Char(required=True, translate=True) + country_id = fields.Many2one(comodel_name='res.country', string="Country", + required=True) + state_id = fields.Many2one(comodel_name='res.country.state', + string='State') + # Parent hierarchy + parent_id = fields.Many2one(comodel_name='res.partner.nuts', + ondelete='restrict') + children = fields.One2many(comodel_name='res.partner.nuts', + inverse_name='parent_id') + parent_left = fields.Integer('Parent Left', select=True) + parent_right = fields.Integer('Parent Right', select=True) diff --git a/base_location_nuts/security/ir.model.access.csv b/base_location_nuts/security/ir.model.access.csv new file mode 100644 index 000000000..5d38ac660 --- /dev/null +++ b/base_location_nuts/security/ir.model.access.csv @@ -0,0 +1,2 @@ +"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink" +"access_res_partner_nuts_user","res_partner_nuts group_user","model_res_partner_nuts","base.group_user",1,0,0,0 diff --git a/base_location_nuts/static/description/icon.png b/base_location_nuts/static/description/icon.png new file mode 100644 index 000000000..b1044a4ba Binary files /dev/null and b/base_location_nuts/static/description/icon.png differ diff --git a/base_location_nuts/views/res_partner_nuts_view.xml b/base_location_nuts/views/res_partner_nuts_view.xml new file mode 100644 index 000000000..877edd243 --- /dev/null +++ b/base_location_nuts/views/res_partner_nuts_view.xml @@ -0,0 +1,76 @@ + + + + + + NUTS Items tree + res.partner.nuts + + + + + + + + + + + NUTS Items tree + res.partner.nuts + +
+ + + + + + + + + + + + +
+
+
+ + + NUTS Items + ir.actions.act_window + res.partner.nuts + form + tree,form + You must click at import wizard to populate NUTS items + in Odoo database in: + Sales > Configuration > Address Book > Localization > Import NUTS 2013 + + + + NUTS search filters + res.partner.nuts + + + + + + + + + + + + + + + +
+
\ No newline at end of file diff --git a/base_location_nuts/views/res_partner_view.xml b/base_location_nuts/views/res_partner_view.xml new file mode 100644 index 000000000..55192e41c --- /dev/null +++ b/base_location_nuts/views/res_partner_view.xml @@ -0,0 +1,78 @@ + + + + + + Partner form with NUTS + res.partner + + + +
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+ + + Partner search with NUTS + res.partner + + + + + + + + + + + + + +
+
\ No newline at end of file diff --git a/base_location_nuts/wizard/__init__.py b/base_location_nuts/wizard/__init__.py new file mode 100644 index 000000000..1f9d83bcf --- /dev/null +++ b/base_location_nuts/wizard/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +############################################################################## +# For copyright and license notices, see __openerp__.py file in root directory +############################################################################## + +from . import nuts_import diff --git a/base_location_nuts/wizard/nuts_import.py b/base_location_nuts/wizard/nuts_import.py new file mode 100644 index 000000000..e63f908fe --- /dev/null +++ b/base_location_nuts/wizard/nuts_import.py @@ -0,0 +1,205 @@ +# -*- coding: utf-8 -*- +############################################################################## +# For copyright and license notices, see __openerp__.py file in root directory +############################################################################## + +from openerp import models, api, _ +from openerp.exceptions import Warning +import requests +import re +import logging +from lxml import etree +from collections import OrderedDict + +from pprint import pformat + +logger = logging.getLogger(__name__) + + +class NutsImport(models.TransientModel): + _name = 'nuts.import' + _description = 'Import NUTS items from European RAMON service' + _parents = [False, False, False, False] + _countries = { + "BE": False, + "BG": False, + "CZ": False, + "DK": False, + "DE": False, + "EE": False, + "IE": False, + "GR": False, # EL + "ES": False, + "FR": False, + "HR": False, + "IT": False, + "CY": False, + "LV": False, + "LT": False, + "LU": False, + "HU": False, + "MT": False, + "NL": False, + "AT": False, + "PL": False, + "PT": False, + "RO": False, + "SI": False, + "SK": False, + "FI": False, + "SE": False, + "GB": False, # UK + } + _current_country = False + _map = OrderedDict([ + ('level', { + 'xpath': '', 'attrib': 'idLevel', + 'type': 'integer', 'required': True}), + ('code', { + 'xpath': './Label/LabelText[@language="ALL"]', + 'type': 'string', 'required': True}), + ('name', { + 'xpath': './Label/LabelText[@language="EN"]', + 'type': 'string', 'required': True}), + ]) + + def _check_node(self, node): + if node.get('id') and node.get('idLevel'): + return True + return False + + def _mapping(self, node): + item = {} + for k, v in self._map.iteritems(): + field_xpath = v.get('xpath', '') + field_attrib = v.get('attrib', False) + field_type = v.get('type', 'string') + field_required = v.get('required', False) + value = '' + if field_xpath: + n = node.find(field_xpath) + else: + n = node + if n is not None: + if field_attrib: + value = n.get(field_attrib, '') + else: + value = n.text + if field_type == 'integer': + try: + value = int(value) + except: + value = 0 + else: + logger.debug("xpath = '%s', not found" % field_xpath) + if field_required and not value: + raise Warning( + _('Value not found for mandatory field %s' % k)) + item[k] = value + return item + + def _download_nuts(self): + url_base = 'http://ec.europa.eu' + url_path = '/eurostat/ramon/nomenclatures/index.cfm' + url_params = { + 'TargetUrl': 'ACT_OTH_CLS_DLD', + 'StrNom': 'NUTS_2013', + 'StrFormat': 'XML', + 'StrLanguageCode': 'EN', + 'StrLayoutCode': 'HIERARCHIC' + } + url = url_base + url_path + '?' + url += '&'.join([k + '=' + v for k, v in url_params.iteritems()]) + logger.info('Starting to download %s' % url) + try: + res_request = requests.get(url) + except Exception, e: + raise Warning( + _('Got an error when trying to download the file: %s.') % + str(e)) + if res_request.status_code != requests.codes.ok: + raise Warning( + _('Got an error %d when trying to download the file %s.') + % (res_request.status_code, url)) + logger.info('Download successfully %d bytes' % + len(res_request.content)) + # Workaround XML: Remove all characters before GR (Greece) + # UK => GB (United Kingdom) + self._countries['EL'] = self._countries['GR'] + self._countries['UK'] = self._countries['GB'] + logger.info('_load_countries = %s' % pformat(self._countries)) + + @api.model + def state_mapping(self, data, node): + # Method to inherit and add state_id relation depending on country + level = data.get('level', 0) + code = data.get('code', '') + if level == 1: + self._current_country = self._countries[code] + return { + 'country_id': self._current_country.id, + } + + @api.model + def create_or_update_nuts(self, node): + if not self._check_node(node): + return False + + nuts_model = self.env['res.partner.nuts'] + data = self._mapping(node) + data.update(self.state_mapping(data, node)) + level = data.get('level', 0) + if level >= 2 and level <= 5: + data['parent_id'] = self._parents[level - 2] + nuts = nuts_model.search([('level', '=', data['level']), + ('code', '=', data['code'])]) + if nuts: + nuts.write(data) + else: + nuts = nuts_model.create(data) + if level >= 1 and level <= 4: + self._parents[level - 1] = nuts.id + return nuts + + @api.one + def run_import(self): + nuts_model = self.env['res.partner.nuts'].\ + with_context(defer_parent_store_computation=True) + self._load_countries() + # All current NUTS (for available countries), + # delete if not found above + nuts_to_delete = nuts_model.search( + [('country_id', 'in', [x.id for x in self._countries.values()])]) + # Download NUTS in english, create or update + logger.info('Import NUTS 2013 English') + xmlcontent = self._download_nuts() + dom = etree.fromstring(xmlcontent) + for node in dom.iter('Item'): + logger.info('Reading level=%s, id=%s' % + (node.get('idLevel', 'N/A'), + node.get('id', 'N/A'))) + nuts = self.create_or_update_nuts(node) + if nuts and nuts in nuts_to_delete: + nuts_to_delete -= nuts + # Delete obsolete NUTS + if nuts_to_delete: + logger.info('%d NUTS entries deleted' % len(nuts_to_delete)) + nuts_to_delete.unlink() + logger.info( + 'The wizard to create NUTS entries from RAMON ' + 'has been successfully completed.') + + return True diff --git a/base_location_nuts/wizard/nuts_import_view.xml b/base_location_nuts/wizard/nuts_import_view.xml new file mode 100644 index 000000000..6acd7bdbc --- /dev/null +++ b/base_location_nuts/wizard/nuts_import_view.xml @@ -0,0 +1,51 @@ + + + + + + NUTS import + nuts.import + +
+
+ This wizard will download the lastest version of + NUTS 2013 from Europe RAMON metadata service. + Updating or creating new NUTS entries if not + found already in the system, and DELETING MISSING + ENTRIES from new downloaded file. +
+
+
+
+
+
+ + + Import NUTS 2013 from RAMON + ir.actions.act_window + nuts.import + + form + form + new + + + + + + Import NUTS 2013 from RAMON + You can import NUTS from RAMON european service. + + 20 + automatic + + +
+