mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge remote-tracking branch 'inventree/master'
This commit is contained in:
commit
b8764c4d6d
@ -1,4 +1,20 @@
|
||||
from djmoney.contrib.exchange.backends.base import BaseExchangeBackend, SimpleExchangeBackend
|
||||
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
|
||||
|
||||
|
||||
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):
|
||||
@ -10,18 +26,53 @@ class InvenTreeManualExchangeBackend(BaseExchangeBackend):
|
||||
Specifically: https://github.com/django-money/django-money/tree/master/djmoney/contrib/exchange/backends
|
||||
"""
|
||||
|
||||
name = "inventree"
|
||||
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):
|
||||
"""
|
||||
Do not get any rates...
|
||||
"""
|
||||
""" 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 ExchangeRateHostBackend(SimpleExchangeBackend):
|
||||
class ExchangeRateHostBackend(InvenTreeManualExchangeBackend):
|
||||
"""
|
||||
Backend for https://exchangerate.host/
|
||||
"""
|
||||
@ -31,6 +82,39 @@ class ExchangeRateHostBackend(SimpleExchangeBackend):
|
||||
def __init__(self):
|
||||
self.url = "https://api.exchangerate.host/latest"
|
||||
|
||||
self.custom_rates = False
|
||||
|
||||
super().__init__(url=self.url)
|
||||
|
||||
def get_params(self):
|
||||
# No API key is required
|
||||
return {}
|
||||
|
||||
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 {}
|
||||
|
@ -7,13 +7,17 @@ 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):
|
||||
""" Provides simple integration of crispy_forms extension. """
|
||||
@ -236,3 +240,35 @@ 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,
|
||||
})
|
||||
)
|
||||
|
@ -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()
|
||||
|
@ -513,10 +513,6 @@ CURRENCIES = CONFIG.get(
|
||||
],
|
||||
)
|
||||
|
||||
BASE_CURRENCY = CONFIG.get('base_currency', 'USD')
|
||||
|
||||
EXCHANGE_BACKEND = 'InvenTree.exchange.InvenTreeManualExchangeBackend'
|
||||
|
||||
# Extract email settings from the config file
|
||||
email_config = CONFIG.get('email', {})
|
||||
|
||||
|
@ -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 currency exchange rates
|
||||
"""
|
||||
|
||||
try:
|
||||
import common.models
|
||||
from django.conf import settings
|
||||
from djmoney.contrib.exchange.backends import FixerBackend
|
||||
from InvenTree.exchange import ExchangeRateHostBackend
|
||||
except AppRegistryNotReady:
|
||||
# Apps not yet loaded!
|
||||
return
|
||||
|
||||
fixer_api_key = common.models.InvenTreeSetting.get_setting('INVENTREE_FIXER_API_KEY', '').strip()
|
||||
backend = ExchangeRateHostBackend()
|
||||
print(f"Updating exchange rates from {backend.url}")
|
||||
|
||||
if not fixer_api_key:
|
||||
# API key not provided
|
||||
return
|
||||
base = common.models.InvenTreeSetting.get_setting('INVENTREE_DEFAULT_CURRENCY')
|
||||
|
||||
backend = FixerBackend(access_key=fixer_api_key)
|
||||
print(f"Using base currency '{base}'")
|
||||
|
||||
currencies = ','.join(settings.CURRENCIES)
|
||||
|
||||
base = settings.BASE_CURRENCY
|
||||
|
||||
backend.update_rates(base_currency=base, symbols=currencies)
|
||||
backend.update_rates(base_currency=base)
|
||||
|
||||
|
||||
def send_email(subject, body, recipients, from_email=None):
|
||||
|
@ -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 CurrencySettingsView
|
||||
|
||||
from common.views import SettingEdit
|
||||
|
||||
@ -90,6 +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/?', CurrencySettingsView.as_view(), name='settings-currencies'),
|
||||
|
||||
url(r'^(?P<pk>\d+)/edit/', SettingEdit.as_view(), name='setting-edit'),
|
||||
|
||||
|
@ -23,10 +23,13 @@ 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
|
||||
from .forms import SettingExchangeRatesForm
|
||||
from .helpers import str2bool
|
||||
from .exchange import get_exchange_rate_backend
|
||||
|
||||
from rest_framework import views
|
||||
|
||||
@ -908,3 +911,89 @@ 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)
|
||||
|
@ -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(
|
||||
|
@ -87,10 +87,11 @@ class InvenTreeSetting(models.Model):
|
||||
'choices': djmoney.settings.CURRENCY_CHOICES,
|
||||
},
|
||||
|
||||
'INVENTREE_FIXER_API_KEY': {
|
||||
'name': _('fixer.io API key'),
|
||||
'description': _('API key for fixer.io currency conversion service'),
|
||||
'default': '',
|
||||
'CUSTOM_EXCHANGE_RATES': {
|
||||
'name': _('Custom Exchange Rates'),
|
||||
'description': _('Enable custom exchange rates'),
|
||||
'validator': bool,
|
||||
'default': False,
|
||||
},
|
||||
|
||||
'INVENTREE_DOWNLOAD_FROM_URL': {
|
||||
|
@ -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 = InvenTreeManualExchangeBackend()
|
||||
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):
|
||||
|
52
InvenTree/templates/InvenTree/settings/currencies.html
Normal file
52
InvenTree/templates/InvenTree/settings/currencies.html
Normal file
@ -0,0 +1,52 @@
|
||||
{% 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="CUSTOM_EXCHANGE_RATES" icon="fa-edit" %}
|
||||
</tbody>
|
||||
</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 %}
|
||||
|
||||
{% 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 %}
|
@ -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>
|
||||
@ -33,4 +31,4 @@
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
@ -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 %}
|
||||
|
Loading…
Reference in New Issue
Block a user