-
Notifications
You must be signed in to change notification settings - Fork 17
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
TA#72095 [16.0][MIG] currency_rate_update_boc (#225)
- Loading branch information
1 parent
f40eb58
commit f9f73e9
Showing
14 changed files
with
345 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
Currency Rate Update Bank of Canada | ||
=================================== | ||
This module adds the Bank of Canada to the exchange rate providers. | ||
|
||
.. contents:: Table of Contents | ||
|
||
Usage | ||
----- | ||
You can create an exchange rate provider using Bank of Canada's data. | ||
|
||
.. image:: https://raw.githubusercontent.com/Numigi/odoo-base-addons/16.0/currency_rate_update_boc/static/description/exchange.png | ||
|
||
You can then update the exchange rates using Bank of Canada's data. | ||
|
||
.. image:: https://raw.githubusercontent.com/Numigi/odoo-base-addons/16.0/currency_rate_update_boc/static/description/update.png | ||
|
||
Configuration | ||
------------- | ||
No configuration is required after installation. | ||
|
||
Contributors | ||
------------ | ||
* Numigi (tm) and all its contributors (https://bit.ly/numigiens) | ||
|
||
More information | ||
---------------- | ||
* Meet us at https://bit.ly/numigi-com |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
# Copyright 2023 Numigi (tm) and all its contributors (https://bit.ly/numigiens) | ||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). | ||
|
||
from . import models |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
# Copyright 2023 Numigi (tm) and all its contributors (https://bit.ly/numigiens) | ||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). | ||
|
||
{ | ||
"name": "Currency Rate Update Bank of Canada", | ||
"version": "16.0.1.0.0", | ||
"author": "Numigi", | ||
"maintainer": "Numigi", | ||
"license": "AGPL-3", | ||
"category": "Financial Management/Configuration", | ||
"summary": "Update exchange rates using Bank of canada", | ||
"depends": [ | ||
"currency_rate_update", | ||
], | ||
"data": [], | ||
"installable": True, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
# Copyright 2023 Numigi (tm) and all its contributors (https://bit.ly/numigiens) | ||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). | ||
|
||
from . import res_currency_rate_provider |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
# Copyright 2023 Numigi (tm) and all its contributors (https://bit.ly/numigiens) | ||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). | ||
|
||
SUPPORTED_CURRENCIES_BOC = [ | ||
"AUD", | ||
"BRL", | ||
"CAD", | ||
"CHF", | ||
"CNY", | ||
"EUR", | ||
"GBP", | ||
"HKD", | ||
"IDR", | ||
"INR", | ||
"JPY", | ||
"KRW", | ||
"MXN", | ||
"MYR", | ||
"NOK", | ||
"NZD", | ||
"PEN", | ||
"RUB", | ||
"SAR", | ||
"SEK", | ||
"SGD", | ||
"THB", | ||
"TRY", | ||
"TWD", | ||
"USD", | ||
"VND", | ||
"ZAR", | ||
] | ||
|
||
BASE_API_ADDRESS = "https://www.bankofcanada.ca/valet/observations/" |
98 changes: 98 additions & 0 deletions
98
currency_rate_update_boc/models/res_currency_rate_provider.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
# Copyright 2023 Numigi (tm) and all its contributors (https://bit.ly/numigiens) | ||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). | ||
|
||
|
||
from collections import defaultdict | ||
from datetime import timedelta | ||
import requests | ||
|
||
from odoo import fields, models, _ | ||
from odoo.exceptions import ValidationError | ||
|
||
from .currency import SUPPORTED_CURRENCIES_BOC, BASE_API_ADDRESS | ||
|
||
|
||
class ResCurrencyRateProvider(models.Model): | ||
_inherit = "res.currency.rate.provider" | ||
|
||
service = fields.Selection( | ||
selection_add=[("bank_of_canada", "Bank of canada")], | ||
ondelete={"bank_of_canada": "set default"}, | ||
) | ||
|
||
def _get_supported_currencies(self): | ||
self.ensure_one() | ||
if self.service != "bank_of_canada": | ||
return super()._get_supported_currencies() | ||
return SUPPORTED_CURRENCIES_BOC | ||
|
||
def _obtain_rates(self, base_currency, currencies, date_from, date_to): | ||
self.ensure_one() | ||
if self.service != "bank_of_canada": | ||
return super()._obtain_rates(base_currency, currencies, date_from, date_to) | ||
|
||
date_from = date_from - timedelta(1) | ||
observations = self._get_rates_from_boc( | ||
base_currency, currencies, date_from, date_to | ||
)["observations"] | ||
return self._process_observations(observations, base_currency, currencies) | ||
|
||
def _get_rates_from_boc(self, base_currency, currencies, date_from, date_to): | ||
url = self._get_boc_url(base_currency, currencies, date_from, date_to) | ||
response = self._get_boc_response(url) | ||
return response.json() | ||
|
||
def _get_boc_url(self, base_currency, currencies, date_from, date_to): | ||
date_from = date_from.strftime("%Y-%m-%d") | ||
date_to = date_to.strftime("%Y-%m-%d") | ||
exchanges = {s for s in self._iter_boc_series(base_currency, currencies)} | ||
date_filter = f"start_date={date_from}&end_date={date_to}" | ||
return f"{BASE_API_ADDRESS}{','.join(exchanges)}?{date_filter}" | ||
|
||
def _iter_boc_series(self, base_currency, currencies): | ||
for currency in sorted(currencies): | ||
if base_currency == "CAD" or currency == "CAD": | ||
yield f"FX{base_currency}{currency}" | ||
else: | ||
yield f"FX{base_currency}CAD" | ||
yield f"FXCAD{currency}" | ||
|
||
def _get_boc_response(self, url): | ||
response = requests.get(url) | ||
if response.status_code >= 400: | ||
raise ValidationError( | ||
_( | ||
"The request to the Valet api of the Bank of Canada terminated" | ||
"with an error.\n\n{} : {}".format(response.text, url) | ||
) | ||
) | ||
|
||
return response | ||
|
||
def _process_observations(self, observations, base_currency, currencies): | ||
result = defaultdict(dict) | ||
for observation in observations: | ||
for currency in currencies: | ||
result[observation["d"]][currency] = round( | ||
self._get_rate_from_boc_observation( | ||
observation, base_currency, currency | ||
), | ||
4, | ||
) | ||
|
||
return result | ||
|
||
def _get_rate_from_boc_observation(self, observation, base_currency, currency): | ||
if base_currency == "CAD" or currency == "CAD": | ||
series = f"FX{base_currency}{currency}" | ||
return self._get_rate_from_boc_series(observation, series) | ||
else: | ||
first_series = f"FX{base_currency}CAD" | ||
second_series = f"FXCAD{currency}" | ||
first_rate = self._get_rate_from_boc_series(observation, first_series) | ||
second_rate = self._get_rate_from_boc_series(observation, second_series) | ||
|
||
return first_rate * second_rate | ||
|
||
def _get_rate_from_boc_series(self, observation, series): | ||
return float(observation[series]["v"]) |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
{ | ||
"terms": { | ||
"url": "https://www.bankofcanada.ca/terms/" | ||
}, | ||
"seriesDetail": { | ||
"FXEURCAD": { | ||
"label": "EUR/CAD", | ||
"description": "European euro to Canadian dollar daily exchange rate", | ||
"dimension": { | ||
"key": "d", | ||
"name": "Date" | ||
} | ||
}, | ||
"FXUSDCAD": { | ||
"label": "USD/CAD", | ||
"description": "US dollar to Canadian dollar daily exchange rate", | ||
"dimension": { | ||
"key": "d", | ||
"name": "Date" | ||
} | ||
} | ||
}, | ||
"observations": [ | ||
{ | ||
"d": "2021-05-03", | ||
"FXEURCAD": { | ||
"v": "1.4808" | ||
}, | ||
"FXUSDCAD": { | ||
"v": "1.2279" | ||
} | ||
} | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
# Copyright 2023 Numigi (tm) and all its contributors (https://bit.ly/numigiens) | ||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). | ||
|
||
from . import test_currency |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
# Copyright 2023 Numigi (tm) and all its contributors (https://bit.ly/numigiens) | ||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). | ||
|
||
from contextlib import contextmanager | ||
from datetime import date, timedelta | ||
from ddt import ddt, data | ||
import json | ||
import os | ||
import pytest | ||
|
||
from odoo.tests.common import TransactionCase | ||
from odoo.exceptions import ValidationError | ||
from odoo.addons.website.tools import MockRequest | ||
|
||
|
||
@ddt | ||
class TestBocRateProvider(TransactionCase): | ||
@classmethod | ||
def setUpClass(cls): | ||
super().setUpClass() | ||
cls.cad = cls.env.ref("base.CAD") | ||
cls.cad.active = True | ||
|
||
cls.usd = cls.env.ref("base.USD") | ||
|
||
cls.company = cls.env["res.company"].create({"name": "Company"}) | ||
cls.company.currency_id = cls.usd | ||
|
||
cls.date = date(2021, 5, 3) | ||
cls.date_string = cls.date.strftime("%Y-%m-%d") | ||
|
||
cls.provider = cls.env["res.currency.rate.provider"].create( | ||
{ | ||
"service": "bank_of_canada", | ||
"currency_ids": [(4, cls.cad.id)], | ||
"company_id": cls.company.id, | ||
} | ||
) | ||
|
||
cls.test_boc_url = ( | ||
"https://www.bankofcanada.ca/valet/observations/" | ||
"FXUSDCAD,FXEURCAD?start_date=%s&end_date=%s" | ||
) % (cls.date_string, cls.date_string) | ||
cls.dumb_test_boc_url = "https://www.bankofcanada.ca/valet/observations/" | ||
|
||
@data("CAD", "USD", "EUR") | ||
def test_supported_currencies(self, currency_code): | ||
supported_currencies = self.provider.available_currency_ids.mapped("name") | ||
assert currency_code in supported_currencies | ||
|
||
def test_boc_response(self): | ||
json_data = self._get_rates_json() | ||
|
||
with self._mock_boc_response(self.test_boc_url, json_data): | ||
rates = self.provider._get_boc_response(self.test_boc_url).json() | ||
assert rates == json_data | ||
|
||
@data(401, 404, 410, 500, 502) | ||
def test_boc_response_exceptions(self, error_code): | ||
with MockRequest(self.env) as m: | ||
m.httprequest.method = "GET" | ||
m.get(self.dumb_test_boc_url, status_code=error_code) | ||
with pytest.raises(ValidationError): | ||
self.provider._get_boc_response(self.dumb_test_boc_url) | ||
|
||
def test_rates_cad2x(self): | ||
rates = self.provider._obtain_rates("CAD", ["USD", "EUR"], self.date, self.date) | ||
assert rates[self.date_string]["USD"] == round(1 / 1.2279, 4) | ||
assert rates[self.date_string]["EUR"] == round(1 / 1.4808, 4) | ||
|
||
def test_rates_x2cad(self): | ||
rates = self.provider._obtain_rates("USD", ["CAD"], self.date, self.date) | ||
assert rates[self.date_string]["CAD"] == 1.2279 | ||
|
||
def test_rates_x2x(self): | ||
rates = self.provider._obtain_rates("USD", ["EUR"], self.date, self.date) | ||
assert rates[self.date_string]["EUR"] == round(1.2279 * 1 / 1.4808, 4) | ||
|
||
def test_invalid_currency(self): | ||
with pytest.raises(ValidationError): | ||
self.provider._obtain_rates("USD", ["ZZZZ"], self.date, self.date) | ||
|
||
def test_invalid_date(self): | ||
with pytest.raises(ValidationError): | ||
self.provider._obtain_rates( | ||
"USD", ["ZZZZ"], self.date, self.date - timedelta(5) | ||
) | ||
|
||
def test_cron__loads_rates_one_day_prior(self): | ||
"""Test that the rates of yesterday are loaded by the cron. | ||
With the Bank of Canada, the daily rates are only available the day after. | ||
When the cron is executed the rates of the current date are not available. | ||
The rates of the current date are loaded the following day. | ||
Otherwise, the cron ends up loading no rate at all. | ||
""" | ||
self.provider.last_successful_run = self.date | ||
self.provider.next_run = self.date + timedelta(1) | ||
|
||
url = self.test_boc_url + "FXUSDCAD" | ||
json_data = self._get_rates_json() | ||
|
||
with self._mock_boc_response(url, json_data): | ||
self.env["res.currency.rate.provider"]._scheduled_update() | ||
|
||
rate = self.cad._get_rates(self.company, self.date)[self.cad.id] | ||
assert round(rate, 4) == 1.2279 | ||
|
||
@contextmanager | ||
def _mock_boc_response(self, url, json_data): | ||
with MockRequest(self.env) as m: | ||
m.httprequest.method = "GET" | ||
m.get(url, json=json_data) | ||
yield | ||
|
||
def _get_rates_json(self): | ||
dir_path = os.path.dirname(os.path.realpath(__file__)) | ||
with open(f"{dir_path}/FX_RATES.json") as f: | ||
data = json.load(f) | ||
return data |