2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-04-28 19:46:46 +00:00

Currency API Updates (#4300)

* Adds an API endpoint for manually updating / refreshing currency data from the server

* Update currency rates manually from the settings page

* Add 'last updated' information to the currency exchange backend

* Load currency exchange data via API (on setings page)

* Bump API version

* Table cleanup
This commit is contained in:
Oliver 2023-02-03 12:43:55 +11:00 committed by GitHub
parent 3869d98b32
commit ce3dabedb6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 102 additions and 64 deletions

View File

@ -2,11 +2,15 @@
# InvenTree API version # InvenTree API version
INVENTREE_API_VERSION = 92 INVENTREE_API_VERSION = 93
""" """
Increment this API version number whenever there is a significant change to the API that any clients need to know about Increment this API version number whenever there is a significant change to the API that any clients need to know about
v93 -> 2023-02-03 : https://github.com/inventree/InvenTree/pull/4300
- Adds extra information to the currency exchange endpoint
- Adds API endpoint for manually updating exchange rates
v92 -> 2023-02-02 : https://github.com/inventree/InvenTree/pull/4293 v92 -> 2023-02-02 : https://github.com/inventree/InvenTree/pull/4293
- Adds API endpoint for currency exchange information - Adds API endpoint for currency exchange information

View File

@ -31,8 +31,8 @@ from stock.urls import stock_urls
from users.api import user_urls from users.api import user_urls
from .api import InfoView, NotFoundView from .api import InfoView, NotFoundView
from .views import (AboutView, AppearanceSelectView, CurrencyRefreshView, from .views import (AboutView, AppearanceSelectView, CustomConnectionsView,
CustomConnectionsView, CustomEmailView, CustomLoginView, CustomEmailView, CustomLoginView,
CustomPasswordResetFromKeyView, CustomPasswordResetFromKeyView,
CustomSessionDeleteOtherView, CustomSessionDeleteView, CustomSessionDeleteOtherView, CustomSessionDeleteView,
CustomTwoFactorRemove, DatabaseStatsView, DynamicJsView, CustomTwoFactorRemove, DatabaseStatsView, DynamicJsView,
@ -73,7 +73,6 @@ settings_urls = [
re_path(r'^i18n/?', include('django.conf.urls.i18n')), re_path(r'^i18n/?', include('django.conf.urls.i18n')),
re_path(r'^appearance/?', AppearanceSelectView.as_view(), name='settings-appearance'), re_path(r'^appearance/?', AppearanceSelectView.as_view(), name='settings-appearance'),
re_path(r'^currencies-refresh/', CurrencyRefreshView.as_view(), name='settings-currencies-refresh'),
# Catch any other urls # Catch any other urls
re_path(r'^.*$', SettingsView.as_view(template_name='InvenTree/settings/settings.html'), name='settings'), re_path(r'^.*$', SettingsView.as_view(template_name='InvenTree/settings/settings.html'), name='settings'),

View File

@ -652,20 +652,6 @@ class CustomLoginView(LoginView):
return super().get(request, *args, **kwargs) return super().get(request, *args, **kwargs)
class CurrencyRefreshView(RedirectView):
"""POST endpoint to refresh / update exchange rates."""
url = reverse_lazy("settings-currencies")
def post(self, request, *args, **kwargs):
"""On a POST request we will attempt to refresh the exchange rates."""
from InvenTree.tasks import offload_task, update_exchange_rates
offload_task(update_exchange_rates, force_sync=True)
return redirect(reverse_lazy('settings'))
class AppearanceSelectView(RedirectView): class AppearanceSelectView(RedirectView):
"""View for selecting a color theme.""" """View for selecting a color theme."""

View File

@ -9,7 +9,7 @@ from django.views.decorators.csrf import csrf_exempt
from django_filters.rest_framework import DjangoFilterBackend from django_filters.rest_framework import DjangoFilterBackend
from django_q.tasks import async_task from django_q.tasks import async_task
from djmoney.contrib.exchange.models import Rate from djmoney.contrib.exchange.models import ExchangeBackend, Rate
from rest_framework import filters, permissions, serializers from rest_framework import filters, permissions, serializers
from rest_framework.exceptions import NotAcceptable, NotFound from rest_framework.exceptions import NotAcceptable, NotFound
from rest_framework.permissions import IsAdminUser from rest_framework.permissions import IsAdminUser
@ -104,10 +104,7 @@ class WebhookView(CsrfExemptMixin, APIView):
class CurrencyExchangeView(APIView): class CurrencyExchangeView(APIView):
"""API endpoint for displaying currency information """API endpoint for displaying currency information"""
TODO: Add a POST hook to refresh / update the currency exchange data
"""
permission_classes = [ permission_classes = [
permissions.IsAuthenticated, permissions.IsAuthenticated,
@ -122,9 +119,17 @@ class CurrencyExchangeView(APIView):
except Exception: except Exception:
rates = [] rates = []
# Information on last update
try:
backend = ExchangeBackend.objects.get(name='InvenTreeExchange')
updated = backend.last_update
except Exception:
updated = None
response = { response = {
'base_currency': common.models.InvenTreeSetting.get_setting('INVENTREE_DEFAULT_CURRENCY', 'USD'), 'base_currency': common.models.InvenTreeSetting.get_setting('INVENTREE_DEFAULT_CURRENCY', 'USD'),
'exchange_rates': {} 'exchange_rates': {},
'updated': updated,
} }
for rate in rates: for rate in rates:
@ -133,6 +138,29 @@ class CurrencyExchangeView(APIView):
return Response(response) return Response(response)
class CurrencyRefreshView(APIView):
"""API endpoint for manually refreshing currency exchange rates.
User must be a 'staff' user to access this endpoint
"""
permission_classes = [
permissions.IsAuthenticated,
permissions.IsAdminUser,
]
def post(self, request, *args, **kwargs):
"""Performing a POST request will update currency exchange rates"""
from InvenTree.tasks import update_exchange_rates
update_exchange_rates
return Response({
'success': 'Exchange rates updated',
})
class SettingsList(ListAPI): class SettingsList(ListAPI):
"""Generic ListView for settings. """Generic ListView for settings.
@ -452,6 +480,7 @@ common_api_urls = [
# Currencies # Currencies
re_path(r'^currency/', include([ re_path(r'^currency/', include([
re_path(r'^exchange/', CurrencyExchangeView.as_view(), name='api-currency-exchange'), re_path(r'^exchange/', CurrencyExchangeView.as_view(), name='api-currency-exchange'),
re_path(r'^refresh/', CurrencyRefreshView.as_view(), name='api-currency-refresh'),
])), ])),
# Notifications # Notifications

View File

@ -11,6 +11,7 @@
<div class='panel-content'> <div class='panel-content'>
<table class='table table-striped table-condensed'> <table class='table table-striped table-condensed'>
<tbody> <tbody>
{% include "InvenTree/settings/setting.html" with key="INVENTREE_DEFAULT_CURRENCY" icon="fa-globe" %}
{% include "InvenTree/settings/setting.html" with key="PART_INTERNAL_PRICE" %} {% include "InvenTree/settings/setting.html" with key="PART_INTERNAL_PRICE" %}
{% include "InvenTree/settings/setting.html" with key="PART_BOM_USE_INTERNAL_PRICE" %} {% include "InvenTree/settings/setting.html" with key="PART_BOM_USE_INTERNAL_PRICE" %}
{% include "InvenTree/settings/setting.html" with key="PRICING_DECIMAL_PLACES" %} {% include "InvenTree/settings/setting.html" with key="PRICING_DECIMAL_PLACES" %}
@ -29,54 +30,27 @@
<div class='panel-heading'> <div class='panel-heading'>
<div class='d-flex flex-wrap'> <div class='d-flex flex-wrap'>
<h4>{% trans "Currency Settings" %}</h4> <h4>{% trans "Exchange Rates" %}</h4>
{% include "spacer.html" %} {% include "spacer.html" %}
<div class='btn-group' role='group'> <div class='btn-group' role='group'>
<form action='{% url "settings-currencies-refresh" %}' method='post'> <button type='button' id='btn-update-rates' class='btn btn-primary float-right'>
<div id='refresh-rates-form'> <span class='fas fa-sync-alt'></span> {% trans "Update Now" %}
{% csrf_token %} </button>
<button type='submit' id='update-rates' class='btn btn-primary float-right'>{% trans "Update Now" %}</button>
</div>
</form>
</div> </div>
</div> </div>
</div> </div>
<div class='panel-content'> <div class='panel-content'>
{% if rates_updated %} {% if rates_updated %}
<div class='alert alert-block alert-info'> <div class='alert alert-block alert-info'>
{% trans "Last Update" %} - {{ rates_updated }} {% trans "Last Update" %} - {{ rates_updated }}
</div> </div>
{% else %} {% else %}
<div class='alert alert-block alert-warning'> <div class='alert alert-block alert-warning'>
{% trans "Last Update" %} - {% trans "Never" %} {% trans "Last Update" %} - {% trans "Never" %}
</div> </div>
{% endif %} {% endif %}
<table class='table table-striped table-condensed'> <table class='table table-striped table-condensed' id='exchange-rate-table'></table>
<tbody>
{% include "InvenTree/settings/setting.html" with key="INVENTREE_DEFAULT_CURRENCY" icon="fa-globe" %}
<tr>
<td></td>
<th>{% trans "Base Currency" %}</th>
<th>{{ base_currency }}</th>
<th colspan='2'></th>
</tr>
<tr>
<td></td>
<th>{% trans "Exchange Rates" %}</th>
<th>{% trans "Currency" %}</th>
<th>{% trans "Rate" %}</th>
</tr>
{% for rate in rates %}
<tr>
<td></td>
<td></td>
<td>{{ rate.currency }}</td>
<td>{{ rate.value }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div> </div>
{% endblock panel_content %} {% endblock panel_content %}

View File

@ -151,6 +151,52 @@ $("#edit-password").on('click', function() {
); );
}); });
$('#btn-update-rates').click(function() {
inventreePut(
'{% url "api-currency-refresh" %}',
{},
{
method: 'POST',
success: function(data) {
location.reload();
}
}
);
});
$('#exchange-rate-table').inventreeTable({
url: '{% url "api-currency-exchange" %}',
search: false,
showColumns: false,
sortable: true,
sidePagination: 'client',
onLoadSuccess: function(response) {
var data = response.exchange_rates || {};
var rows = [];
for (var currency in data) {
rows.push({
'currency': currency,
'rate': data[currency],
});
}
$('#exchange-rate-table').bootstrapTable('load', rows);
},
columns: [
{
field: 'currency',
sortable: true,
title: '{% trans "Currency" %}',
},
{
field: 'rate',
sortable: true,
title: '{% trans "Rate" %}',
}
]
});
$('#category-select').select2({ $('#category-select').select2({
placeholder: '', placeholder: '',