Add scheduled task to update rates

issue8804
review347191002
This commit is contained in:
Cédric Krier committed 2021-02-21 19:31:10 +01:00
1 parent 81ce1da6d6
commit ec2ca6d49a
12 files changed
+392 -2

No files matched your search

+2
View File
@@ -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
+4
View File
@@ -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
View File
@@ -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')
+56
View File
@@ -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>
+20
View File
@@ -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
+26
View File
@@ -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
+13
View File
@@ -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"))
+2 -1
View File
@@ -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,
+54
View File
@@ -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
+5
View File
@@ -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
+23
View File
@@ -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>
+9
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="source" expand="1"/>
<field name="frequency"/>
<field name="currency"/>
<field name="last_update"/>
</tree>