diff --git a/InvenTree/InvenTree/exchange.py b/InvenTree/InvenTree/exchange.py index 0a75436b1e..0695e69f48 100644 --- a/InvenTree/InvenTree/exchange.py +++ b/InvenTree/InvenTree/exchange.py @@ -1,120 +1,29 @@ from django.conf import settings as inventree_settings -from djmoney.contrib.exchange.backends.base import BaseExchangeBackend -from djmoney.contrib.exchange.models import Rate - -from common.models import InvenTreeSetting +from djmoney.contrib.exchange.backends.base import SimpleExchangeBackend -def get_exchange_rate_backend(): - """ Return the exchange rate backend set by user """ - - custom = InvenTreeSetting.get_setting('CUSTOM_EXCHANGE_RATES', False) - - if custom: - return InvenTreeManualExchangeBackend() - else: - return ExchangeRateHostBackend() - - -class InvenTreeManualExchangeBackend(BaseExchangeBackend): +class InvenTreeExchange(SimpleExchangeBackend): """ - Backend for manually updating currency exchange rates + Backend for automatically updating currency exchange rates. - See the documentation for django-money: https://github.com/django-money/django-money - - Specifically: https://github.com/django-money/django-money/tree/master/djmoney/contrib/exchange/backends + Uses the exchangerate.host service API """ - name = 'inventree' - url = None - custom_rates = True - base_currency = None - currencies = [] - - def update_default_currency(self): - """ Update to base currency """ - - self.base_currency = InvenTreeSetting.get_setting('INVENTREE_DEFAULT_CURRENCY', 'USD') - - def __init__(self, url=None): - """ Overrides init to update url, base currency and currencies """ - - self.url = url - - self.update_default_currency() - - # Update name - self.name = self.name + '-' + self.base_currency.lower() - - self.currencies = inventree_settings.CURRENCIES - - super().__init__() - - def get_rates(self, **kwargs): - """ Returns a mapping : """ - - return kwargs.get('rates', {}) - - def get_stored_rates(self): - """ Returns stored rate for specified backend and base currency """ - - stored_rates = {} - - stored_rates_obj = Rate.objects.all().prefetch_related('backend') - - for rate in stored_rates_obj: - # Find match for backend and base currency - if rate.backend.name == self.name and rate.backend.base_currency == self.base_currency: - # print(f'{rate.currency} | {rate.value} | {rate.backend} | {rate.backend.base_currency}') - stored_rates[rate.currency] = rate.value - - return stored_rates - - -class ExchangeRateHostBackend(InvenTreeManualExchangeBackend): - """ - Backend for https://exchangerate.host/ - """ - - name = "exchangerate.host" + name = "InvenTreeExchange" def __init__(self): self.url = "https://api.exchangerate.host/latest" - self.custom_rates = False - - super().__init__(url=self.url) + super().__init__() def get_params(self): # No API key is required - return {} + return { + } - def update_rates(self, base_currency=None): - """ Override update_rates method using currencies found in the settings - """ + def update_rates(self, base_currency=inventree_settings.BASE_CURRENCY): - if base_currency: - self.base_currency = base_currency - else: - self.update_default_currency() - - symbols = ','.join(self.currencies) + symbols = ','.join(inventree_settings.CURRENCIES) - super().update_rates(base_currency=self.base_currency, symbols=symbols) - - def get_rates(self, **params): - """ Returns a mapping : """ - - # Set base currency - params.update(base=self.base_currency) - - response = self.get_response(**params) - - try: - return self.parse_json(response)['rates'] - except KeyError: - # API response did not contain any rate - pass - - return {} + super().update_rates(base=base_currency, symbols=symbols) diff --git a/InvenTree/InvenTree/forms.py b/InvenTree/InvenTree/forms.py index 488e982ddc..d843c1ddef 100644 --- a/InvenTree/InvenTree/forms.py +++ b/InvenTree/InvenTree/forms.py @@ -16,8 +16,6 @@ from crispy_forms.bootstrap import PrependedText, AppendedText, PrependedAppende from common.models import ColorTheme from part.models import PartCategory -from .exchange import InvenTreeManualExchangeBackend - class HelperForm(forms.ModelForm): """ Provides simple integration of crispy_forms extension. """ @@ -240,35 +238,3 @@ class SettingCategorySelectForm(forms.ModelForm): css_class='row', ), ) - - -class SettingExchangeRatesForm(forms.Form): - """ Form for displaying and setting currency exchange rates manually """ - - def __init__(self, *args, **kwargs): - - super().__init__(*args, **kwargs) - - exchange_rate_backend = InvenTreeManualExchangeBackend() - - # Update default currency (in case it has changed) - exchange_rate_backend.update_default_currency() - - for currency in exchange_rate_backend.currencies: - if currency != exchange_rate_backend.base_currency: - # Set field name - field_name = currency - # Set field input box - self.fields[field_name] = forms.CharField( - label=field_name, - required=False, - widget=forms.NumberInput(attrs={ - 'name': field_name, - 'class': 'numberinput', - 'style': 'width: 200px;', - 'type': 'number', - 'min': '0', - 'step': 'any', - 'value': 0, - }) - ) diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index 718a10b5e5..618c9f730f 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -19,6 +19,8 @@ import shutil import sys from datetime import datetime +import moneyed + import yaml from django.utils.translation import gettext_lazy as _ @@ -513,11 +515,20 @@ CURRENCIES = CONFIG.get( ], ) -DEFAULT_CURRENCY = get_setting( - 'INVENTREE_DEFAULT_CURRENCY', - CONFIG.get('default_currency', 'USD') +# Check that each provided currency is supported +for currency in CURRENCIES: + if currency not in moneyed.CURRENCIES: + print(f"Currency code '{currency}' is not supported") + sys.exit(1) + +BASE_CURRENCY = get_setting( + 'INVENTREE_BASE_CURRENCY', + CONFIG.get('base_currency', 'USD') ) +# Custom currency exchange backend +EXCHANGE_BACKEND = 'InvenTree.exchange.InvenTreeExchange' + # Extract email settings from the config file email_config = CONFIG.get('email', {}) diff --git a/InvenTree/InvenTree/tasks.py b/InvenTree/InvenTree/tasks.py index 365a94fd07..9a71d5d84c 100644 --- a/InvenTree/InvenTree/tasks.py +++ b/InvenTree/InvenTree/tasks.py @@ -167,16 +167,16 @@ def update_exchange_rates(): """ try: - import common.models - from InvenTree.exchange import ExchangeRateHostBackend + from InvenTree.exchange import InvenTreeExchange + from django.conf import settings except AppRegistryNotReady: # Apps not yet loaded! return - backend = ExchangeRateHostBackend() + backend = InvenTreeExchange() print(f"Updating exchange rates from {backend.url}") - base = common.models.InvenTreeSetting.get_setting('INVENTREE_DEFAULT_CURRENCY') + base = settings.BASE_CURRENCY print(f"Using base currency '{base}'") diff --git a/InvenTree/InvenTree/tests.py b/InvenTree/InvenTree/tests.py index d65829cf8e..b7e5b98c1b 100644 --- a/InvenTree/InvenTree/tests.py +++ b/InvenTree/InvenTree/tests.py @@ -5,6 +5,12 @@ from django.test import TestCase import django.core.exceptions as django_exceptions from django.core.exceptions import ValidationError +from django.conf import settings + +from djmoney.money import Money +from djmoney.contrib.exchange.models import Rate, convert_money +from djmoney.contrib.exchange.exceptions import MissingRate + from .validators import validate_overage, validate_part_name from . import helpers from . import version @@ -13,6 +19,8 @@ from mptt.exceptions import InvalidMove from decimal import Decimal +import InvenTree.tasks + from stock.models import StockLocation @@ -308,3 +316,46 @@ class TestVersionNumber(TestCase): self.assertTrue(v_c > v_b) self.assertTrue(v_d > v_c) self.assertTrue(v_d > v_a) + + +class CurrencyTests(TestCase): + """ + Unit tests for currency / exchange rate functionality + """ + + def test_rates(self): + + # Initially, there will not be any exchange rate information + rates = Rate.objects.all() + + self.assertEqual(rates.count(), 0) + + # Without rate information, we cannot convert anything... + with self.assertRaises(MissingRate): + convert_money(Money(100, 'USD'), 'AUD') + + with self.assertRaises(MissingRate): + convert_money(Money(100, 'AUD'), 'USD') + + currencies = settings.CURRENCIES + + InvenTree.tasks.update_exchange_rates() + + rates = Rate.objects.all() + + self.assertEqual(rates.count(), len(currencies)) + + # Now that we have some exchange rate information, we can perform conversions + + # Forwards + convert_money(Money(100, 'USD'), 'AUD') + + # Backwards + convert_money(Money(100, 'AUD'), 'USD') + + # Convert between non base currencies + convert_money(Money(100, 'CAD'), 'NZD') + + # Convert to a symbol which is not covered + with self.assertRaises(MissingRate): + convert_money(Money(100, 'GBP'), 'ZWL') diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index d297dc18ad..bce493fb23 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -39,9 +39,9 @@ from rest_framework.documentation import include_docs_urls from .views import IndexView, SearchView, DatabaseStatsView from .views import SettingsView, EditUserView, SetPasswordView +from .views import CurrencySettingsView, CurrencyRefreshView from .views import AppearanceSelectView, SettingCategorySelectView from .views import DynamicJsView -from .views import CurrencySettingsView from common.views import SettingEdit @@ -83,15 +83,16 @@ settings_urls = [ url(r'^appearance/?', AppearanceSelectView.as_view(), name='settings-appearance'), url(r'^i18n/?', include('django.conf.urls.i18n')), - url(r'^global/?', SettingsView.as_view(template_name='InvenTree/settings/global.html'), name='settings-global'), - url(r'^report/?', SettingsView.as_view(template_name='InvenTree/settings/report.html'), name='settings-report'), - url(r'^category/?', SettingCategorySelectView.as_view(), name='settings-category'), - url(r'^part/?', SettingsView.as_view(template_name='InvenTree/settings/part.html'), name='settings-part'), - url(r'^stock/?', SettingsView.as_view(template_name='InvenTree/settings/stock.html'), name='settings-stock'), - url(r'^build/?', SettingsView.as_view(template_name='InvenTree/settings/build.html'), name='settings-build'), - url(r'^purchase-order/?', SettingsView.as_view(template_name='InvenTree/settings/po.html'), name='settings-po'), - url(r'^sales-order/?', SettingsView.as_view(template_name='InvenTree/settings/so.html'), name='settings-so'), - url(r'^currencies/?', CurrencySettingsView.as_view(), name='settings-currencies'), + url(r'^global/', SettingsView.as_view(template_name='InvenTree/settings/global.html'), name='settings-global'), + url(r'^report/', SettingsView.as_view(template_name='InvenTree/settings/report.html'), name='settings-report'), + url(r'^category/', SettingCategorySelectView.as_view(), name='settings-category'), + url(r'^part/', SettingsView.as_view(template_name='InvenTree/settings/part.html'), name='settings-part'), + url(r'^stock/', SettingsView.as_view(template_name='InvenTree/settings/stock.html'), name='settings-stock'), + url(r'^build/', SettingsView.as_view(template_name='InvenTree/settings/build.html'), name='settings-build'), + url(r'^purchase-order/', SettingsView.as_view(template_name='InvenTree/settings/po.html'), name='settings-po'), + url(r'^sales-order/', SettingsView.as_view(template_name='InvenTree/settings/so.html'), name='settings-so'), + url(r'^currencies/', CurrencySettingsView.as_view(), name='settings-currencies'), + url(r'^currencies-refresh/', CurrencyRefreshView.as_view(), name='settings-currencies-refresh'), url(r'^(?P\d+)/edit/', SettingEdit.as_view(), name='setting-edit'), diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py index a18845bf02..4ac90bd722 100644 --- a/InvenTree/InvenTree/views.py +++ b/InvenTree/InvenTree/views.py @@ -12,24 +12,26 @@ from django.utils.translation import gettext_lazy as _ from django.template.loader import render_to_string from django.http import JsonResponse, HttpResponseRedirect from django.urls import reverse_lazy +from django.conf import settings from django.contrib.auth.mixins import PermissionRequiredMixin from django.views import View from django.views.generic import ListView, DetailView, CreateView, FormView, DeleteView, UpdateView -from django.views.generic.base import TemplateView +from django.views.generic.base import RedirectView, TemplateView + +from djmoney.contrib.exchange.models import ExchangeBackend, Rate from part.models import Part, PartCategory from stock.models import StockLocation, StockItem from common.models import InvenTreeSetting, ColorTheme from users.models import check_user_role, RuleSet -from InvenTree.helpers import clean_decimal + +import InvenTree.tasks from .forms import DeleteForm, EditUserForm, SetPasswordForm from .forms import ColorThemeSelectForm, SettingCategorySelectForm -from .forms import SettingExchangeRatesForm from .helpers import str2bool -from .exchange import get_exchange_rate_backend from rest_framework import views @@ -772,6 +774,50 @@ class SettingsView(TemplateView): return ctx +class CurrencyRefreshView(RedirectView): + + url = reverse_lazy("settings-currencies") + + def post(self, request, *args, **kwargs): + """ + On a POST request we will attempt to refresh the exchange rates + """ + + print("POST!") + + # Will block for a little bit + InvenTree.tasks.update_exchange_rates() + + return self.get(request, *args, **kwargs) + + +class CurrencySettingsView(TemplateView): + """ + View for configuring currency settings + """ + + template_name = "InvenTree/settings/currencies.html" + + def get_context_data(self, **kwargs): + + ctx = super().get_context_data(**kwargs).copy() + + ctx['settings'] = InvenTreeSetting.objects.all().order_by('key') + ctx["base_currency"] = settings.BASE_CURRENCY + ctx["currencies"] = settings.CURRENCIES + + ctx["rates"] = Rate.objects.filter(backend="InvenTreeExchange") + + # When were the rates last updated? + try: + backend = ExchangeBackend.objects.get(name='InvenTreeExchange') + ctx["rates_updated"] = backend.last_update + except: + ctx["rates_updated"] = None + + return ctx + + class AppearanceSelectView(FormView): """ View for selecting a color theme """ @@ -911,89 +957,3 @@ class DatabaseStatsView(AjaxView): """ return ctx - - -class CurrencySettingsView(FormView): - - form_class = SettingExchangeRatesForm - template_name = 'InvenTree/settings/currencies.html' - success_url = reverse_lazy('settings-currencies') - - exchange_rate_backend = None - - def get_exchange_rate_backend(self): - - if not self.exchange_rate_backend: - self.exchange_rate_backend = get_exchange_rate_backend() - - return self.exchange_rate_backend - - def get_context_data(self, **kwargs): - - context = super().get_context_data(**kwargs) - - # Set default API result - if 'api_rates_success' not in context: - context['default_currency'] = True - else: - # Update form - context['form'] = self.get_form() - - # Get exchange rate backend - exchange_rate_backend = self.get_exchange_rate_backend() - - context['default_currency'] = exchange_rate_backend.base_currency - - context['custom_rates'] = exchange_rate_backend.custom_rates - - context['exchange_backend'] = exchange_rate_backend.name - - return context - - def get_form(self): - - form = super().get_form() - - # Get exchange rate backend - exchange_rate_backend = self.get_exchange_rate_backend() - - # Get stored exchange rates - stored_rates = exchange_rate_backend.get_stored_rates() - - for field in form.fields: - if not exchange_rate_backend.custom_rates: - # Disable all the fields - form.fields[field].disabled = True - form.fields[field].initial = clean_decimal(stored_rates.get(field, 0)) - - return form - - def post(self, request, *args, **kwargs): - - form = self.get_form() - - # Get exchange rate backend - exchange_rate_backend = self.get_exchange_rate_backend() - - if not exchange_rate_backend.custom_rates: - # Refresh rate from Fixer.IO API - exchange_rate_backend.update_rates(base_currency=exchange_rate_backend.base_currency) - # Check if rates have been updated - if not exchange_rate_backend.get_stored_rates(): - # Update context - context = {'api_rates_success': False} - # Return view with updated context - return self.render_to_response(self.get_context_data(form=form, **context)) - else: - # Update rates from form - manual_rates = {} - - if form.is_valid(): - for field, value in form.cleaned_data.items(): - manual_rates[field] = clean_decimal(value) - - exchange_rate_backend.update_rates(base_currency=exchange_rate_backend.base_currency, **{'rates': manual_rates}) - else: - return self.form_invalid(form) - - return self.form_valid(form) diff --git a/InvenTree/common/forms.py b/InvenTree/common/forms.py index bab7ede74c..f161c8cc01 100644 --- a/InvenTree/common/forms.py +++ b/InvenTree/common/forms.py @@ -13,6 +13,8 @@ from djmoney.forms.fields import MoneyField from InvenTree.forms import HelperForm from InvenTree.helpers import clean_decimal +from common.settings import currency_code_default + from .files import FileManager from .models import InvenTreeSetting @@ -182,7 +184,7 @@ class MatchItem(forms.Form): if 'price' in col_guess.lower(): self.fields[field_name] = MoneyField( label=_(col_guess), - default_currency=InvenTreeSetting.get_setting('INVENTREE_DEFAULT_CURRENCY'), + default_currency=currency_code_default(), decimal_places=5, max_digits=19, required=False, diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index 74c6c82b41..236e48770f 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -14,11 +14,12 @@ from django.db import models, transaction from django.db.utils import IntegrityError, OperationalError from django.conf import settings -import djmoney.settings from djmoney.models.fields import MoneyField from djmoney.contrib.exchange.models import convert_money from djmoney.contrib.exchange.exceptions import MissingRate +from common.settings import currency_code_default + from django.utils.translation import ugettext_lazy as _ from django.core.validators import MinValueValidator, URLValidator from django.core.exceptions import ValidationError @@ -80,20 +81,6 @@ class InvenTreeSetting(models.Model): 'default': '', }, - 'INVENTREE_DEFAULT_CURRENCY': { - 'name': _('Default Currency'), - 'description': _('Default currency'), - 'default': 'USD', - 'choices': djmoney.settings.CURRENCY_CHOICES, - }, - - 'CUSTOM_EXCHANGE_RATES': { - 'name': _('Custom Exchange Rates'), - 'description': _('Enable custom exchange rates'), - 'validator': bool, - 'default': False, - }, - 'INVENTREE_DOWNLOAD_FROM_URL': { 'name': _('Download from URL'), 'description': _('Allow download of remote images and files from external URL'), @@ -766,7 +753,7 @@ def get_price(instance, quantity, moq=True, multiples=True, currency=None): if currency is None: # Default currency selection - currency = InvenTreeSetting.get_setting('INVENTREE_DEFAULT_CURRENCY') + currency = currency_code_default() pb_min = None for pb in instance.price_breaks.all(): diff --git a/InvenTree/common/settings.py b/InvenTree/common/settings.py index 4d98bc495b..60265f4cb9 100644 --- a/InvenTree/common/settings.py +++ b/InvenTree/common/settings.py @@ -7,7 +7,8 @@ from __future__ import unicode_literals from moneyed import CURRENCIES -from common.models import InvenTreeSetting +import common.models +from django.conf import settings def currency_code_default(): @@ -15,7 +16,7 @@ def currency_code_default(): Returns the default currency code (or USD if not specified) """ - code = InvenTreeSetting.get_setting('INVENTREE_DEFAULT_CURRENCY') + code = settings.BASE_CURRENCY if code not in CURRENCIES: code = 'USD' @@ -28,4 +29,4 @@ def stock_expiry_enabled(): Returns True if the stock expiry feature is enabled """ - return InvenTreeSetting.get_setting('STOCK_ENABLE_EXPIRY') + return common.models.InvenTreeSetting.get_setting('STOCK_ENABLE_EXPIRY') diff --git a/InvenTree/common/test_views.py b/InvenTree/common/test_views.py index 8dc5830108..56a244ba0c 100644 --- a/InvenTree/common/test_views.py +++ b/InvenTree/common/test_views.py @@ -98,20 +98,15 @@ class SettingsViewTest(TestCase): Tests for a setting which has choices """ - setting = InvenTreeSetting.get_setting_object('INVENTREE_DEFAULT_CURRENCY') + setting = InvenTreeSetting.get_setting_object('PURCHASEORDER_REFERENCE_PREFIX') # Default value! - self.assertEqual(setting.value, 'USD') + self.assertEqual(setting.value, 'PO') url = self.get_url(setting.pk) # Try posting an invalid currency option - data, errors = self.post(url, {'value': 'XPQaaa'}, valid=False) - - self.assertIsNotNone(errors.get('value'), None) - - # Try posting a valid currency option - data, errors = self.post(url, {'value': 'AUD'}, valid=True) + data, errors = self.post(url, {'value': 'Purchase Order'}, valid=True) def test_binary_values(self): """ diff --git a/InvenTree/company/tests.py b/InvenTree/company/tests.py index 2c6e722440..b1e05efe14 100644 --- a/InvenTree/company/tests.py +++ b/InvenTree/company/tests.py @@ -11,9 +11,6 @@ from .models import Company, Contact, ManufacturerPart, SupplierPart from .models import rename_company_image from part.models import Part -from InvenTree.exchange import InvenTreeManualExchangeBackend -from djmoney.contrib.exchange.models import Rate - class CompanySimpleTest(TestCase): @@ -40,16 +37,6 @@ class CompanySimpleTest(TestCase): self.acme0002 = SupplierPart.objects.get(SKU='ACME0002') self.zerglphs = SupplierPart.objects.get(SKU='ZERGLPHS') self.zergm312 = SupplierPart.objects.get(SKU='ZERGM312') - - # Exchange rate backend - backend = InvenTreeManualExchangeBackend() - backend.update_rates(base_currency=backend.base_currency) - - Rate.objects.create( - currency='AUD', - value='1.35', - backend_id=backend.name, - ) def test_company_model(self): c = Company.objects.get(name='ABC Co.') diff --git a/InvenTree/config_template.yaml b/InvenTree/config_template.yaml index 87dfb6b545..1333b876b8 100644 --- a/InvenTree/config_template.yaml +++ b/InvenTree/config_template.yaml @@ -52,6 +52,9 @@ language: en-us # Use the environment variable INVENTREE_TIMEZONE timezone: UTC +# Base currency code +base_currency: USD + # List of currencies supported by default. # Add other currencies here to allow use in InvenTree currencies: diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 2fabdb9fc8..af817f1cf3 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -2675,7 +2675,7 @@ class PartSalePriceBreakCreate(AjaxCreateView): initials['part'] = self.get_part() - default_currency = InvenTreeSetting.get_setting('INVENTREE_DEFAULT_CURRENCY') + default_currency = settings.BASE_CURRENCY currency = CURRENCIES.get(default_currency, None) if currency is not None: diff --git a/InvenTree/templates/InvenTree/settings/currencies.html b/InvenTree/templates/InvenTree/settings/currencies.html index dd47bc6cdd..78598236f9 100644 --- a/InvenTree/templates/InvenTree/settings/currencies.html +++ b/InvenTree/templates/InvenTree/settings/currencies.html @@ -13,40 +13,43 @@ {% block settings %} - {% include "InvenTree/settings/header.html" %} - {% include "InvenTree/settings/setting.html" with key="INVENTREE_DEFAULT_CURRENCY" icon="fa-dollar-sign" %} - {% include "InvenTree/settings/setting.html" with key="CUSTOM_EXCHANGE_RATES" icon="fa-edit" %} + + + + + + + + {% for rate in rates %} + + + + + {% endfor %} + + + +
{% trans "Base Currency" %}{{ base_currency }}
{% trans "Exchange Rates" %}
{{ rate.currency }}{{ rate.value }}
+ {% trans "Last Update" %} + + {% if rates_updated %} + {{ rates_updated }} + {% else %} + {% trans "Never" %} + {% endif %} +
+
+ {% csrf_token %} + +
+
+
-
-
-

{% blocktrans with cur=default_currency %}Exchange Rates - Convert to {{cur}}{% endblocktrans %}

-
-
- -
-
- {% csrf_token %} - {% load crispy_forms_tags %} - {% crispy form %} - {% if custom_rates is False %} - - {% else %} - - {% endif %} -
-
- {% endblock %} {% block js_ready %} {{ block.super }} - -{% if api_rates_success is False %} - var alert_msg = {% blocktrans %}"Failed to refresh exchange rates" {% endblocktrans %}; - showAlertOrCache("alert-danger", alert_msg, null, 5000); -{% endif %} - {% endblock %} \ No newline at end of file diff --git a/InvenTree/templates/InvenTree/settings/tabs.html b/InvenTree/templates/InvenTree/settings/tabs.html index 360618fc34..86a88b68f0 100644 --- a/InvenTree/templates/InvenTree/settings/tabs.html +++ b/InvenTree/templates/InvenTree/settings/tabs.html @@ -15,6 +15,9 @@
  • {% trans "Global" %}
  • +
  • + {% trans "Currencies" %} +
  • {% trans "Report" %}
  • @@ -36,8 +39,5 @@
  • {% trans "Sales Orders" %}
  • -
  • - {% trans "Currencies" %} -
  • {% endif %}