Add scheduled task to update rates
issue8804 review347191002
This commit is contained in:
1 parent
81ce1da6d6
commit
ec2ca6d49a
12 files changed
+392
-2
No files matched your search
@@ -1,3 +1,5 @@
|
||||
* Add scheduled task to update rates
|
||||
|
||||
Version 5.8.0 - 2020-11-02
|
||||
* Bug fixes (see mercurial logs for details)
|
||||
* Remove support for Python 3.5
|
||||
|
||||
@@ -7,7 +7,11 @@ from trytond.pool import Pool
|
||||
def register():
|
||||
# Prevent to import backend when importing scripts
|
||||
from . import currency
|
||||
from . import ir
|
||||
Pool.register(
|
||||
currency.Currency,
|
||||
currency.CurrencyRate,
|
||||
currency.Cron,
|
||||
currency.Cron_Currency,
|
||||
ir.Cron,
|
||||
module='currency', type_='model')
|
||||
+178
-1
@@ -1,22 +1,33 @@
|
||||
# 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
|
||||
import logging
|
||||
|
||||
from decimal import Decimal, ROUND_HALF_EVEN, localcontext
|
||||
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
from sql import Window
|
||||
from sql.functions import NthValue
|
||||
|
||||
try:
|
||||
from forex_python.converter import CurrencyRates, RatesNotAvailableError
|
||||
get_rates = CurrencyRates(force_decimal=True).get_rates
|
||||
except ImportError:
|
||||
CurrencyRates = get_rates = None
|
||||
|
||||
from trytond.i18n import gettext
|
||||
from trytond.model import (
|
||||
ModelView, ModelSQL, DeactivableMixin, fields, Unique, Check, SymbolMixin)
|
||||
from trytond.transaction import Transaction
|
||||
from trytond.pool import Pool
|
||||
from trytond.rpc import RPC
|
||||
from trytond.pyson import Eval
|
||||
from trytond.pyson import Eval, If
|
||||
|
||||
from .exceptions import RateError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Currency(SymbolMixin, DeactivableMixin, ModelSQL, ModelView):
|
||||
'Currency'
|
||||
@@ -269,3 +280,169 @@ class CurrencyRate(ModelSQL, ModelView):
|
||||
|
||||
def get_rec_name(self, name):
|
||||
return str(self.date)
|
||||
|
||||
|
||||
class CronFetchError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class Cron(ModelSQL, ModelView):
|
||||
"Currency Cron"
|
||||
__name__ = 'currency.cron'
|
||||
|
||||
source = fields.Selection(
|
||||
[], "Source", required=True,
|
||||
help="The external source for rates.")
|
||||
frequency = fields.Selection([
|
||||
('daily', "Daily"),
|
||||
('weekly', "Weekly"),
|
||||
('monthly', "Monthly"),
|
||||
], "Frequency", required=True,
|
||||
help="How frequently rates must be updated.")
|
||||
weekday = fields.Many2One(
|
||||
'ir.calendar.day', "Day of Week",
|
||||
states={
|
||||
'required': Eval('frequency') == 'weekly',
|
||||
'invisible': Eval('frequency') != 'weekly',
|
||||
},
|
||||
depends=['frequency'])
|
||||
day = fields.Integer(
|
||||
"Day of Month",
|
||||
domain=[If(Eval('frequency') == 'monthly',
|
||||
[('day', '>=', 1), ('day', '<=', 31)],
|
||||
[('day', '=', None)]),
|
||||
],
|
||||
states={
|
||||
'required': Eval('frequency') == 'monthly',
|
||||
'invisible': Eval('frequency') != 'monthly',
|
||||
},
|
||||
depends=['frequency'])
|
||||
currency = fields.Many2One(
|
||||
'currency.currency', "Currency", required=True,
|
||||
help="The base currency to fetch rate.")
|
||||
currencies = fields.Many2Many(
|
||||
'currency.cron-currency.currency', 'cron', 'currency', "Currencies",
|
||||
help="The currencies to update the rate.")
|
||||
last_update = fields.Date("Last Update", required=True)
|
||||
|
||||
@classmethod
|
||||
def __setup__(cls):
|
||||
super().__setup__()
|
||||
cls._buttons.update({
|
||||
'run': {},
|
||||
})
|
||||
if CurrencyRates:
|
||||
cls.source.selection.append(('ecb', "European Central Bank"))
|
||||
|
||||
@classmethod
|
||||
def default_frequency(cls):
|
||||
return 'monthly'
|
||||
|
||||
@classmethod
|
||||
def default_day(cls):
|
||||
return 1
|
||||
|
||||
@classmethod
|
||||
def default_last_update(cls):
|
||||
pool = Pool()
|
||||
Date = pool.get('ir.date')
|
||||
return Date.today()
|
||||
|
||||
@classmethod
|
||||
@ModelView.button
|
||||
def run(cls, crons):
|
||||
cls.update(crons)
|
||||
|
||||
@classmethod
|
||||
def update(cls, crons=None):
|
||||
pool = Pool()
|
||||
Rate = pool.get('currency.currency.rate')
|
||||
if crons is None:
|
||||
crons = cls.search([])
|
||||
rates = []
|
||||
for cron in crons:
|
||||
rates.extend(cron._update())
|
||||
Rate.save(rates)
|
||||
cls.save(crons)
|
||||
|
||||
def _update(self):
|
||||
limit = self.limit_update()
|
||||
date = self.next_update()
|
||||
while date <= limit:
|
||||
try:
|
||||
yield from self._rates(date)
|
||||
except CronFetchError:
|
||||
logger.warning("Could not fetch rates temporary")
|
||||
break
|
||||
except Exception:
|
||||
logger.error("Fail to fetch rates", exc_info=True)
|
||||
break
|
||||
self.last_update = date
|
||||
date = self.next_update()
|
||||
|
||||
def next_update(self):
|
||||
return self.last_update + self.delta()
|
||||
|
||||
def limit_update(self):
|
||||
pool = Pool()
|
||||
Date = pool.get('ir.date')
|
||||
return Date.today()
|
||||
|
||||
def delta(self):
|
||||
if self.frequency == 'daily':
|
||||
delta = relativedelta(days=1)
|
||||
elif self.frequency == 'weekly':
|
||||
delta = relativedelta(weeks=1, weekday=int(self.weekday.index))
|
||||
elif self.frequency == 'monthly':
|
||||
delta = relativedelta(months=1, day=self.day)
|
||||
else:
|
||||
delta = relativedelta()
|
||||
return delta
|
||||
|
||||
def _rates(self, date, rounding=None):
|
||||
pool = Pool()
|
||||
Rate = pool.get('currency.currency.rate')
|
||||
values = getattr(self, 'fetch_%s' % self.source)(date)
|
||||
|
||||
exp = Decimal(Decimal(1) / 10 ** Rate.rate.digits[1])
|
||||
rates = Rate.search([
|
||||
('date', '=', date),
|
||||
])
|
||||
code2rates = {r.currency.code: r for r in rates}
|
||||
|
||||
def get_rate(currency):
|
||||
if currency.code in code2rates:
|
||||
rate = code2rates[currency.code]
|
||||
else:
|
||||
rate = Rate(date=date, currency=currency)
|
||||
return rate
|
||||
|
||||
rate = get_rate(self.currency)
|
||||
rate.rate = Decimal(1).quantize(exp, rounding=rounding)
|
||||
yield rate
|
||||
|
||||
for currency in self.currencies:
|
||||
if currency.code not in values:
|
||||
continue
|
||||
value = values[currency.code]
|
||||
rate = get_rate(currency)
|
||||
rate.rate = value.quantize(exp, rounding=rounding)
|
||||
yield rate
|
||||
|
||||
def fetch_ecb(self, date):
|
||||
try:
|
||||
return get_rates(self.currency.code, date)
|
||||
except RatesNotAvailableError as e:
|
||||
raise CronFetchError() from e
|
||||
|
||||
|
||||
class Cron_Currency(ModelSQL):
|
||||
"Currency Cron - Currency"
|
||||
__name__ = 'currency.cron-currency.currency'
|
||||
|
||||
cron = fields.Many2One(
|
||||
'currency.cron', "Cron",
|
||||
required=True, select=True, ondelete='CASCADE')
|
||||
currency = fields.Many2One(
|
||||
'currency.currency', "Currency",
|
||||
required=True, ondelete='CASCADE')
|
||||
@@ -98,5 +98,61 @@ this repository contains the full copyright notices and license terms. -->
|
||||
<field name="perm_delete" eval="True"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="cron_view_list">
|
||||
<field name="model">currency.cron</field>
|
||||
<field name="type">tree</field>
|
||||
<field name="name">cron_list</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="cron_view_form">
|
||||
<field name="model">currency.cron</field>
|
||||
<field name="type">form</field>
|
||||
<field name="name">cron_form</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.action.act_window" id="act_cron_form">
|
||||
<field name="name">Scheduled Rate Updates</field>
|
||||
<field name="res_model">currency.cron</field>
|
||||
</record>
|
||||
<record model="ir.action.act_window.view" id="act_cron_form_view1">
|
||||
<field name="sequence" eval="10"/>
|
||||
<field name="view" ref="cron_view_list"/>
|
||||
<field name="act_window" ref="act_cron_form"/>
|
||||
</record>
|
||||
<record model="ir.action.act_window.view" id="act_cron_form_view2">
|
||||
<field name="sequence" eval="20"/>
|
||||
<field name="view" ref="cron_view_form"/>
|
||||
<field name="act_window" ref="act_cron_form"/>
|
||||
</record>
|
||||
|
||||
<menuitem parent="menu_currency" action="act_cron_form" sequence="20" id="menu_cron_form"/>
|
||||
|
||||
<record model="ir.model.access" id="access_cron">
|
||||
<field name="model" search="[('model', '=', 'currency.cron')]"/>
|
||||
<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_cron_currency_admin">
|
||||
<field name="model" search="[('model', '=', 'currency.cron')]"/>
|
||||
<field name="group" ref="group_currency_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>
|
||||
|
||||
<record model="ir.model.button" id="cron_run_button">
|
||||
<field name="name">run</field>
|
||||
<field name="string">Run</field>
|
||||
<field name="model" search="[('model', '=', 'currency.cron')]"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.cron" id="cron_cron">
|
||||
<field name="method">currency.cron|update</field>
|
||||
<field name="interval_number" eval="1"/>
|
||||
<field name="interval_type">days</field>
|
||||
</record>
|
||||
</data>
|
||||
</tryton>
|
||||
@@ -37,3 +37,23 @@ currencies.
|
||||
As exchange rates vary over time they are stored along with the date from
|
||||
when they apply.
|
||||
All exchange rates are relative with respect to each other.
|
||||
|
||||
.. _model-currency.cron:
|
||||
|
||||
Scheduled Rate Updates
|
||||
======================
|
||||
|
||||
The `Exchange Rates <model-currency.rate>` can be automatically updated using a
|
||||
`Scheduled Task <trytond:model-ir.cron>`.
|
||||
When the scheduled task is run it uses the external source to fill in the
|
||||
missing rates since the last update.
|
||||
The frequency determines which dates get rates.
|
||||
|
||||
.. seealso::
|
||||
|
||||
Scheduled Rate Updates can be found by opening the main menu item:
|
||||
|
||||
|Currency --> Scheduled Rate Updates|__
|
||||
|
||||
.. |Currency --> Scheduled Rate Updates| replace:: :menuselection:`Currency --> Scheduled Rate Updates`
|
||||
__ https://demo.tryton.org/model/currency.cron
|
||||
@@ -35,3 +35,29 @@ the 15th of January you just need to set the rate on the USD currency to
|
||||
and 14th of January the last available rate will be used.
|
||||
Here this rate would be ``1.1222`` as this was the rate set for the 1st of
|
||||
January.
|
||||
|
||||
.. _Scheduling rate updates:
|
||||
|
||||
Scheduling rate updates
|
||||
=======================
|
||||
|
||||
You may want to define some `Scheduled Rate Updates <model-currency.cron>` to
|
||||
keep the `Exchange Rates <model-currency.currency.rate>` up to date.
|
||||
When doing this you have to pick a source, the base `currency
|
||||
<model-currency.currency>` and a frequency.
|
||||
Then for each required date since the last update, a rate of ``1.0`` will be
|
||||
set for the base currency and the corresponding rate will be set for each of
|
||||
the selected currencies.
|
||||
|
||||
.. warning::
|
||||
|
||||
It is strongly advised to use the same base currency for all *Scheduled Rate
|
||||
Updates*.
|
||||
|
||||
.. note::
|
||||
|
||||
The currency module only supports the rates provided by the `European
|
||||
Central Bank`_, but third party modules can add additional sources for
|
||||
exchange rates.
|
||||
|
||||
.. _European Central Bank: https://www.ecb.europa.eu/stats/policy_and_exchange_rates/euro_reference_exchange_rates/html/index.en.html
|
||||
@@ -0,0 +1,13 @@
|
||||
# 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.pool import PoolMeta
|
||||
|
||||
|
||||
class Cron(metaclass=PoolMeta):
|
||||
__name__ = 'ir.cron'
|
||||
|
||||
@classmethod
|
||||
def __setup__(cls):
|
||||
super().__setup__()
|
||||
cls.method.selection.append(
|
||||
('currency.cron|update', "Update Currency Rates"))
|
||||
@@ -60,7 +60,7 @@ else:
|
||||
if local_version:
|
||||
version += '+' + '.'.join(local_version)
|
||||
|
||||
requires = ['python-sql >= 0.9']
|
||||
requires = ['python-sql >= 0.9', 'python-dateutil']
|
||||
for dep in info.get('depends', []):
|
||||
if not re.match(r'(ir|res)(\W|$)', dep):
|
||||
requires.append(get_require_version('trytond_%s' % dep))
|
||||
@@ -144,6 +144,7 @@ setup(name=name,
|
||||
extras_require={
|
||||
'data': [
|
||||
'pycountry', 'forex-python', get_require_version('proteus')],
|
||||
'ecb_rate': ['forex-python'],
|
||||
},
|
||||
dependency_links=dependency_links,
|
||||
zip_safe=False,
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
====================
|
||||
Currency Rate Update
|
||||
====================
|
||||
|
||||
Imports::
|
||||
|
||||
>>> import datetime as dt
|
||||
>>> from proteus import Model
|
||||
>>> from trytond.tests.tools import activate_modules
|
||||
|
||||
>>> today = dt.date.today()
|
||||
>>> yesterday = today - dt.timedelta(days=1)
|
||||
>>> before_yesterday = yesterday - dt.timedelta(days=1)
|
||||
|
||||
Activate modules::
|
||||
|
||||
>>> config = activate_modules('currency')
|
||||
|
||||
Import models::
|
||||
|
||||
>>> Currency = Model.get('currency.currency')
|
||||
>>> Cron = Model.get('currency.cron')
|
||||
|
||||
Create some currencies::
|
||||
|
||||
>>> eur = Currency(name="Euro", code='EUR', symbol="€")
|
||||
>>> eur.save()
|
||||
>>> usd = Currency(name="U.S. Dollar", code='USD', symbol="$")
|
||||
>>> usd.save()
|
||||
|
||||
Setup cron::
|
||||
|
||||
>>> cron = Cron()
|
||||
>>> cron.source = 'ecb'
|
||||
>>> cron.frequency = 'daily'
|
||||
>>> cron.day = None
|
||||
>>> cron.currency = eur
|
||||
>>> cron.currencies.append(Currency(usd.id))
|
||||
>>> cron.last_update = before_yesterday
|
||||
>>> cron.save()
|
||||
|
||||
Run update::
|
||||
|
||||
>>> cron.click('run')
|
||||
>>> cron.last_update >= yesterday
|
||||
True
|
||||
|
||||
>>> eur.reload()
|
||||
>>> rate, = [r for r in eur.rates if r.date == yesterday]
|
||||
>>> rate.rate
|
||||
Decimal('1.000000')
|
||||
>>> rate, = [r for r in usd.rates if r.date == yesterday]
|
||||
>>> bool(rate.rate)
|
||||
True
|
||||
@@ -303,4 +303,9 @@ def suite():
|
||||
tearDown=doctest_teardown, encoding='utf-8',
|
||||
checker=doctest_checker,
|
||||
optionflags=doctest.REPORT_ONLY_FIRST_FAILURE))
|
||||
suite.addTests(doctest.DocFileSuite(
|
||||
'scenario_currency_rate_update.rst',
|
||||
tearDown=doctest_teardown, encoding='utf-8',
|
||||
checker=doctest_checker,
|
||||
optionflags=doctest.REPORT_ONLY_FIRST_FAILURE))
|
||||
return suite
|
||||
@@ -0,0 +1,23 @@
|
||||
<?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"/>
|
||||
<field name="source"/>
|
||||
<label name="frequency"/>
|
||||
<group id="frequency" col="-1">
|
||||
<field name="frequency"/>
|
||||
<label name="weekday"/>
|
||||
<field name="weekday"/>
|
||||
<label name="day"/>
|
||||
<field name="day"/>
|
||||
</group>
|
||||
<label name="currency"/>
|
||||
<field name="currency"/>
|
||||
<label name="last_update"/>
|
||||
<group id="run">
|
||||
<field name="last_update"/>
|
||||
<button name="run"/>
|
||||
</group>
|
||||
<field name="currencies" colspan="4"/>
|
||||
</form>
|
||||
@@ -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="source" expand="1"/>
|
||||
<field name="frequency"/>
|
||||
<field name="currency"/>
|
||||
<field name="last_update"/>
|
||||
</tree>
|
||||
Reference in new issue
Block a user