From 34ded08ee717a961d5d7eaf70590f415416ca4a6 Mon Sep 17 00:00:00 2001
From: eeintech <eeintech@eeinte.ch>
Date: Wed, 19 May 2021 16:17:03 -0400
Subject: [PATCH 01/12] Added InvenTreeFixerExchangeBackend class

---
 InvenTree/InvenTree/exchange.py | 44 +++++++++++++++++++++++++++++++++
 InvenTree/InvenTree/settings.py |  2 +-
 InvenTree/InvenTree/tasks.py    | 26 ++++++++-----------
 InvenTree/config_template.yaml  |  7 ++++++
 4 files changed, 62 insertions(+), 17 deletions(-)

diff --git a/InvenTree/InvenTree/exchange.py b/InvenTree/InvenTree/exchange.py
index 06de4861ec..59d2883e2a 100644
--- a/InvenTree/InvenTree/exchange.py
+++ b/InvenTree/InvenTree/exchange.py
@@ -1,4 +1,10 @@
+from django.conf import settings as inventree_settings
+
+from djmoney import settings as djmoney_settings
 from djmoney.contrib.exchange.backends.base import BaseExchangeBackend
+from djmoney.contrib.exchange.backends import FixerBackend
+
+from common.models import InvenTreeSetting
 
 
 class InvenTreeManualExchangeBackend(BaseExchangeBackend):
@@ -19,3 +25,41 @@ class InvenTreeManualExchangeBackend(BaseExchangeBackend):
         """
 
         return {}
+
+
+class InvenTreeFixerExchangeBackend(FixerBackend):
+    """
+    Backend for updating currency exchange rates using Fixer.IO API
+    """
+
+    def get_api_key(self):
+        """ Get API key from global settings """
+
+        fixer_api_key = InvenTreeSetting.get_setting('INVENTREE_FIXER_API_KEY', '').strip()
+
+        if not fixer_api_key:
+            # API key not provided
+            return None
+
+        return fixer_api_key
+
+    def __init__(self):
+        """ Override FixerBackend init to get access_key from global settings """
+
+        fixer_api_key = self.get_api_key()
+
+        super().__init__(url=djmoney_settings.FIXER_URL, access_key=fixer_api_key)
+
+    def update_rates(self):
+        """ Override update_rates method using currencies found in the settings """
+
+        currencies = ','.join(inventree_settings.CURRENCIES)
+
+        base = inventree_settings.BASE_CURRENCY
+
+        super().update_rates(base_currency=base, symbols=currencies)
+
+    def get_rates(self, **kwargs):
+        """ Returns a mapping <currency>: <rate> """
+
+        return {}
diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py
index 5cf0b0c544..e99c3a3eea 100644
--- a/InvenTree/InvenTree/settings.py
+++ b/InvenTree/InvenTree/settings.py
@@ -515,7 +515,7 @@ CURRENCIES = CONFIG.get(
 
 BASE_CURRENCY = CONFIG.get('base_currency', 'USD')
 
-EXCHANGE_BACKEND = 'InvenTree.exchange.InvenTreeManualExchangeBackend'
+EXCHANGE_BACKEND = 'InvenTree.exchange.' + CONFIG.get('exchange_backend', 'InvenTreeManualExchangeBackend')
 
 # 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 b81b4e6de4..0076bb9544 100644
--- a/InvenTree/InvenTree/tasks.py
+++ b/InvenTree/InvenTree/tasks.py
@@ -163,30 +163,24 @@ def check_for_updates():
 
 def update_exchange_rates():
     """
-    If an API key for fixer.io has been provided, attempt to update currency exchange rates
+    Update backend rates
     """
 
     try:
-        import common.models
+        from .exchange import InvenTreeManualExchangeBackend, InvenTreeFixerExchangeBackend
         from django.conf import settings
-        from djmoney.contrib.exchange.backends import FixerBackend
     except AppRegistryNotReady:
         # Apps not yet loaded!
         return
+    
+    # Get backend
+    if 'InvenTreeManualExchangeBackend' in settings.EXCHANGE_BACKEND:
+        backend = InvenTreeFixerExchangeBackend()
+    else:
+        backend = InvenTreeManualExchangeBackend()
 
-    fixer_api_key = common.models.InvenTreeSetting.get_setting('INVENTREE_FIXER_API_KEY', '').strip()
-
-    if not fixer_api_key:
-        # API key not provided
-        return
-
-    backend = FixerBackend(access_key=fixer_api_key)
-
-    currencies = ','.join(settings.CURRENCIES)
-
-    base = settings.BASE_CURRENCY
-
-    backend.update_rates(base_currency=base, symbols=currencies)
+    # Update rates
+    backend.update_rates()
 
 
 def send_email(subject, body, recipients, from_email=None):
diff --git a/InvenTree/config_template.yaml b/InvenTree/config_template.yaml
index 87dfb6b545..63525b0d79 100644
--- a/InvenTree/config_template.yaml
+++ b/InvenTree/config_template.yaml
@@ -62,6 +62,13 @@ currencies:
   - JPY
   - NZD
   - USD
+# Define base currency (can also be defined in the global settings)
+# base_currency: USD
+# Define exchange backend
+# Choices are:
+# - InvenTreeManualExchangeBackend
+# - InvenTreeFixerExchangeBackend
+exchange_backend: InvenTreeFixerExchangeBackend
 
 # Email backend configuration
 # Ref: https://docs.djangoproject.com/en/dev/topics/email/

From 6d5b2d3227f1bf0a92971e473ab226374e8359a3 Mon Sep 17 00:00:00 2001
From: eeintech <eeintech@eeinte.ch>
Date: Wed, 19 May 2021 17:06:41 -0400
Subject: [PATCH 02/12] Added 'Currencies' settings view in global settings

---
 InvenTree/InvenTree/tasks.py                  |  4 +-
 InvenTree/InvenTree/urls.py                   |  3 ++
 InvenTree/InvenTree/views.py                  | 15 +++++++
 .../InvenTree/settings/currencies.html        | 43 +++++++++++++++++++
 .../templates/InvenTree/settings/global.html  |  2 -
 .../templates/InvenTree/settings/tabs.html    |  3 ++
 6 files changed, 66 insertions(+), 4 deletions(-)
 create mode 100644 InvenTree/templates/InvenTree/settings/currencies.html

diff --git a/InvenTree/InvenTree/tasks.py b/InvenTree/InvenTree/tasks.py
index 0076bb9544..fb6a45f368 100644
--- a/InvenTree/InvenTree/tasks.py
+++ b/InvenTree/InvenTree/tasks.py
@@ -175,9 +175,9 @@ def update_exchange_rates():
     
     # Get backend
     if 'InvenTreeManualExchangeBackend' in settings.EXCHANGE_BACKEND:
-        backend = InvenTreeFixerExchangeBackend()
-    else:
         backend = InvenTreeManualExchangeBackend()
+    else:
+        backend = InvenTreeFixerExchangeBackend()
 
     # Update rates
     backend.update_rates()
diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py
index ab2ced7d5e..d3132ca2a8 100644
--- a/InvenTree/InvenTree/urls.py
+++ b/InvenTree/InvenTree/urls.py
@@ -41,6 +41,7 @@ from .views import IndexView, SearchView, DatabaseStatsView
 from .views import SettingsView, EditUserView, SetPasswordView
 from .views import AppearanceSelectView, SettingCategorySelectView
 from .views import DynamicJsView
+from .views import ExchangeRatesView
 
 from common.views import SettingEdit
 
@@ -90,6 +91,8 @@ settings_urls = [
     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/?', SettingsView.as_view(template_name='InvenTree/settings/currencies.html'), name='settings-currencies'),
+    url(r'^echange-rates/?', ExchangeRatesView.as_view(), name='refresh-exchange-rates'),
 
     url(r'^(?P<pk>\d+)/edit/', SettingEdit.as_view(), name='setting-edit'),
 
diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py
index d285efae36..bd03a18dda 100644
--- a/InvenTree/InvenTree/views.py
+++ b/InvenTree/InvenTree/views.py
@@ -27,6 +27,7 @@ from users.models import check_user_role, RuleSet
 from .forms import DeleteForm, EditUserForm, SetPasswordForm
 from .forms import ColorThemeSelectForm, SettingCategorySelectForm
 from .helpers import str2bool
+from .tasks import update_exchange_rates
 
 from rest_framework import views
 
@@ -908,3 +909,17 @@ class DatabaseStatsView(AjaxView):
         """
 
         return ctx
+
+
+class ExchangeRatesView(SettingsView):
+
+    success_url = reverse_lazy('settings-currencies')
+
+    def post(self, request, *args, **kwargs):
+
+        # Process exchange rates
+        update_exchange_rates()
+
+        # TODO: Update context
+
+        return HttpResponseRedirect(self.success_url)
diff --git a/InvenTree/templates/InvenTree/settings/currencies.html b/InvenTree/templates/InvenTree/settings/currencies.html
new file mode 100644
index 0000000000..9ec5b518fb
--- /dev/null
+++ b/InvenTree/templates/InvenTree/settings/currencies.html
@@ -0,0 +1,43 @@
+{% extends "InvenTree/settings/settings.html" %}
+{% load i18n %}
+{% load inventree_extras %}
+
+{% block tabs %}
+{% include "InvenTree/settings/tabs.html" with tab='currencies' %}
+{% endblock %}
+
+{% block subtitle %}
+{% trans "Currency Settings" %}
+{% endblock %}
+
+{% block settings %}
+
+<table class='table table-striped table-condensed'>
+    {% include "InvenTree/settings/header.html" %}
+    <tbody>
+        {% include "InvenTree/settings/setting.html" with key="INVENTREE_DEFAULT_CURRENCY" icon="fa-dollar-sign" %}
+        {% include "InvenTree/settings/setting.html" with key="INVENTREE_FIXER_API_KEY" icon="fa-key" %}
+    </tbody>
+</table>
+
+<div class='row'>
+    <div class='col-sm-6'>
+        <h4>{% trans "Exchange Rates" %}</h4>
+    </div>
+</div>
+
+<table class='table table-striped table-condensed' id='exchange-rates'>
+</table>
+
+<form action="{% url 'refresh-exchange-rates' %}" method="post">
+    {% csrf_token %}
+    <button type="submit" class='btn btn-primary'>{% trans "Refresh Exchange Rates" %}</button>
+</form>
+
+{% endblock %}
+
+{% block js_ready %}
+{{ block.super }}
+{% comment %} TODO: Update exchange-rates table! {% endcomment %}
+{% comment %} Or do it using context instead of JS? {% endcomment %}
+{% endblock %}
\ No newline at end of file
diff --git a/InvenTree/templates/InvenTree/settings/global.html b/InvenTree/templates/InvenTree/settings/global.html
index 5c5dccfb2a..fd91dafc67 100644
--- a/InvenTree/templates/InvenTree/settings/global.html
+++ b/InvenTree/templates/InvenTree/settings/global.html
@@ -19,8 +19,6 @@
         {% include "InvenTree/settings/setting.html" with key="INVENTREE_INSTANCE_TITLE" icon="fa-info-circle" %}
         {% include "InvenTree/settings/setting.html" with key="INVENTREE_BASE_URL" icon="fa-globe" %}
         {% include "InvenTree/settings/setting.html" with key="INVENTREE_COMPANY_NAME" icon="fa-building" %}
-        {% include "InvenTree/settings/setting.html" with key="INVENTREE_DEFAULT_CURRENCY" icon="fa-dollar-sign" %}
-        {% include "InvenTree/settings/setting.html" with key="INVENTREE_FIXER_API_KEY" icon="fa-key" %}
         {% include "InvenTree/settings/setting.html" with key="INVENTREE_DOWNLOAD_FROM_URL" icon="fa-cloud-download-alt" %}
     </tbody>
 </table>
diff --git a/InvenTree/templates/InvenTree/settings/tabs.html b/InvenTree/templates/InvenTree/settings/tabs.html
index 3f8be0a313..360618fc34 100644
--- a/InvenTree/templates/InvenTree/settings/tabs.html
+++ b/InvenTree/templates/InvenTree/settings/tabs.html
@@ -36,5 +36,8 @@
     <li {% if tab == 'so' %} class='active'{% endif %}>
         <a href="{% url 'settings-so' %}"><span class='fas fa-truck'></span> {% trans "Sales Orders" %}</a>
     </li>
+    <li {% if tab == 'currencies' %} class='active'{% endif %}>
+        <a href="{% url 'settings-currencies' %}"><span class='fas fa-dollar-sign'></span> {% trans "Currencies" %}</a>
+    </li>
 </ul>
 {% endif %}

From bed6a7e49c11f2cbb8ad4eb12ef1e6a16f16e616 Mon Sep 17 00:00:00 2001
From: eeintech <eeintech@eeinte.ch>
Date: Thu, 20 May 2021 09:49:56 -0400
Subject: [PATCH 03/12] Added exchange rates form

---
 InvenTree/InvenTree/exchange.py               | 61 +++++++++++++------
 InvenTree/InvenTree/forms.py                  | 36 ++++++++++-
 InvenTree/InvenTree/tasks.py                  | 10 +--
 InvenTree/InvenTree/urls.py                   |  5 +-
 InvenTree/InvenTree/views.py                  | 37 ++++++++++-
 .../InvenTree/settings/currencies.html        | 19 +++---
 6 files changed, 125 insertions(+), 43 deletions(-)

diff --git a/InvenTree/InvenTree/exchange.py b/InvenTree/InvenTree/exchange.py
index 59d2883e2a..03891828a0 100644
--- a/InvenTree/InvenTree/exchange.py
+++ b/InvenTree/InvenTree/exchange.py
@@ -1,12 +1,21 @@
+from django.core.exceptions import ImproperlyConfigured
 from django.conf import settings as inventree_settings
 
 from djmoney import settings as djmoney_settings
 from djmoney.contrib.exchange.backends.base import BaseExchangeBackend
-from djmoney.contrib.exchange.backends import FixerBackend
 
 from common.models import InvenTreeSetting
 
 
+def get_exchange_rate_backend():
+    """ Return the exchange rate backend set by user """
+
+    if 'InvenTreeManualExchangeBackend' in inventree_settings.EXCHANGE_BACKEND:
+        return InvenTreeManualExchangeBackend()
+    else:
+        return InvenTreeFixerExchangeBackend()
+
+
 class InvenTreeManualExchangeBackend(BaseExchangeBackend):
     """
     Backend for manually updating currency exchange rates
@@ -16,22 +25,39 @@ class InvenTreeManualExchangeBackend(BaseExchangeBackend):
     Specifically: https://github.com/django-money/django-money/tree/master/djmoney/contrib/exchange/backends
     """
 
-    name = "inventree"
+    name = 'inventree'
     url = None
+    default_currency = None
+    currencies = []
+
+    def update_default_currency(self):
+
+        self.default_currency = InvenTreeSetting.get_setting('INVENTREE_DEFAULT_CURRENCY', inventree_settings.BASE_CURRENCY)
+
+    def __init__(self, url=None):
+
+        self.url = url
+
+        self.update_default_currency()
+
+        self.currencies = inventree_settings.CURRENCIES
+
+        super().__init__()
 
     def get_rates(self, **kwargs):
-        """
-        Do not get any rates...
-        """
+        """ Returns a mapping <currency>: <rate> """
 
         return {}
 
 
-class InvenTreeFixerExchangeBackend(FixerBackend):
+class InvenTreeFixerExchangeBackend(InvenTreeManualExchangeBackend):
     """
     Backend for updating currency exchange rates using Fixer.IO API
     """
 
+    name = 'fixer.io'
+    access_key = None
+
     def get_api_key(self):
         """ Get API key from global settings """
 
@@ -48,18 +74,17 @@ class InvenTreeFixerExchangeBackend(FixerBackend):
 
         fixer_api_key = self.get_api_key()
 
-        super().__init__(url=djmoney_settings.FIXER_URL, access_key=fixer_api_key)
+        if fixer_api_key is None:
+            raise ImproperlyConfigured("fixer.io API key is needed to use InvenTreeFixerExchangeBackend")
+        
+        self.access_key = fixer_api_key
+
+        super().__init__(url=djmoney_settings.FIXER_URL)
 
     def update_rates(self):
-        """ Override update_rates method using currencies found in the settings """
+        """ Override update_rates method using currencies found in the settings
+        """
+        
+        symbols = ','.join(self.currencies)
 
-        currencies = ','.join(inventree_settings.CURRENCIES)
-
-        base = inventree_settings.BASE_CURRENCY
-
-        super().update_rates(base_currency=base, symbols=currencies)
-
-    def get_rates(self, **kwargs):
-        """ Returns a mapping <currency>: <rate> """
-
-        return {}
+        super().update_rates(base_currency=self.base_currency, symbols=symbols)
diff --git a/InvenTree/InvenTree/forms.py b/InvenTree/InvenTree/forms.py
index 52d1c8758f..a744671afb 100644
--- a/InvenTree/InvenTree/forms.py
+++ b/InvenTree/InvenTree/forms.py
@@ -7,12 +7,15 @@ from __future__ import unicode_literals
 
 from django.utils.translation import ugettext_lazy as _
 from django import forms
+from django.contrib.auth.models import User
+
 from crispy_forms.helper import FormHelper
 from crispy_forms.layout import Layout, Field
 from crispy_forms.bootstrap import PrependedText, AppendedText, PrependedAppendedText, StrictButton, Div
-from django.contrib.auth.models import User
+
 from common.models import ColorTheme
 from part.models import PartCategory
+from .exchange import InvenTreeManualExchangeBackend
 
 
 class HelperForm(forms.ModelForm):
@@ -236,3 +239,34 @@ 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.default_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',
+                        'type': 'number',
+                        'min': '0',
+                        'step': 'any',
+                        'value': '',
+                    })
+                )
diff --git a/InvenTree/InvenTree/tasks.py b/InvenTree/InvenTree/tasks.py
index fb6a45f368..0468365dae 100644
--- a/InvenTree/InvenTree/tasks.py
+++ b/InvenTree/InvenTree/tasks.py
@@ -167,17 +167,13 @@ def update_exchange_rates():
     """
 
     try:
-        from .exchange import InvenTreeManualExchangeBackend, InvenTreeFixerExchangeBackend
-        from django.conf import settings
+        from .exchange import get_exchange_rate_backend
     except AppRegistryNotReady:
         # Apps not yet loaded!
         return
     
-    # Get backend
-    if 'InvenTreeManualExchangeBackend' in settings.EXCHANGE_BACKEND:
-        backend = InvenTreeManualExchangeBackend()
-    else:
-        backend = InvenTreeFixerExchangeBackend()
+    # Get exchange rate backend
+    backend = get_exchange_rate_backend()
 
     # Update rates
     backend.update_rates()
diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py
index d3132ca2a8..d297dc18ad 100644
--- a/InvenTree/InvenTree/urls.py
+++ b/InvenTree/InvenTree/urls.py
@@ -41,7 +41,7 @@ from .views import IndexView, SearchView, DatabaseStatsView
 from .views import SettingsView, EditUserView, SetPasswordView
 from .views import AppearanceSelectView, SettingCategorySelectView
 from .views import DynamicJsView
-from .views import ExchangeRatesView
+from .views import CurrencySettingsView
 
 from common.views import SettingEdit
 
@@ -91,8 +91,7 @@ settings_urls = [
     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/?', SettingsView.as_view(template_name='InvenTree/settings/currencies.html'), name='settings-currencies'),
-    url(r'^echange-rates/?', ExchangeRatesView.as_view(), name='refresh-exchange-rates'),
+    url(r'^currencies/?', CurrencySettingsView.as_view(), name='settings-currencies'),
 
     url(r'^(?P<pk>\d+)/edit/', SettingEdit.as_view(), name='setting-edit'),
 
diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py
index bd03a18dda..50f4a095e8 100644
--- a/InvenTree/InvenTree/views.py
+++ b/InvenTree/InvenTree/views.py
@@ -26,8 +26,9 @@ from users.models import check_user_role, RuleSet
 
 from .forms import DeleteForm, EditUserForm, SetPasswordForm
 from .forms import ColorThemeSelectForm, SettingCategorySelectForm
+from .forms import SettingExchangeRatesForm
 from .helpers import str2bool
-from .tasks import update_exchange_rates
+from .exchange import get_exchange_rate_backend
 
 from rest_framework import views
 
@@ -911,14 +912,44 @@ class DatabaseStatsView(AjaxView):
         return ctx
 
 
-class ExchangeRatesView(SettingsView):
+class CurrencySettingsView(FormView):
 
+    form_class = SettingExchangeRatesForm
+    template_name = 'InvenTree/settings/currencies.html'
     success_url = reverse_lazy('settings-currencies')
 
+    def get_context_data(self, **kwargs):
+
+        context = super().get_context_data(**kwargs)
+
+        # Get exchange rate backend
+        exchange_rate_backend = get_exchange_rate_backend()
+
+        context['exchange_backend'] = exchange_rate_backend.name
+
+        return context
+
+    def get_form(self):
+
+        form = super().get_form()
+
+        # Get exchange rate backend
+        exchange_rate_backend = get_exchange_rate_backend()
+
+        if exchange_rate_backend.name == 'fixer.io':
+            # Disable all the fields
+            for field in form.fields:
+                form.fields[field].disabled = True
+
+        return form
+
     def post(self, request, *args, **kwargs):
 
+        # Get exchange rate backend
+        exchange_rate_backend = get_exchange_rate_backend()
+
         # Process exchange rates
-        update_exchange_rates()
+        exchange_rate_backend.update_rates()
 
         # TODO: Update context
 
diff --git a/InvenTree/templates/InvenTree/settings/currencies.html b/InvenTree/templates/InvenTree/settings/currencies.html
index 9ec5b518fb..52d6558bad 100644
--- a/InvenTree/templates/InvenTree/settings/currencies.html
+++ b/InvenTree/templates/InvenTree/settings/currencies.html
@@ -26,18 +26,15 @@
     </div>
 </div>
 
-<table class='table table-striped table-condensed' id='exchange-rates'>
-</table>
-
-<form action="{% url 'refresh-exchange-rates' %}" method="post">
+<form action="{% url 'settings-currencies' %}" method="post">
     {% csrf_token %}
-    <button type="submit" class='btn btn-primary'>{% trans "Refresh Exchange Rates" %}</button>
+    {% load crispy_forms_tags %}
+    {% crispy form %}
+    {% if exchange_backend == 'fixer.io' %}
+        <button type="submit" class='btn btn-primary'>{% trans "Refresh Exchange Rates" %}</button>
+    {% else %}
+        <button type="submit" class='btn btn-primary'>{% trans "Update Exchange Rates" %}</button>
+    {% endif %}
 </form>
 
 {% endblock %}
-
-{% block js_ready %}
-{{ block.super }}
-{% comment %} TODO: Update exchange-rates table! {% endcomment %}
-{% comment %} Or do it using context instead of JS? {% endcomment %}
-{% endblock %}
\ No newline at end of file

From 747b0554e1f92323a3da20e82a3415cafc74a639 Mon Sep 17 00:00:00 2001
From: eeintech <eeintech@eeinte.ch>
Date: Thu, 20 May 2021 13:45:26 -0400
Subject: [PATCH 04/12] Ready for review

---
 InvenTree/InvenTree/exchange.py               | 75 +++++++++++++++----
 InvenTree/InvenTree/forms.py                  |  6 +-
 InvenTree/InvenTree/helpers.py                | 18 ++++-
 InvenTree/InvenTree/views.py                  | 61 ++++++++++++---
 InvenTree/common/forms.py                     | 22 +-----
 .../InvenTree/settings/currencies.html        | 14 +++-
 6 files changed, 148 insertions(+), 48 deletions(-)

diff --git a/InvenTree/InvenTree/exchange.py b/InvenTree/InvenTree/exchange.py
index 03891828a0..ce1db06711 100644
--- a/InvenTree/InvenTree/exchange.py
+++ b/InvenTree/InvenTree/exchange.py
@@ -3,6 +3,7 @@ from django.conf import settings as inventree_settings
 
 from djmoney import settings as djmoney_settings
 from djmoney.contrib.exchange.backends.base import BaseExchangeBackend
+from djmoney.contrib.exchange.models import Rate
 
 from common.models import InvenTreeSetting
 
@@ -27,19 +28,24 @@ class InvenTreeManualExchangeBackend(BaseExchangeBackend):
 
     name = 'inventree'
     url = None
-    default_currency = None
+    base_currency = None
     currencies = []
 
     def update_default_currency(self):
+        """ Update to base currency """
 
-        self.default_currency = InvenTreeSetting.get_setting('INVENTREE_DEFAULT_CURRENCY', inventree_settings.BASE_CURRENCY)
+        self.base_currency = InvenTreeSetting.get_setting('INVENTREE_DEFAULT_CURRENCY', inventree_settings.BASE_CURRENCY)
 
     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__()
@@ -47,7 +53,22 @@ class InvenTreeManualExchangeBackend(BaseExchangeBackend):
     def get_rates(self, **kwargs):
         """ Returns a mapping <currency>: <rate> """
 
-        return {}
+        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 InvenTreeFixerExchangeBackend(InvenTreeManualExchangeBackend):
@@ -55,7 +76,7 @@ class InvenTreeFixerExchangeBackend(InvenTreeManualExchangeBackend):
     Backend for updating currency exchange rates using Fixer.IO API
     """
 
-    name = 'fixer.io'
+    name = 'fixer'
     access_key = None
 
     def get_api_key(self):
@@ -67,24 +88,48 @@ class InvenTreeFixerExchangeBackend(InvenTreeManualExchangeBackend):
             # API key not provided
             return None
 
-        return fixer_api_key
-
-    def __init__(self):
-        """ Override FixerBackend init to get access_key from global settings """
-
-        fixer_api_key = self.get_api_key()
-
-        if fixer_api_key is None:
-            raise ImproperlyConfigured("fixer.io API key is needed to use InvenTreeFixerExchangeBackend")
-        
         self.access_key = fixer_api_key
 
+    def __init__(self):
+        """ Override init to get access_key from global settings """
+
+        self.get_api_key()
+
+        if self.access_key is None:
+            raise ImproperlyConfigured("fixer.io API key is needed to use InvenTreeFixerExchangeBackend")
+        
         super().__init__(url=djmoney_settings.FIXER_URL)
 
-    def update_rates(self):
+    def get_params(self):
+        """ Returns parameters (access key) """
+
+        return {"access_key": self.access_key}
+
+    def update_rates(self, base_currency=None):
         """ Override update_rates method using currencies found in the settings
         """
+
+        if base_currency:
+            self.base_currency = base_currency
+        else:
+            self.update_default_currency()
         
         symbols = ','.join(self.currencies)
 
         super().update_rates(base_currency=self.base_currency, symbols=symbols)
+
+    def get_rates(self, **params):
+        """ Returns a mapping <currency>: <rate> """
+
+        # 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 {}
diff --git a/InvenTree/InvenTree/forms.py b/InvenTree/InvenTree/forms.py
index a744671afb..488e982ddc 100644
--- a/InvenTree/InvenTree/forms.py
+++ b/InvenTree/InvenTree/forms.py
@@ -15,6 +15,7 @@ from crispy_forms.bootstrap import PrependedText, AppendedText, PrependedAppende
 
 from common.models import ColorTheme
 from part.models import PartCategory
+
 from .exchange import InvenTreeManualExchangeBackend
 
 
@@ -254,7 +255,7 @@ class SettingExchangeRatesForm(forms.Form):
         exchange_rate_backend.update_default_currency()
 
         for currency in exchange_rate_backend.currencies:
-            if currency != exchange_rate_backend.default_currency:
+            if currency != exchange_rate_backend.base_currency:
                 # Set field name
                 field_name = currency
                 # Set field input box
@@ -264,9 +265,10 @@ class SettingExchangeRatesForm(forms.Form):
                     widget=forms.NumberInput(attrs={
                         'name': field_name,
                         'class': 'numberinput',
+                        'style': 'width: 200px;',
                         'type': 'number',
                         'min': '0',
                         'step': 'any',
-                        'value': '',
+                        'value': 0,
                     })
                 )
diff --git a/InvenTree/InvenTree/helpers.py b/InvenTree/InvenTree/helpers.py
index 1097c5663b..9d00697230 100644
--- a/InvenTree/InvenTree/helpers.py
+++ b/InvenTree/InvenTree/helpers.py
@@ -8,7 +8,7 @@ import json
 import os.path
 from PIL import Image
 
-from decimal import Decimal
+from decimal import Decimal, InvalidOperation
 
 from wsgiref.util import FileWrapper
 from django.http import StreamingHttpResponse
@@ -606,3 +606,19 @@ def getNewestMigrationFile(app, exclude_extension=True):
         newest_file = newest_file.replace('.py', '')
 
     return newest_file
+
+
+def clean_decimal(number):
+    """ Clean-up decimal value """
+
+    # Check if empty
+    if number is None or number == '':
+        return Decimal(0)
+
+    # Check if decimal type
+    try:
+        clean_number = Decimal(number)
+    except InvalidOperation:
+        clean_number = number
+
+    return clean_number.quantize(Decimal(1)) if clean_number == clean_number.to_integral() else clean_number.normalize()
diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py
index 50f4a095e8..48e42dd890 100644
--- a/InvenTree/InvenTree/views.py
+++ b/InvenTree/InvenTree/views.py
@@ -23,6 +23,7 @@ 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
 
 from .forms import DeleteForm, EditUserForm, SetPasswordForm
 from .forms import ColorThemeSelectForm, SettingCategorySelectForm
@@ -918,12 +919,30 @@ class CurrencySettingsView(FormView):
     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 = get_exchange_rate_backend()
+        exchange_rate_backend = self.get_exchange_rate_backend()
+
+        context['default_currency'] = exchange_rate_backend.base_currency
 
         context['exchange_backend'] = exchange_rate_backend.name
 
@@ -934,23 +953,45 @@ class CurrencySettingsView(FormView):
         form = super().get_form()
 
         # Get exchange rate backend
-        exchange_rate_backend = get_exchange_rate_backend()
+        exchange_rate_backend = self.get_exchange_rate_backend()
 
-        if exchange_rate_backend.name == 'fixer.io':
-            # Disable all the fields
-            for field in form.fields:
+        # Get stored exchange rates
+        stored_rates = exchange_rate_backend.get_stored_rates()
+            
+        for field in form.fields:
+            if 'fixer' in exchange_rate_backend.name:
+                # 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 = get_exchange_rate_backend()
+        exchange_rate_backend = self.get_exchange_rate_backend()
 
-        # Process exchange rates
-        exchange_rate_backend.update_rates()
+        if 'fixer' in exchange_rate_backend.name:
+            # 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 = {}
 
-        # TODO: Update context
+            if form.is_valid():
+                for field, value in form.cleaned_data.items():
+                    manual_rates[field] = clean_decimal(value)
 
-        return HttpResponseRedirect(self.success_url)
+                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 8a0017e38b..bab7ede74c 100644
--- a/InvenTree/common/forms.py
+++ b/InvenTree/common/forms.py
@@ -5,14 +5,13 @@ Django forms for interacting with common objects
 # -*- coding: utf-8 -*-
 from __future__ import unicode_literals
 
-from decimal import Decimal, InvalidOperation
-
 from django import forms
 from django.utils.translation import gettext as _
 
 from djmoney.forms.fields import MoneyField
 
 from InvenTree.forms import HelperForm
+from InvenTree.helpers import clean_decimal
 
 from .files import FileManager
 from .models import InvenTreeSetting
@@ -119,21 +118,6 @@ class MatchItem(forms.Form):
 
         super().__init__(*args, **kwargs)
 
-        def clean(number):
-            """ Clean-up decimal value """
-
-            # Check if empty
-            if not number:
-                return number
-
-            # Check if decimal type
-            try:
-                clean_number = Decimal(number)
-            except InvalidOperation:
-                clean_number = number
-
-            return clean_number.quantize(Decimal(1)) if clean_number == clean_number.to_integral() else clean_number.normalize()
-
         # Setup FileManager
         file_manager.setup()
 
@@ -160,7 +144,7 @@ class MatchItem(forms.Form):
                                     'type': 'number',
                                     'min': '0',
                                     'step': 'any',
-                                    'value': clean(row.get('quantity', '')),
+                                    'value': clean_decimal(row.get('quantity', '')),
                                 })
                             )
 
@@ -202,7 +186,7 @@ class MatchItem(forms.Form):
                                 decimal_places=5,
                                 max_digits=19,
                                 required=False,
-                                default_amount=clean(value),
+                                default_amount=clean_decimal(value),
                             )
                         else:
                             self.fields[field_name] = forms.CharField(
diff --git a/InvenTree/templates/InvenTree/settings/currencies.html b/InvenTree/templates/InvenTree/settings/currencies.html
index 52d6558bad..10b5709238 100644
--- a/InvenTree/templates/InvenTree/settings/currencies.html
+++ b/InvenTree/templates/InvenTree/settings/currencies.html
@@ -22,11 +22,12 @@
 
 <div class='row'>
     <div class='col-sm-6'>
-        <h4>{% trans "Exchange Rates" %}</h4>
+        <h4>{% trans "Exchange Rates - Convert to " %}{{ default_currency }}</h4>
     </div>
 </div>
 
 <form action="{% url 'settings-currencies' %}" method="post">
+    <div id='exchange_rate_form'>
     {% csrf_token %}
     {% load crispy_forms_tags %}
     {% crispy form %}
@@ -35,6 +36,17 @@
     {% else %}
         <button type="submit" class='btn btn-primary'>{% trans "Update Exchange Rates" %}</button>
     {% endif %}
+    </div>
 </form>
 
 {% endblock %}
+
+{% block js_ready %}
+{{ block.super }}
+
+{% if api_rates_success is False %}
+    var alert_msg = {% blocktrans %}"Failed to refresh exchange rates. Verify your API key and/or subscription plan" {% endblocktrans %};
+    showAlertOrCache("alert-danger", alert_msg, null, 5000);
+{% endif %}
+
+{% endblock %}
\ No newline at end of file

From 27799b43b236525b818fa32d2d4c66b57e0fa96b Mon Sep 17 00:00:00 2001
From: eeintech <eeintech@eeinte.ch>
Date: Thu, 20 May 2021 13:51:54 -0400
Subject: [PATCH 05/12] Template fix/improvement

---
 InvenTree/templates/InvenTree/settings/currencies.html | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/InvenTree/templates/InvenTree/settings/currencies.html b/InvenTree/templates/InvenTree/settings/currencies.html
index 10b5709238..fa9d234988 100644
--- a/InvenTree/templates/InvenTree/settings/currencies.html
+++ b/InvenTree/templates/InvenTree/settings/currencies.html
@@ -16,7 +16,9 @@
     {% include "InvenTree/settings/header.html" %}
     <tbody>
         {% include "InvenTree/settings/setting.html" with key="INVENTREE_DEFAULT_CURRENCY" icon="fa-dollar-sign" %}
+        {% if 'fixer' in exchange_backend %}
         {% include "InvenTree/settings/setting.html" with key="INVENTREE_FIXER_API_KEY" icon="fa-key" %}
+        {% endif %}
     </tbody>
 </table>
 
@@ -31,7 +33,7 @@
     {% csrf_token %}
     {% load crispy_forms_tags %}
     {% crispy form %}
-    {% if exchange_backend == 'fixer.io' %}
+    {% if 'fixer' in exchange_backend %}
         <button type="submit" class='btn btn-primary'>{% trans "Refresh Exchange Rates" %}</button>
     {% else %}
         <button type="submit" class='btn btn-primary'>{% trans "Update Exchange Rates" %}</button>

From bbd95f2c7059255b6217d01ea0c338b96deb8eae Mon Sep 17 00:00:00 2001
From: eeintech <eeintech@eeinte.ch>
Date: Thu, 20 May 2021 14:52:56 -0400
Subject: [PATCH 06/12] Fixed exchange rate backend test

---
 InvenTree/InvenTree/tasks.py |  2 +-
 InvenTree/company/tests.py   | 10 ++++++----
 2 files changed, 7 insertions(+), 5 deletions(-)

diff --git a/InvenTree/InvenTree/tasks.py b/InvenTree/InvenTree/tasks.py
index 0468365dae..b3649bcdcf 100644
--- a/InvenTree/InvenTree/tasks.py
+++ b/InvenTree/InvenTree/tasks.py
@@ -176,7 +176,7 @@ def update_exchange_rates():
     backend = get_exchange_rate_backend()
 
     # Update rates
-    backend.update_rates()
+    backend.update_rates(base_currency=backend.base_currency)
 
 
 def send_email(subject, body, recipients, from_email=None):
diff --git a/InvenTree/company/tests.py b/InvenTree/company/tests.py
index 5dd3bf81ab..e07516e180 100644
--- a/InvenTree/company/tests.py
+++ b/InvenTree/company/tests.py
@@ -11,7 +11,7 @@ from .models import Company, Contact, ManufacturerPart, SupplierPart
 from .models import rename_company_image
 from part.models import Part
 
-from InvenTree.exchange import InvenTreeManualExchangeBackend
+from InvenTree.exchange import get_exchange_rate_backend
 from djmoney.contrib.exchange.models import Rate
 
 
@@ -40,13 +40,15 @@ class CompanySimpleTest(TestCase):
         self.acme0002 = SupplierPart.objects.get(SKU='ACME0002')
         self.zerglphs = SupplierPart.objects.get(SKU='ZERGLPHS')
         self.zergm312 = SupplierPart.objects.get(SKU='ZERGM312')
-
-        InvenTreeManualExchangeBackend().update_rates()
+        
+        # Exchange rate backend
+        backend = get_exchange_rate_backend()
+        backend.update_rates(base_currency=backend.base_currency)
 
         Rate.objects.create(
             currency='AUD',
             value='1.35',
-            backend_id='inventree',
+            backend_id=backend.name,
         )
 
     def test_company_model(self):

From be3f37f28f286fcf05ef6b24af6526a6835331ed Mon Sep 17 00:00:00 2001
From: eeintech <eeintech@eeinte.ch>
Date: Thu, 20 May 2021 15:22:18 -0400
Subject: [PATCH 07/12] Updated default currency backend

---
 InvenTree/config_template.yaml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/InvenTree/config_template.yaml b/InvenTree/config_template.yaml
index 63525b0d79..7d301934f0 100644
--- a/InvenTree/config_template.yaml
+++ b/InvenTree/config_template.yaml
@@ -68,7 +68,7 @@ currencies:
 # Choices are:
 # - InvenTreeManualExchangeBackend
 # - InvenTreeFixerExchangeBackend
-exchange_backend: InvenTreeFixerExchangeBackend
+exchange_backend: InvenTreeManualExchangeBackend
 
 # Email backend configuration
 # Ref: https://docs.djangoproject.com/en/dev/topics/email/

From 53ce848145c3137581ce0b8ea7b6315e82165b74 Mon Sep 17 00:00:00 2001
From: Matthias <matmair@live.de>
Date: Sat, 22 May 2021 14:48:56 +0200
Subject: [PATCH 08/12] better translation

---
 InvenTree/templates/InvenTree/settings/currencies.html | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/InvenTree/templates/InvenTree/settings/currencies.html b/InvenTree/templates/InvenTree/settings/currencies.html
index fa9d234988..d9b046b9f5 100644
--- a/InvenTree/templates/InvenTree/settings/currencies.html
+++ b/InvenTree/templates/InvenTree/settings/currencies.html
@@ -24,7 +24,7 @@
 
 <div class='row'>
     <div class='col-sm-6'>
-        <h4>{% trans "Exchange Rates - Convert to " %}{{ default_currency }}</h4>
+        <h4>{% blocktrans with cur=default_currency %}Exchange Rates - Convert to {{cur}}{% endblocktrans %}</h4>
     </div>
 </div>
 

From 4503f23ae4e7c8292c59c93ba66d7a6a021d71a4 Mon Sep 17 00:00:00 2001
From: Matthias <matmair@live.de>
Date: Sat, 22 May 2021 14:55:05 +0200
Subject: [PATCH 09/12] beeing safe with wrong / unknown setttings

---
 InvenTree/InvenTree/exchange.py | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/InvenTree/InvenTree/exchange.py b/InvenTree/InvenTree/exchange.py
index ce1db06711..8de0c9902f 100644
--- a/InvenTree/InvenTree/exchange.py
+++ b/InvenTree/InvenTree/exchange.py
@@ -13,8 +13,10 @@ def get_exchange_rate_backend():
 
     if 'InvenTreeManualExchangeBackend' in inventree_settings.EXCHANGE_BACKEND:
         return InvenTreeManualExchangeBackend()
-    else:
+    elif 'InvenTreeFixerExchangeBackend' in inventree_settings.EXCHANGE_BACKEND:
         return InvenTreeFixerExchangeBackend()
+    else:
+        raise ImproperlyConfigured('Exchange Backend wrongly configured')
 
 
 class InvenTreeManualExchangeBackend(BaseExchangeBackend):

From b1b974a1f611141dd5c8a751bcfc0b79264ef756 Mon Sep 17 00:00:00 2001
From: Matthias <matmair@live.de>
Date: Sat, 22 May 2021 14:55:41 +0200
Subject: [PATCH 10/12] safer check if fixer-backend is used

---
 InvenTree/InvenTree/views.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py
index 48e42dd890..778316ddd5 100644
--- a/InvenTree/InvenTree/views.py
+++ b/InvenTree/InvenTree/views.py
@@ -959,7 +959,7 @@ class CurrencySettingsView(FormView):
         stored_rates = exchange_rate_backend.get_stored_rates()
             
         for field in form.fields:
-            if 'fixer' in exchange_rate_backend.name:
+            if exchange_rate_backend.name.startswith('fixer-'):
                 # Disable all the fields
                 form.fields[field].disabled = True
             form.fields[field].initial = clean_decimal(stored_rates.get(field, 0))
@@ -973,7 +973,7 @@ class CurrencySettingsView(FormView):
         # Get exchange rate backend
         exchange_rate_backend = self.get_exchange_rate_backend()
 
-        if 'fixer' in exchange_rate_backend.name:
+        if exchange_rate_backend.name.startswith('fixer-'):
             # Refresh rate from Fixer.IO API
             exchange_rate_backend.update_rates(base_currency=exchange_rate_backend.base_currency)
             # Check if rates have been updated

From 93bfe4c5f11d05e873629d4427ae13fae3d0b205 Mon Sep 17 00:00:00 2001
From: eeintech <eeintech@eeinte.ch>
Date: Tue, 25 May 2021 11:19:07 -0400
Subject: [PATCH 11/12] Added 'Custom Exchange Rate' boolea setting Removed
 Fixer.io exchange rate backend

---
 InvenTree/InvenTree/exchange.py               | 60 +++++--------------
 InvenTree/InvenTree/settings.py               |  2 -
 InvenTree/InvenTree/tasks.py                  |  7 +--
 InvenTree/InvenTree/views.py                  |  6 +-
 InvenTree/common/models.py                    |  7 +++
 InvenTree/config_template.yaml                |  7 ---
 .../InvenTree/settings/currencies.html        |  6 +-
 7 files changed, 29 insertions(+), 66 deletions(-)

diff --git a/InvenTree/InvenTree/exchange.py b/InvenTree/InvenTree/exchange.py
index 2a2d287e1c..0a75436b1e 100644
--- a/InvenTree/InvenTree/exchange.py
+++ b/InvenTree/InvenTree/exchange.py
@@ -1,7 +1,5 @@
-from django.core.exceptions import ImproperlyConfigured
 from django.conf import settings as inventree_settings
 
-from djmoney import settings as djmoney_settings
 from djmoney.contrib.exchange.backends.base import BaseExchangeBackend
 from djmoney.contrib.exchange.models import Rate
 
@@ -11,12 +9,12 @@ from common.models import InvenTreeSetting
 def get_exchange_rate_backend():
     """ Return the exchange rate backend set by user """
 
-    if 'InvenTreeManualExchangeBackend' in inventree_settings.EXCHANGE_BACKEND:
+    custom = InvenTreeSetting.get_setting('CUSTOM_EXCHANGE_RATES', False)
+
+    if custom:
         return InvenTreeManualExchangeBackend()
-    elif 'InvenTreeFixerExchangeBackend' in inventree_settings.EXCHANGE_BACKEND:
-        return InvenTreeFixerExchangeBackend()
     else:
-        raise ImproperlyConfigured('Exchange Backend wrongly configured')
+        return ExchangeRateHostBackend()
 
 
 class InvenTreeManualExchangeBackend(BaseExchangeBackend):
@@ -30,13 +28,14 @@ class InvenTreeManualExchangeBackend(BaseExchangeBackend):
 
     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', inventree_settings.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 """
@@ -73,39 +72,23 @@ class InvenTreeManualExchangeBackend(BaseExchangeBackend):
         return stored_rates
 
 
-class InvenTreeFixerExchangeBackend(InvenTreeManualExchangeBackend):
+class ExchangeRateHostBackend(InvenTreeManualExchangeBackend):
     """
-    Backend for updating currency exchange rates using Fixer.IO API
+    Backend for https://exchangerate.host/
     """
 
-    name = 'fixer'
-    access_key = None
-
-    def get_api_key(self):
-        """ Get API key from global settings """
-
-        fixer_api_key = InvenTreeSetting.get_setting('INVENTREE_FIXER_API_KEY', '').strip()
-
-        if not fixer_api_key:
-            # API key not provided
-            return None
-
-        self.access_key = fixer_api_key
+    name = "exchangerate.host"
 
     def __init__(self):
-        """ Override init to get access_key from global settings """
+        self.url = "https://api.exchangerate.host/latest"
 
-        self.get_api_key()
+        self.custom_rates = False
 
-        if self.access_key is None:
-            raise ImproperlyConfigured("fixer.io API key is needed to use InvenTreeFixerExchangeBackend")
-        
-        super().__init__(url=djmoney_settings.FIXER_URL)
+        super().__init__(url=self.url)
 
     def get_params(self):
-        """ Returns parameters (access key) """
-
-        return {"access_key": self.access_key}
+        # No API key is required
+        return {}
 
     def update_rates(self, base_currency=None):
         """ Override update_rates method using currencies found in the settings
@@ -135,18 +118,3 @@ class InvenTreeFixerExchangeBackend(InvenTreeManualExchangeBackend):
             pass
 
         return {}
-
-
-class ExchangeRateHostBackend(SimpleExchangeBackend):
-    """
-    Backend for https://exchangerate.host/
-    """
-
-    name = "exchangerate.host"
-
-    def __init__(self):
-        self.url = "https://api.exchangerate.host/latest"
-
-    def get_params(self):
-        # No API key is required
-        return {}
diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py
index f0a3312b39..afa43396f5 100644
--- a/InvenTree/InvenTree/settings.py
+++ b/InvenTree/InvenTree/settings.py
@@ -513,8 +513,6 @@ CURRENCIES = CONFIG.get(
     ],
 )
 
-EXCHANGE_BACKEND = 'InvenTree.exchange.ExchangeRateHostBackend'
-
 # 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 ad33232fe0..365a94fd07 100644
--- a/InvenTree/InvenTree/tasks.py
+++ b/InvenTree/InvenTree/tasks.py
@@ -163,12 +163,11 @@ def check_for_updates():
 
 def update_exchange_rates():
     """
-    If an API key for fixer.io has been provided, attempt to update currency exchange rates
+    Update currency exchange rates
     """
 
     try:
         import common.models
-        from django.conf import settings
         from InvenTree.exchange import ExchangeRateHostBackend
     except AppRegistryNotReady:
         # Apps not yet loaded!
@@ -177,13 +176,11 @@ def update_exchange_rates():
     backend = ExchangeRateHostBackend()
     print(f"Updating exchange rates from {backend.url}")
 
-    currencies = ','.join(settings.CURRENCIES)
-
     base = common.models.InvenTreeSetting.get_setting('INVENTREE_DEFAULT_CURRENCY')
 
     print(f"Using base currency '{base}'")
 
-    backend.update_rates(base_currency=base, symbols=currencies)
+    backend.update_rates(base_currency=base)
 
 
 def send_email(subject, body, recipients, from_email=None):
diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py
index 778316ddd5..a18845bf02 100644
--- a/InvenTree/InvenTree/views.py
+++ b/InvenTree/InvenTree/views.py
@@ -944,6 +944,8 @@ class CurrencySettingsView(FormView):
 
         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
@@ -959,7 +961,7 @@ class CurrencySettingsView(FormView):
         stored_rates = exchange_rate_backend.get_stored_rates()
             
         for field in form.fields:
-            if exchange_rate_backend.name.startswith('fixer-'):
+            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))
@@ -973,7 +975,7 @@ class CurrencySettingsView(FormView):
         # Get exchange rate backend
         exchange_rate_backend = self.get_exchange_rate_backend()
 
-        if exchange_rate_backend.name.startswith('fixer-'):
+        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
diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py
index 99712b2a93..74c6c82b41 100644
--- a/InvenTree/common/models.py
+++ b/InvenTree/common/models.py
@@ -87,6 +87,13 @@ class InvenTreeSetting(models.Model):
             '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'),
diff --git a/InvenTree/config_template.yaml b/InvenTree/config_template.yaml
index 7d301934f0..87dfb6b545 100644
--- a/InvenTree/config_template.yaml
+++ b/InvenTree/config_template.yaml
@@ -62,13 +62,6 @@ currencies:
   - JPY
   - NZD
   - USD
-# Define base currency (can also be defined in the global settings)
-# base_currency: USD
-# Define exchange backend
-# Choices are:
-# - InvenTreeManualExchangeBackend
-# - InvenTreeFixerExchangeBackend
-exchange_backend: InvenTreeManualExchangeBackend
 
 # Email backend configuration
 # Ref: https://docs.djangoproject.com/en/dev/topics/email/
diff --git a/InvenTree/templates/InvenTree/settings/currencies.html b/InvenTree/templates/InvenTree/settings/currencies.html
index d9b046b9f5..b6cf9fea81 100644
--- a/InvenTree/templates/InvenTree/settings/currencies.html
+++ b/InvenTree/templates/InvenTree/settings/currencies.html
@@ -16,9 +16,7 @@
     {% include "InvenTree/settings/header.html" %}
     <tbody>
         {% include "InvenTree/settings/setting.html" with key="INVENTREE_DEFAULT_CURRENCY" icon="fa-dollar-sign" %}
-        {% if 'fixer' in exchange_backend %}
-        {% include "InvenTree/settings/setting.html" with key="INVENTREE_FIXER_API_KEY" icon="fa-key" %}
-        {% endif %}
+        {% include "InvenTree/settings/setting.html" with key="CUSTOM_EXCHANGE_RATES" icon="fa-edit" %}
     </tbody>
 </table>
 
@@ -33,7 +31,7 @@
     {% csrf_token %}
     {% load crispy_forms_tags %}
     {% crispy form %}
-    {% if 'fixer' in exchange_backend %}
+    {% if custom_rates is False %}
         <button type="submit" class='btn btn-primary'>{% trans "Refresh Exchange Rates" %}</button>
     {% else %}
         <button type="submit" class='btn btn-primary'>{% trans "Update Exchange Rates" %}</button>

From b04ad48178f72fabed21bb6ac7255f716095f200 Mon Sep 17 00:00:00 2001
From: eeintech <eeintech@eeinte.ch>
Date: Tue, 25 May 2021 11:39:04 -0400
Subject: [PATCH 12/12] Fixed test using manual exchange backend, template
 update

---
 InvenTree/company/tests.py                             | 4 ++--
 InvenTree/templates/InvenTree/settings/currencies.html | 2 +-
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/InvenTree/company/tests.py b/InvenTree/company/tests.py
index e07516e180..2c6e722440 100644
--- a/InvenTree/company/tests.py
+++ b/InvenTree/company/tests.py
@@ -11,7 +11,7 @@ from .models import Company, Contact, ManufacturerPart, SupplierPart
 from .models import rename_company_image
 from part.models import Part
 
-from InvenTree.exchange import get_exchange_rate_backend
+from InvenTree.exchange import InvenTreeManualExchangeBackend
 from djmoney.contrib.exchange.models import Rate
 
 
@@ -42,7 +42,7 @@ class CompanySimpleTest(TestCase):
         self.zergm312 = SupplierPart.objects.get(SKU='ZERGM312')
         
         # Exchange rate backend
-        backend = get_exchange_rate_backend()
+        backend = InvenTreeManualExchangeBackend()
         backend.update_rates(base_currency=backend.base_currency)
 
         Rate.objects.create(
diff --git a/InvenTree/templates/InvenTree/settings/currencies.html b/InvenTree/templates/InvenTree/settings/currencies.html
index b6cf9fea81..dd47bc6cdd 100644
--- a/InvenTree/templates/InvenTree/settings/currencies.html
+++ b/InvenTree/templates/InvenTree/settings/currencies.html
@@ -45,7 +45,7 @@
 {{ block.super }}
 
 {% if api_rates_success is False %}
-    var alert_msg = {% blocktrans %}"Failed to refresh exchange rates. Verify your API key and/or subscription plan" {% endblocktrans %};
+    var alert_msg = {% blocktrans %}"Failed to refresh exchange rates" {% endblocktrans %};
     showAlertOrCache("alert-danger", alert_msg, null, 5000);
 {% endif %}