mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge pull request #1611 from SchrodingersGat/exchange-simplification
Exchange simplification
This commit is contained in:
commit
59aca2791a
@ -1,120 +1,29 @@
|
|||||||
from django.conf import settings as inventree_settings
|
from django.conf import settings as inventree_settings
|
||||||
|
|
||||||
from djmoney.contrib.exchange.backends.base import BaseExchangeBackend
|
from djmoney.contrib.exchange.backends.base import SimpleExchangeBackend
|
||||||
from djmoney.contrib.exchange.models import Rate
|
|
||||||
|
|
||||||
from common.models import InvenTreeSetting
|
|
||||||
|
|
||||||
|
|
||||||
def get_exchange_rate_backend():
|
class InvenTreeExchange(SimpleExchangeBackend):
|
||||||
""" 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):
|
|
||||||
"""
|
"""
|
||||||
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
|
Uses the exchangerate.host service API
|
||||||
|
|
||||||
Specifically: https://github.com/django-money/django-money/tree/master/djmoney/contrib/exchange/backends
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = 'inventree'
|
name = "InvenTreeExchange"
|
||||||
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 <currency>: <rate> """
|
|
||||||
|
|
||||||
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"
|
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.url = "https://api.exchangerate.host/latest"
|
self.url = "https://api.exchangerate.host/latest"
|
||||||
|
|
||||||
self.custom_rates = False
|
super().__init__()
|
||||||
|
|
||||||
super().__init__(url=self.url)
|
|
||||||
|
|
||||||
def get_params(self):
|
def get_params(self):
|
||||||
# No API key is required
|
# No API key is required
|
||||||
return {}
|
return {
|
||||||
|
}
|
||||||
|
|
||||||
def update_rates(self, base_currency=None):
|
def update_rates(self, base_currency=inventree_settings.BASE_CURRENCY):
|
||||||
""" Override update_rates method using currencies found in the settings
|
|
||||||
"""
|
|
||||||
|
|
||||||
if base_currency:
|
symbols = ','.join(inventree_settings.CURRENCIES)
|
||||||
self.base_currency = base_currency
|
|
||||||
else:
|
|
||||||
self.update_default_currency()
|
|
||||||
|
|
||||||
symbols = ','.join(self.currencies)
|
|
||||||
|
|
||||||
super().update_rates(base_currency=self.base_currency, symbols=symbols)
|
super().update_rates(base=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 {}
|
|
||||||
|
@ -16,8 +16,6 @@ from crispy_forms.bootstrap import PrependedText, AppendedText, PrependedAppende
|
|||||||
from common.models import ColorTheme
|
from common.models import ColorTheme
|
||||||
from part.models import PartCategory
|
from part.models import PartCategory
|
||||||
|
|
||||||
from .exchange import InvenTreeManualExchangeBackend
|
|
||||||
|
|
||||||
|
|
||||||
class HelperForm(forms.ModelForm):
|
class HelperForm(forms.ModelForm):
|
||||||
""" Provides simple integration of crispy_forms extension. """
|
""" Provides simple integration of crispy_forms extension. """
|
||||||
@ -240,35 +238,3 @@ class SettingCategorySelectForm(forms.ModelForm):
|
|||||||
css_class='row',
|
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,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
@ -19,6 +19,8 @@ import shutil
|
|||||||
import sys
|
import sys
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
|
import moneyed
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
@ -513,11 +515,20 @@ CURRENCIES = CONFIG.get(
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
DEFAULT_CURRENCY = get_setting(
|
# Check that each provided currency is supported
|
||||||
'INVENTREE_DEFAULT_CURRENCY',
|
for currency in CURRENCIES:
|
||||||
CONFIG.get('default_currency', 'USD')
|
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
|
# Extract email settings from the config file
|
||||||
email_config = CONFIG.get('email', {})
|
email_config = CONFIG.get('email', {})
|
||||||
|
|
||||||
|
@ -167,16 +167,16 @@ def update_exchange_rates():
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import common.models
|
from InvenTree.exchange import InvenTreeExchange
|
||||||
from InvenTree.exchange import ExchangeRateHostBackend
|
from django.conf import settings
|
||||||
except AppRegistryNotReady:
|
except AppRegistryNotReady:
|
||||||
# Apps not yet loaded!
|
# Apps not yet loaded!
|
||||||
return
|
return
|
||||||
|
|
||||||
backend = ExchangeRateHostBackend()
|
backend = InvenTreeExchange()
|
||||||
print(f"Updating exchange rates from {backend.url}")
|
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}'")
|
print(f"Using base currency '{base}'")
|
||||||
|
|
||||||
|
@ -5,6 +5,12 @@ from django.test import TestCase
|
|||||||
import django.core.exceptions as django_exceptions
|
import django.core.exceptions as django_exceptions
|
||||||
from django.core.exceptions import ValidationError
|
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 .validators import validate_overage, validate_part_name
|
||||||
from . import helpers
|
from . import helpers
|
||||||
from . import version
|
from . import version
|
||||||
@ -13,6 +19,8 @@ from mptt.exceptions import InvalidMove
|
|||||||
|
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
|
import InvenTree.tasks
|
||||||
|
|
||||||
from stock.models import StockLocation
|
from stock.models import StockLocation
|
||||||
|
|
||||||
|
|
||||||
@ -308,3 +316,46 @@ class TestVersionNumber(TestCase):
|
|||||||
self.assertTrue(v_c > v_b)
|
self.assertTrue(v_c > v_b)
|
||||||
self.assertTrue(v_d > v_c)
|
self.assertTrue(v_d > v_c)
|
||||||
self.assertTrue(v_d > v_a)
|
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')
|
||||||
|
@ -39,9 +39,9 @@ from rest_framework.documentation import include_docs_urls
|
|||||||
|
|
||||||
from .views import IndexView, SearchView, DatabaseStatsView
|
from .views import IndexView, SearchView, DatabaseStatsView
|
||||||
from .views import SettingsView, EditUserView, SetPasswordView
|
from .views import SettingsView, EditUserView, SetPasswordView
|
||||||
|
from .views import CurrencySettingsView, CurrencyRefreshView
|
||||||
from .views import AppearanceSelectView, SettingCategorySelectView
|
from .views import AppearanceSelectView, SettingCategorySelectView
|
||||||
from .views import DynamicJsView
|
from .views import DynamicJsView
|
||||||
from .views import CurrencySettingsView
|
|
||||||
|
|
||||||
from common.views import SettingEdit
|
from common.views import SettingEdit
|
||||||
|
|
||||||
@ -83,15 +83,16 @@ settings_urls = [
|
|||||||
url(r'^appearance/?', AppearanceSelectView.as_view(), name='settings-appearance'),
|
url(r'^appearance/?', AppearanceSelectView.as_view(), name='settings-appearance'),
|
||||||
url(r'^i18n/?', include('django.conf.urls.i18n')),
|
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'^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'^report/', SettingsView.as_view(template_name='InvenTree/settings/report.html'), name='settings-report'),
|
||||||
url(r'^category/?', SettingCategorySelectView.as_view(), name='settings-category'),
|
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'^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'^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'^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'^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'^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/', CurrencySettingsView.as_view(), name='settings-currencies'),
|
||||||
|
url(r'^currencies-refresh/', CurrencyRefreshView.as_view(), name='settings-currencies-refresh'),
|
||||||
|
|
||||||
url(r'^(?P<pk>\d+)/edit/', SettingEdit.as_view(), name='setting-edit'),
|
url(r'^(?P<pk>\d+)/edit/', SettingEdit.as_view(), name='setting-edit'),
|
||||||
|
|
||||||
|
@ -12,24 +12,26 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
from django.template.loader import render_to_string
|
from django.template.loader import render_to_string
|
||||||
from django.http import JsonResponse, HttpResponseRedirect
|
from django.http import JsonResponse, HttpResponseRedirect
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||||
|
|
||||||
from django.views import View
|
from django.views import View
|
||||||
from django.views.generic import ListView, DetailView, CreateView, FormView, DeleteView, UpdateView
|
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 part.models import Part, PartCategory
|
||||||
from stock.models import StockLocation, StockItem
|
from stock.models import StockLocation, StockItem
|
||||||
from common.models import InvenTreeSetting, ColorTheme
|
from common.models import InvenTreeSetting, ColorTheme
|
||||||
from users.models import check_user_role, RuleSet
|
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 DeleteForm, EditUserForm, SetPasswordForm
|
||||||
from .forms import ColorThemeSelectForm, SettingCategorySelectForm
|
from .forms import ColorThemeSelectForm, SettingCategorySelectForm
|
||||||
from .forms import SettingExchangeRatesForm
|
|
||||||
from .helpers import str2bool
|
from .helpers import str2bool
|
||||||
from .exchange import get_exchange_rate_backend
|
|
||||||
|
|
||||||
from rest_framework import views
|
from rest_framework import views
|
||||||
|
|
||||||
@ -772,6 +774,50 @@ class SettingsView(TemplateView):
|
|||||||
return ctx
|
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):
|
class AppearanceSelectView(FormView):
|
||||||
""" View for selecting a color theme """
|
""" View for selecting a color theme """
|
||||||
|
|
||||||
@ -911,89 +957,3 @@ class DatabaseStatsView(AjaxView):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
return ctx
|
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)
|
|
||||||
|
@ -13,6 +13,8 @@ from djmoney.forms.fields import MoneyField
|
|||||||
from InvenTree.forms import HelperForm
|
from InvenTree.forms import HelperForm
|
||||||
from InvenTree.helpers import clean_decimal
|
from InvenTree.helpers import clean_decimal
|
||||||
|
|
||||||
|
from common.settings import currency_code_default
|
||||||
|
|
||||||
from .files import FileManager
|
from .files import FileManager
|
||||||
from .models import InvenTreeSetting
|
from .models import InvenTreeSetting
|
||||||
|
|
||||||
@ -182,7 +184,7 @@ class MatchItem(forms.Form):
|
|||||||
if 'price' in col_guess.lower():
|
if 'price' in col_guess.lower():
|
||||||
self.fields[field_name] = MoneyField(
|
self.fields[field_name] = MoneyField(
|
||||||
label=_(col_guess),
|
label=_(col_guess),
|
||||||
default_currency=InvenTreeSetting.get_setting('INVENTREE_DEFAULT_CURRENCY'),
|
default_currency=currency_code_default(),
|
||||||
decimal_places=5,
|
decimal_places=5,
|
||||||
max_digits=19,
|
max_digits=19,
|
||||||
required=False,
|
required=False,
|
||||||
|
@ -14,11 +14,12 @@ from django.db import models, transaction
|
|||||||
from django.db.utils import IntegrityError, OperationalError
|
from django.db.utils import IntegrityError, OperationalError
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
import djmoney.settings
|
|
||||||
from djmoney.models.fields import MoneyField
|
from djmoney.models.fields import MoneyField
|
||||||
from djmoney.contrib.exchange.models import convert_money
|
from djmoney.contrib.exchange.models import convert_money
|
||||||
from djmoney.contrib.exchange.exceptions import MissingRate
|
from djmoney.contrib.exchange.exceptions import MissingRate
|
||||||
|
|
||||||
|
from common.settings import currency_code_default
|
||||||
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django.core.validators import MinValueValidator, URLValidator
|
from django.core.validators import MinValueValidator, URLValidator
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
@ -80,20 +81,6 @@ class InvenTreeSetting(models.Model):
|
|||||||
'default': '',
|
'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': {
|
'INVENTREE_DOWNLOAD_FROM_URL': {
|
||||||
'name': _('Download from URL'),
|
'name': _('Download from URL'),
|
||||||
'description': _('Allow download of remote images and files from external 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:
|
if currency is None:
|
||||||
# Default currency selection
|
# Default currency selection
|
||||||
currency = InvenTreeSetting.get_setting('INVENTREE_DEFAULT_CURRENCY')
|
currency = currency_code_default()
|
||||||
|
|
||||||
pb_min = None
|
pb_min = None
|
||||||
for pb in instance.price_breaks.all():
|
for pb in instance.price_breaks.all():
|
||||||
|
@ -7,7 +7,8 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
from moneyed import CURRENCIES
|
from moneyed import CURRENCIES
|
||||||
|
|
||||||
from common.models import InvenTreeSetting
|
import common.models
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
|
||||||
def currency_code_default():
|
def currency_code_default():
|
||||||
@ -15,7 +16,7 @@ def currency_code_default():
|
|||||||
Returns the default currency code (or USD if not specified)
|
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:
|
if code not in CURRENCIES:
|
||||||
code = 'USD'
|
code = 'USD'
|
||||||
@ -28,4 +29,4 @@ def stock_expiry_enabled():
|
|||||||
Returns True if the stock expiry feature is 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')
|
||||||
|
@ -98,20 +98,15 @@ class SettingsViewTest(TestCase):
|
|||||||
Tests for a setting which has choices
|
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!
|
# Default value!
|
||||||
self.assertEqual(setting.value, 'USD')
|
self.assertEqual(setting.value, 'PO')
|
||||||
|
|
||||||
url = self.get_url(setting.pk)
|
url = self.get_url(setting.pk)
|
||||||
|
|
||||||
# Try posting an invalid currency option
|
# Try posting an invalid currency option
|
||||||
data, errors = self.post(url, {'value': 'XPQaaa'}, valid=False)
|
data, errors = self.post(url, {'value': 'Purchase Order'}, valid=True)
|
||||||
|
|
||||||
self.assertIsNotNone(errors.get('value'), None)
|
|
||||||
|
|
||||||
# Try posting a valid currency option
|
|
||||||
data, errors = self.post(url, {'value': 'AUD'}, valid=True)
|
|
||||||
|
|
||||||
def test_binary_values(self):
|
def test_binary_values(self):
|
||||||
"""
|
"""
|
||||||
|
@ -11,9 +11,6 @@ from .models import Company, Contact, ManufacturerPart, SupplierPart
|
|||||||
from .models import rename_company_image
|
from .models import rename_company_image
|
||||||
from part.models import Part
|
from part.models import Part
|
||||||
|
|
||||||
from InvenTree.exchange import InvenTreeManualExchangeBackend
|
|
||||||
from djmoney.contrib.exchange.models import Rate
|
|
||||||
|
|
||||||
|
|
||||||
class CompanySimpleTest(TestCase):
|
class CompanySimpleTest(TestCase):
|
||||||
|
|
||||||
@ -40,16 +37,6 @@ class CompanySimpleTest(TestCase):
|
|||||||
self.acme0002 = SupplierPart.objects.get(SKU='ACME0002')
|
self.acme0002 = SupplierPart.objects.get(SKU='ACME0002')
|
||||||
self.zerglphs = SupplierPart.objects.get(SKU='ZERGLPHS')
|
self.zerglphs = SupplierPart.objects.get(SKU='ZERGLPHS')
|
||||||
self.zergm312 = SupplierPart.objects.get(SKU='ZERGM312')
|
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):
|
def test_company_model(self):
|
||||||
c = Company.objects.get(name='ABC Co.')
|
c = Company.objects.get(name='ABC Co.')
|
||||||
|
@ -52,6 +52,9 @@ language: en-us
|
|||||||
# Use the environment variable INVENTREE_TIMEZONE
|
# Use the environment variable INVENTREE_TIMEZONE
|
||||||
timezone: UTC
|
timezone: UTC
|
||||||
|
|
||||||
|
# Base currency code
|
||||||
|
base_currency: USD
|
||||||
|
|
||||||
# List of currencies supported by default.
|
# List of currencies supported by default.
|
||||||
# Add other currencies here to allow use in InvenTree
|
# Add other currencies here to allow use in InvenTree
|
||||||
currencies:
|
currencies:
|
||||||
|
@ -2675,7 +2675,7 @@ class PartSalePriceBreakCreate(AjaxCreateView):
|
|||||||
|
|
||||||
initials['part'] = self.get_part()
|
initials['part'] = self.get_part()
|
||||||
|
|
||||||
default_currency = InvenTreeSetting.get_setting('INVENTREE_DEFAULT_CURRENCY')
|
default_currency = settings.BASE_CURRENCY
|
||||||
currency = CURRENCIES.get(default_currency, None)
|
currency = CURRENCIES.get(default_currency, None)
|
||||||
|
|
||||||
if currency is not None:
|
if currency is not None:
|
||||||
|
@ -13,40 +13,43 @@
|
|||||||
{% block settings %}
|
{% block settings %}
|
||||||
|
|
||||||
<table class='table table-striped table-condensed'>
|
<table class='table table-striped table-condensed'>
|
||||||
{% include "InvenTree/settings/header.html" %}
|
|
||||||
<tbody>
|
<tbody>
|
||||||
{% include "InvenTree/settings/setting.html" with key="INVENTREE_DEFAULT_CURRENCY" icon="fa-dollar-sign" %}
|
<tr>
|
||||||
{% include "InvenTree/settings/setting.html" with key="CUSTOM_EXCHANGE_RATES" icon="fa-edit" %}
|
<th>{% trans "Base Currency" %}</th>
|
||||||
|
<th>{{ base_currency }}</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th colspan='2'>{% trans "Exchange Rates" %}</th>
|
||||||
|
</tr>
|
||||||
|
{% for rate in rates %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ rate.currency }}</td>
|
||||||
|
<td>{{ rate.value }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
{% trans "Last Update" %}
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
{% if rates_updated %}
|
||||||
|
{{ rates_updated }}
|
||||||
|
{% else %}
|
||||||
|
<i>{% trans "Never" %}</i>
|
||||||
|
{% endif %}
|
||||||
|
<form action='{% url "settings-currencies-refresh" %}' method='post'>
|
||||||
|
<div id='refresh-rates-form'>
|
||||||
|
{% csrf_token %}
|
||||||
|
<button type='submit' id='update-rates' class='btn btn-default float-right'>{% trans "Update Now" %}</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<div class='row'>
|
|
||||||
<div class='col-sm-6'>
|
|
||||||
<h4>{% blocktrans with cur=default_currency %}Exchange Rates - Convert to {{cur}}{% endblocktrans %}</h4>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form action="{% url 'settings-currencies' %}" method="post">
|
|
||||||
<div id='exchange_rate_form'>
|
|
||||||
{% csrf_token %}
|
|
||||||
{% load crispy_forms_tags %}
|
|
||||||
{% crispy form %}
|
|
||||||
{% 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>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block js_ready %}
|
{% block js_ready %}
|
||||||
{{ block.super }}
|
{{ 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 %}
|
{% endblock %}
|
@ -15,6 +15,9 @@
|
|||||||
<li {% if tab == 'global' %} class='active' {% endif %}>
|
<li {% if tab == 'global' %} class='active' {% endif %}>
|
||||||
<a href='{% url "settings-global" %}'><span class='fas fa-globe'></span> {% trans "Global" %}</a>
|
<a href='{% url "settings-global" %}'><span class='fas fa-globe'></span> {% trans "Global" %}</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li {% if tab == 'currencies' %} class='active'{% endif %}>
|
||||||
|
<a href="{% url 'settings-currencies' %}"><span class='fas fa-dollar-sign'></span> {% trans "Currencies" %}</a>
|
||||||
|
</li>
|
||||||
<li {% if tab == 'report' %} class='active' {% endif %}>
|
<li {% if tab == 'report' %} class='active' {% endif %}>
|
||||||
<a href='{% url "settings-report" %}'><span class='fas fa-file-pdf'></span> {% trans "Report" %}</a>
|
<a href='{% url "settings-report" %}'><span class='fas fa-file-pdf'></span> {% trans "Report" %}</a>
|
||||||
</li>
|
</li>
|
||||||
@ -36,8 +39,5 @@
|
|||||||
<li {% if tab == 'so' %} class='active'{% endif %}>
|
<li {% if tab == 'so' %} class='active'{% endif %}>
|
||||||
<a href="{% url 'settings-so' %}"><span class='fas fa-truck'></span> {% trans "Sales Orders" %}</a>
|
<a href="{% url 'settings-so' %}"><span class='fas fa-truck'></span> {% trans "Sales Orders" %}</a>
|
||||||
</li>
|
</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>
|
</ul>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
Loading…
Reference in New Issue
Block a user