diff --git a/currency_rate_update/README.rst b/currency_rate_update/README.rst index c8130cad5..5e6972ed0 100644 --- a/currency_rate_update/README.rst +++ b/currency_rate_update/README.rst @@ -1,10 +1,12 @@ .. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg - :alt: License + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +==================== Currency Rate Update ==================== -Import exchange rates from the Internet. +Download exchange rates automatically from the Internet. The module is able to use the following sources: @@ -38,13 +40,11 @@ The module is able to use the following sources: Configuration ============= -The update can be set under the company form. -You can set for each services which currency you want to update. -The logs of the update are visible under the service note. -You can active or deactivate the update. -The module uses internal ir-cron feature from Odoo, so the job is -launched once the server starts if the 'first execute date' is before -the current day. +To configure the module, go to *Accounting > Configuration > Multi-currencies > Rate Auto-download* and create one or several services to download rates from the Internet. + +Then, go to the page *Accounting > Configuration > Settings* and, in the section *Multi Currencies*, make sure that the option *Automatic Currency Rates Download* is enabled. + +In developper mode, in the menu *Settings > Technical > Scheduled Actions*, make sure that the action *Currency Rate Update* is active. If you want to run it immediately, use the button *Run Manually*. Usage ===== @@ -72,7 +72,6 @@ Roadmap: * Updated daily from Citibank N.A., source in EUR. Information may be delayed. This is parsed from an HTML page, so it may be broken at anytime. - Bug Tracker =========== @@ -80,7 +79,6 @@ Bugs are tracked on `GitHub Issues (BOC) * Dmytro Katyukha - Maintainer ---------- -.. image:: http://odoo-community.org/logo.png +.. image:: https://odoo-community.org/logo.png :alt: Odoo Community Association - :target: http://odoo-community.org + :target: https://odoo-community.org This module is maintained by the OCA. @@ -122,4 +119,4 @@ 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. +To contribute to this module, please visit https://odoo-community.org. diff --git a/currency_rate_update/__init__.py b/currency_rate_update/__init__.py index d99ae047e..e1dcac71b 100644 --- a/currency_rate_update/__init__.py +++ b/currency_rate_update/__init__.py @@ -1,2 +1,4 @@ -from . import model +# -*- coding: utf-8 -*- + +from . import models from .services.currency_getter_interface import CurrencyGetterInterface diff --git a/currency_rate_update/__manifest__.py b/currency_rate_update/__manifest__.py index a810ded94..3e90702b9 100644 --- a/currency_rate_update/__manifest__.py +++ b/currency_rate_update/__manifest__.py @@ -13,13 +13,11 @@ "account", # Added to ensure account security groups are present ], "data": [ - "view/service_cron_data.xml", - "view/currency_rate_update.xml", - "view/company_view.xml", + "data/cron.xml", + "views/currency_rate_update.xml", + "views/account_config_settings.xml", "security/rule.xml", "security/ir.model.access.csv", ], - "images": [], - "demo": [], 'installable': True } diff --git a/currency_rate_update/view/service_cron_data.xml b/currency_rate_update/data/cron.xml similarity index 100% rename from currency_rate_update/view/service_cron_data.xml rename to currency_rate_update/data/cron.xml diff --git a/currency_rate_update/model/__init__.py b/currency_rate_update/model/__init__.py deleted file mode 100644 index f67366f87..000000000 --- a/currency_rate_update/model/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from . import currency_rate_update -from . import company diff --git a/currency_rate_update/model/company.py b/currency_rate_update/model/company.py deleted file mode 100644 index 8ab0d0c1a..000000000 --- a/currency_rate_update/model/company.py +++ /dev/null @@ -1,27 +0,0 @@ -# -*- coding: utf-8 -*- -# © 2009-2016 Camptocamp -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). - -from odoo import models, fields, api - - -class ResCompany(models.Model): - """override company to add currency update""" - - _inherit = "res.company" - - @api.multi - def button_refresh_currency(self): - """Refresh the currencies rates""" - self.ensure_one() - self.services_to_use.refresh_currency() - - # Activate the currency update - auto_currency_up = fields.Boolean( - string='Automatic Update', - help="Automatic update of the currencies for this company") - # List of services to fetch rates - services_to_use = fields.One2many( - 'currency.rate.update.service', - 'company_id', - string='Currency update services') diff --git a/currency_rate_update/models/__init__.py b/currency_rate_update/models/__init__.py new file mode 100644 index 000000000..8896effe1 --- /dev/null +++ b/currency_rate_update/models/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- + +from . import currency_rate_update +from . import company +from . import account_config_settings diff --git a/currency_rate_update/models/account_config_settings.py b/currency_rate_update/models/account_config_settings.py new file mode 100644 index 000000000..123ae4ced --- /dev/null +++ b/currency_rate_update/models/account_config_settings.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +# © 2016 Akretion (Alexis de Lattre ) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import models, fields + + +class AccountConfigSettings(models.TransientModel): + _inherit = 'account.config.settings' + + auto_currency_up = fields.Boolean( + related='company_id.auto_currency_up') diff --git a/currency_rate_update/models/company.py b/currency_rate_update/models/company.py new file mode 100644 index 000000000..1e4d96bc8 --- /dev/null +++ b/currency_rate_update/models/company.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +# © 2009-2016 Camptocamp +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import models, fields + + +class ResCompany(models.Model): + _inherit = "res.company" + + # Activate the currency update + auto_currency_up = fields.Boolean( + string='Automatic Currency Rates Download', default=True, + help="Automatic download of currency rates for this company") diff --git a/currency_rate_update/model/currency_rate_update.py b/currency_rate_update/models/currency_rate_update.py similarity index 50% rename from currency_rate_update/model/currency_rate_update.py rename to currency_rate_update/models/currency_rate_update.py index f2dedf794..5de2f36ac 100644 --- a/currency_rate_update/model/currency_rate_update.py +++ b/currency_rate_update/models/currency_rate_update.py @@ -9,7 +9,9 @@ from datetime import datetime, time from dateutil.relativedelta import relativedelta from odoo import models, fields, api, _ -from odoo import exceptions +from odoo.exceptions import UserError, ValidationError +from odoo.tools import float_compare + from ..services.currency_getter_interface import CurrencyGetterType @@ -29,17 +31,20 @@ class CurrencyRateUpdateService(models.Model): _name = "currency.rate.update.service" _description = "Currency Rate Update" - @api.one + @api.multi @api.constrains('max_delta_days') def _check_max_delta_days(self): - if self.max_delta_days < 0: - raise exceptions.Warning(_('Max delta days must be >= 0')) + for srv in self: + if srv.max_delta_days < 0: + raise ValidationError(_( + 'Max delta days must be >= 0')) - @api.one + @api.multi @api.constrains('interval_number') def _check_interval_number(self): - if self.interval_number < 0: - raise exceptions.Warning(_('Interval number must be >= 0')) + for srv in self: + if srv.interval_number < 0: + raise ValidationError(_('Interval number must be >= 0')) @api.onchange('interval_number') def _onchange_interval_number(self): @@ -74,6 +79,10 @@ class CurrencyRateUpdateService(models.Model): 'service_id', 'currency_id', string='Currencies available') + # I can't just put readonly=True in the field above because I need + # it as r+w for the on_change to work + currency_list_readonly = fields.Many2many( + related='currency_list', readonly=True) # List of currency to update currency_to_update = fields.Many2many('res.currency', 'res_currency_auto_update_rel', @@ -83,7 +92,7 @@ class CurrencyRateUpdateService(models.Model): 'this service') # Link with company company_id = fields.Many2one( - 'res.company', 'Company', + 'res.company', 'Company', required=True, default=lambda self: self.env['res.company']._company_default_get( 'currency.rate.update.service')) # Note fileds that will be used as a logger @@ -92,7 +101,7 @@ class CurrencyRateUpdateService(models.Model): string='Max delta days', default=4, required=True, help="If the time delta between the rate date given by the " "webservice and the current date exceeds this value, " - "then the currency rate is not updated in OpenERP.") + "then the currency rate is not updated in Odoo.") interval_type = fields.Selection([ ('days', 'Day(s)'), ('weeks', 'Week(s)'), @@ -107,81 +116,87 @@ class CurrencyRateUpdateService(models.Model): _('You can use a service only one time per ' 'company !'))] - @api.one + @api.multi def refresh_currency(self): """Refresh the currencies rates !!for all companies now""" - _logger.info( - 'Starting to refresh currencies with service %s (company: %s)', - self.service, self.company_id.name) rate_obj = self.env['res.currency.rate'] - company = self.company_id - # The multi company currency can be set or no so we handle - # The two case - if company.auto_currency_up: - main_currency = self.company_id.currency_id - if not main_currency: - raise exceptions.Warning(_('There is no main ' - 'currency defined!')) - if main_currency.rate != 1: - raise exceptions.Warning(_('Base currency rate should ' - 'be 1.00!')) - note = self.note or '' - try: - # We initalize the class that will handle the request - # and return a dict of rate - getter = CurrencyGetterType.get(self.service) - curr_to_fetch = [x.name for x in self.currency_to_update] - res, log_info = getter.get_updated_currency( - curr_to_fetch, - main_currency.name, - self.max_delta_days - ) - rate_name = \ - fields.Datetime.to_string(datetime.utcnow().replace( - hour=0, minute=0, second=0, microsecond=0)) - for curr in self.currency_to_update: - if curr.id == main_currency.id: - continue - do_create = True - for rate in curr.rate_ids: - if rate.name == rate_name: - rate.rate = res[curr.name] - do_create = False - break - if do_create: - vals = { - 'currency_id': curr.id, - 'rate': res[curr.name], - 'name': rate_name - } - rate_obj.create(vals) - _logger.info( - 'Updated currency %s via service %s', - curr.name, self.service) + for srv in self: + _logger.info( + 'Starting to refresh currencies with service %s (company: %s)', + srv.service, srv.company_id.name) + company = srv.company_id + # The multi company currency can be set or no so we handle + # The two case + if company.auto_currency_up: + main_currency = company.currency_id + # No need to test if main_currency exists, because it is a + # required field + if float_compare( + main_currency.rate, 1, + precision_rounding=main_currency.rounding): + raise UserError(_( + "In company '%s', the rate of the main currency (%s) " + "must be 1.00 (current rate: %s).") % ( + company.name, + main_currency.name, + main_currency.rate)) + note = srv.note or '' + try: + # We initalize the class that will handle the request + # and return a dict of rate + getter = CurrencyGetterType.get(srv.service) + curr_to_fetch = [x.name for x in srv.currency_to_update] + res, log_info = getter.get_updated_currency( + curr_to_fetch, + main_currency.name, + srv.max_delta_days + ) + rate_name = \ + fields.Datetime.to_string(datetime.utcnow().replace( + hour=0, minute=0, second=0, microsecond=0)) + for curr in srv.currency_to_update: + if curr == main_currency: + continue + do_create = True + for rate in curr.rate_ids: + if rate.name == rate_name: + rate.rate = res[curr.name] + do_create = False + break + if do_create: + vals = { + 'currency_id': curr.id, + 'rate': res[curr.name], + 'name': rate_name + } + rate_obj.create(vals) + _logger.info( + 'Updated currency %s via service %s', + curr.name, srv.service) - # Show the most recent note at the top - msg = '%s \n%s currency updated. %s' % ( - log_info or '', - fields.Datetime.to_string(datetime.today()), - note - ) - self.write({'note': msg}) - except Exception as exc: - error_msg = '\n%s ERROR : %s %s' % ( - fields.Datetime.to_string(datetime.today()), - repr(exc), - note - ) - _logger.error(repr(exc)) - self.write({'note': error_msg}) - if self._context.get('cron', False): - midnight = time(0, 0) - next_run = (datetime.combine( - fields.Date.from_string(self.next_run), - midnight) + - _intervalTypes[str(self.interval_type)] - (self.interval_number)).date() - self.next_run = next_run + # Show the most recent note at the top + msg = '%s \n%s currency updated. %s' % ( + log_info or '', + fields.Datetime.to_string(datetime.today()), + note + ) + srv.write({'note': msg}) + except Exception as exc: + error_msg = '\n%s ERROR: %s %s' % ( + fields.Datetime.to_string(datetime.today()), + repr(exc), + note + ) + _logger.error(repr(exc)) + srv.write({'note': error_msg}) + if self._context.get('cron'): + midnight = time(0, 0) + next_run = (datetime.combine( + fields.Date.from_string(srv.next_run), + midnight) + + _intervalTypes[str(srv.interval_type)] + (srv.interval_number)).date() + srv.next_run = next_run return True @api.multi diff --git a/currency_rate_update/security/rule.xml b/currency_rate_update/security/rule.xml index 2de7a9379..bf5a78f9e 100644 --- a/currency_rate_update/security/rule.xml +++ b/currency_rate_update/security/rule.xml @@ -1,13 +1,10 @@ - - + Current Rate Update Service multi-company - ['|', ('company_id', '=', False), ('company_id', 'child_of', [user.company_id.id])] - diff --git a/currency_rate_update/services/currency_getter_interface.py b/currency_rate_update/services/currency_getter_interface.py index 40e350482..f646b65dd 100644 --- a/currency_rate_update/services/currency_getter_interface.py +++ b/currency_rate_update/services/currency_getter_interface.py @@ -5,8 +5,8 @@ import logging from datetime import datetime -from odoo import fields -from odoo.exceptions import except_orm +from odoo import fields, _ +from odoo.exceptions import UserError _logger = logging.getLogger(__name__) @@ -143,15 +143,11 @@ class CurrencyGetterInterface(object): objfile.close() return rawfile except ImportError: - raise except_orm( - 'Error !', - self.MOD_NAME + 'Unable to import urllib !' - ) + raise UserError( + _('Unable to import urllib.')) except IOError: - raise except_orm( - 'Error !', - self.MOD_NAME + 'Web Service does not exist !' - ) + raise UserError( + _('Web Service does not exist (%s)!') % url) def check_rate_date(self, rate_date, max_delta_days): """Check date constrains. rate_date must be of datetime type""" @@ -160,9 +156,9 @@ class CurrencyGetterInterface(object): raise Exception( 'The rate timestamp %s is %d days away from today, ' 'which is over the limit (%d days). ' - 'Rate not updated in OpenERP.' % (rate_date, - days_delta, - max_delta_days) + 'Rate not updated in Odoo.' % (rate_date, + days_delta, + max_delta_days) ) # We always have a warning when rate_date != today diff --git a/currency_rate_update/services/update_service_ECB.py b/currency_rate_update/services/update_service_ECB.py index 33121a409..73ea0689d 100644 --- a/currency_rate_update/services/update_service_ECB.py +++ b/currency_rate_update/services/update_service_ECB.py @@ -6,7 +6,7 @@ from .currency_getter_interface import CurrencyGetterInterface from datetime import datetime -from odoo.tools import DEFAULT_SERVER_DATE_FORMAT +from lxml import etree import logging _logger = logging.getLogger(__name__) @@ -49,8 +49,6 @@ class ECBGetter(CurrencyGetterInterface): # We do not want to update the main currency if main_currency in currency_array: currency_array.remove(main_currency) - # Move to new XML lib cf Launchpad bug #645263 - from lxml import etree _logger.debug("ECB currency rate service : connecting...") rawfile = self.get_url(url) dom = etree.fromstring(rawfile) @@ -61,8 +59,9 @@ class ECBGetter(CurrencyGetterInterface): } rate_date = dom.xpath('/gesmes:Envelope/def:Cube/def:Cube/@time', namespaces=ecb_ns)[0] - rate_date_datetime = datetime.strptime(rate_date, - DEFAULT_SERVER_DATE_FORMAT) + # Don't use DEFAULT_SERVER_DATE_FORMAT here, because it's + # the format of the XML of ECB, not the format of Odoo server ! + rate_date_datetime = datetime.strptime(rate_date, '%Y-%m-%d') self.check_rate_date(rate_date_datetime, max_delta_days) # We dynamically update supported currencies self.supported_currency_array = dom.xpath( diff --git a/currency_rate_update/view/company_view.xml b/currency_rate_update/view/company_view.xml deleted file mode 100644 index a21a1a25f..000000000 --- a/currency_rate_update/view/company_view.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - res.company.form.inherit - res.company - - - - - - - - - -