diff --git a/InvenTree/InvenTree/apps.py b/InvenTree/InvenTree/apps.py index aeddb714a0..feb46ee667 100644 --- a/InvenTree/InvenTree/apps.py +++ b/InvenTree/InvenTree/apps.py @@ -4,7 +4,6 @@ import logging from django.apps import AppConfig from django.core.exceptions import AppRegistryNotReady -from django.conf import settings from InvenTree.ready import isInTestMode, canAppAccessDatabase import InvenTree.tasks @@ -66,10 +65,11 @@ class InvenTreeConfig(AppConfig): from djmoney.contrib.exchange.models import ExchangeBackend from datetime import datetime, timedelta from InvenTree.tasks import update_exchange_rates + from common.settings import currency_code_default except AppRegistryNotReady: pass - base_currency = settings.BASE_CURRENCY + base_currency = currency_code_default() update = False diff --git a/InvenTree/InvenTree/exchange.py b/InvenTree/InvenTree/exchange.py index 0695e69f48..c75a827cc7 100644 --- a/InvenTree/InvenTree/exchange.py +++ b/InvenTree/InvenTree/exchange.py @@ -1,4 +1,4 @@ -from django.conf import settings as inventree_settings +from common.settings import currency_code_default, currency_codes from djmoney.contrib.exchange.backends.base import SimpleExchangeBackend @@ -22,8 +22,8 @@ class InvenTreeExchange(SimpleExchangeBackend): return { } - def update_rates(self, base_currency=inventree_settings.BASE_CURRENCY): + def update_rates(self, base_currency=currency_code_default()): - symbols = ','.join(inventree_settings.CURRENCIES) + symbols = ','.join(currency_codes()) super().update_rates(base=base_currency, symbols=symbols) diff --git a/InvenTree/InvenTree/fields.py b/InvenTree/InvenTree/fields.py index c496c1bb22..8d9ab77463 100644 --- a/InvenTree/InvenTree/fields.py +++ b/InvenTree/InvenTree/fields.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals +import sys from .validators import allowable_url_schemes @@ -13,8 +14,11 @@ from django.core import validators from django import forms from decimal import Decimal +from djmoney.models.fields import MoneyField as ModelMoneyField +from djmoney.forms.fields import MoneyField import InvenTree.helpers +import common.settings class InvenTreeURLFormField(FormURLField): @@ -34,6 +38,42 @@ class InvenTreeURLField(models.URLField): }) +def money_kwargs(): + """ returns the database settings for MoneyFields """ + kwargs = {} + kwargs['currency_choices'] = common.settings.currency_code_mappings() + kwargs['default_currency'] = common.settings.currency_code_default + return kwargs + + +class InvenTreeModelMoneyField(ModelMoneyField): + """ custom MoneyField for clean migrations while using dynamic currency settings """ + def __init__(self, **kwargs): + # detect if creating migration + if 'makemigrations' in sys.argv: + # remove currency information for a clean migration + kwargs['default_currency'] = '' + kwargs['currency_choices'] = [] + else: + # set defaults + kwargs.update(money_kwargs()) + + super().__init__(**kwargs) + + def formfield(self, **kwargs): + """ override form class to use own function """ + kwargs['form_class'] = InvenTreeMoneyField + return super().formfield(**kwargs) + + +class InvenTreeMoneyField(MoneyField): + """ custom MoneyField for clean migrations while using dynamic currency settings """ + def __init__(self, *args, **kwargs): + # override initial values with the real info from database + kwargs.update(money_kwargs()) + super().__init__(*args, **kwargs) + + class DatePickerFormField(forms.DateField): """ Custom date-picker field diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index 569c77a940..3b7c95e245 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -523,10 +523,6 @@ for currency in 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' diff --git a/InvenTree/InvenTree/tasks.py b/InvenTree/InvenTree/tasks.py index f0fe504072..cab87ee64b 100644 --- a/InvenTree/InvenTree/tasks.py +++ b/InvenTree/InvenTree/tasks.py @@ -170,7 +170,7 @@ def update_exchange_rates(): try: from InvenTree.exchange import InvenTreeExchange from djmoney.contrib.exchange.models import ExchangeBackend, Rate - from django.conf import settings + from common.settings import currency_code_default, currency_codes except AppRegistryNotReady: # Apps not yet loaded! logger.info("Could not perform 'update_exchange_rates' - App registry not ready") @@ -192,14 +192,14 @@ def update_exchange_rates(): backend = InvenTreeExchange() print(f"Updating exchange rates from {backend.url}") - base = settings.BASE_CURRENCY + base = currency_code_default() print(f"Using base currency '{base}'") backend.update_rates(base_currency=base) # Remove any exchange rates which are not in the provided currencies - Rate.objects.filter(backend="InvenTreeExchange").exclude(currency__in=settings.CURRENCIES).delete() + Rate.objects.filter(backend="InvenTreeExchange").exclude(currency__in=currency_codes()).delete() def send_email(subject, body, recipients, from_email=None): diff --git a/InvenTree/InvenTree/tests.py b/InvenTree/InvenTree/tests.py index cb570d0176..06dcad9797 100644 --- a/InvenTree/InvenTree/tests.py +++ b/InvenTree/InvenTree/tests.py @@ -5,8 +5,6 @@ from django.test import TestCase import django.core.exceptions as django_exceptions from django.core.exceptions import ValidationError -from django.conf import settings - from djmoney.money import Money from djmoney.contrib.exchange.models import Rate, convert_money from djmoney.contrib.exchange.exceptions import MissingRate @@ -20,6 +18,7 @@ from decimal import Decimal import InvenTree.tasks from stock.models import StockLocation +from common.settings import currency_codes class ValidatorTest(TestCase): @@ -335,13 +334,11 @@ class CurrencyTests(TestCase): 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)) + self.assertEqual(rates.count(), len(currency_codes())) # Now that we have some exchange rate information, we can perform conversions diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py index 17caeb872d..4b559642ca 100644 --- a/InvenTree/InvenTree/views.py +++ b/InvenTree/InvenTree/views.py @@ -12,7 +12,6 @@ from django.utils.translation import gettext_lazy as _ from django.template.loader import render_to_string from django.http import HttpResponse, JsonResponse, HttpResponseRedirect from django.urls import reverse_lazy -from django.conf import settings from django.contrib.auth.mixins import PermissionRequiredMixin @@ -21,6 +20,7 @@ from django.views.generic import ListView, DetailView, CreateView, FormView, Del from django.views.generic.base import RedirectView, TemplateView from djmoney.contrib.exchange.models import ExchangeBackend, Rate +from common.settings import currency_code_default, currency_codes from part.models import Part, PartCategory from stock.models import StockLocation, StockItem @@ -820,8 +820,8 @@ class CurrencySettingsView(TemplateView): 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["base_currency"] = currency_code_default() + ctx["currencies"] = currency_codes ctx["rates"] = Rate.objects.filter(backend="InvenTreeExchange") diff --git a/InvenTree/common/migrations/0010_migrate_currency_setting.py b/InvenTree/common/migrations/0010_migrate_currency_setting.py new file mode 100644 index 0000000000..23076ff200 --- /dev/null +++ b/InvenTree/common/migrations/0010_migrate_currency_setting.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.4 on 2021-07-01 15:39 + +from django.db import migrations +from common.models import InvenTreeSetting +from InvenTree.settings import get_setting, CONFIG + +def set_default_currency(apps, schema_editor): + """ migrate the currency setting from config.yml to db """ + # get value from settings-file + base_currency = get_setting('INVENTREE_BASE_CURRENCY', CONFIG.get('base_currency', 'USD')) + # write to database + InvenTreeSetting.set_setting('INVENTREE_DEFAULT_CURRENCY', base_currency, None, create=True) + + +class Migration(migrations.Migration): + + dependencies = [ + ('common', '0009_delete_currency'), + ] + + operations = [ + migrations.RunPython(set_default_currency), + ] diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index 7e56429c51..c8a5839f4e 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -14,11 +14,11 @@ from django.db import models, transaction from django.db.utils import IntegrityError, OperationalError from django.conf import settings -from djmoney.models.fields import MoneyField +from djmoney.settings import CURRENCY_CHOICES from djmoney.contrib.exchange.models import convert_money from djmoney.contrib.exchange.exceptions import MissingRate -from common.settings import currency_code_default +import common.settings from django.utils.translation import ugettext_lazy as _ from django.core.validators import MinValueValidator, URLValidator @@ -81,6 +81,13 @@ class InvenTreeSetting(models.Model): 'default': '', }, + 'INVENTREE_DEFAULT_CURRENCY': { + 'name': _('Default Currency'), + 'description': _('Default currency'), + 'default': 'USD', + 'choices': CURRENCY_CHOICES, + }, + 'INVENTREE_DOWNLOAD_FROM_URL': { 'name': _('Download from URL'), 'description': _('Allow download of remote images and files from external URL'), @@ -735,10 +742,9 @@ class PriceBreak(models.Model): help_text=_('Price break quantity'), ) - price = MoneyField( + price = InvenTree.fields.InvenTreeModelMoneyField( max_digits=19, decimal_places=4, - default_currency=currency_code_default(), null=True, verbose_name=_('Price'), help_text=_('Unit price at specified quantity'), @@ -791,7 +797,7 @@ def get_price(instance, quantity, moq=True, multiples=True, currency=None, break if currency is None: # Default currency selection - currency = currency_code_default() + currency = common.settings.currency_code_default() pb_min = None for pb in price_breaks: diff --git a/InvenTree/common/settings.py b/InvenTree/common/settings.py index 60265f4cb9..e255ed0904 100644 --- a/InvenTree/common/settings.py +++ b/InvenTree/common/settings.py @@ -6,9 +6,9 @@ User-configurable settings for the common app from __future__ import unicode_literals from moneyed import CURRENCIES +from django.conf import settings import common.models -from django.conf import settings def currency_code_default(): @@ -16,7 +16,7 @@ def currency_code_default(): Returns the default currency code (or USD if not specified) """ - code = settings.BASE_CURRENCY + code = common.models.InvenTreeSetting.get_setting('INVENTREE_DEFAULT_CURRENCY') if code not in CURRENCIES: code = 'USD' @@ -24,6 +24,20 @@ def currency_code_default(): return code +def currency_code_mappings(): + """ + Returns the current currency choices + """ + return [(a, a) for a in settings.CURRENCIES] + + +def currency_codes(): + """ + Returns the current currency codes + """ + return [a for a in settings.CURRENCIES] + + def stock_expiry_enabled(): """ Returns True if the stock expiry feature is enabled diff --git a/InvenTree/company/forms.py b/InvenTree/company/forms.py index 63c07ff7a4..7a0195a680 100644 --- a/InvenTree/company/forms.py +++ b/InvenTree/company/forms.py @@ -6,7 +6,7 @@ Django Forms for interacting with Company app from __future__ import unicode_literals from InvenTree.forms import HelperForm -from InvenTree.fields import RoundingDecimalFormField +from InvenTree.fields import InvenTreeMoneyField, RoundingDecimalFormField from django.utils.translation import ugettext_lazy as _ import django.forms @@ -67,9 +67,8 @@ class EditSupplierPartForm(HelperForm): 'note': 'fa-pencil-alt', } - single_pricing = MoneyField( + single_pricing = InvenTreeMoneyField( label=_('Single Price'), - default_currency=currency_code_default(), help_text=_('Single quantity price'), decimal_places=4, max_digits=19, diff --git a/InvenTree/company/migrations/0039_auto_20210701_0509.py b/InvenTree/company/migrations/0039_auto_20210701_0509.py new file mode 100644 index 0000000000..094c7a5009 --- /dev/null +++ b/InvenTree/company/migrations/0039_auto_20210701_0509.py @@ -0,0 +1,25 @@ +# Generated by Django 3.2.4 on 2021-07-01 05:09 + +import InvenTree.fields +from django.db import migrations +import djmoney.models.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('company', '0038_manufacturerpartparameter'), + ] + + operations = [ + migrations.AlterField( + model_name='supplierpricebreak', + name='price', + field=InvenTree.fields.InvenTreeModelMoneyField(currency_choices=[], decimal_places=4, default_currency='', help_text='Unit price at specified quantity', max_digits=19, null=True, verbose_name='Price'), + ), + migrations.AlterField( + model_name='supplierpricebreak', + name='price_currency', + field=djmoney.models.fields.CurrencyField(choices=[], default='', editable=False, max_length=3), + ), + ] diff --git a/InvenTree/order/forms.py b/InvenTree/order/forms.py index 39536d457a..74b6d5f47b 100644 --- a/InvenTree/order/forms.py +++ b/InvenTree/order/forms.py @@ -10,15 +10,12 @@ from django.utils.translation import ugettext_lazy as _ from mptt.fields import TreeNodeChoiceField -from djmoney.forms.fields import MoneyField - from InvenTree.forms import HelperForm -from InvenTree.fields import RoundingDecimalFormField +from InvenTree.fields import InvenTreeMoneyField, RoundingDecimalFormField from InvenTree.fields import DatePickerFormField from InvenTree.helpers import clean_decimal -from common.models import InvenTreeSetting from common.forms import MatchItemForm import part.models @@ -297,9 +294,8 @@ class OrderMatchItemForm(MatchItemForm): ) # set price field elif 'price' in col_guess.lower(): - return MoneyField( + return InvenTreeMoneyField( label=_(col_guess), - default_currency=InvenTreeSetting.get_setting('INVENTREE_DEFAULT_CURRENCY'), decimal_places=5, max_digits=19, required=False, diff --git a/InvenTree/order/migrations/0047_auto_20210701_0509.py b/InvenTree/order/migrations/0047_auto_20210701_0509.py new file mode 100644 index 0000000000..1f732e5f61 --- /dev/null +++ b/InvenTree/order/migrations/0047_auto_20210701_0509.py @@ -0,0 +1,35 @@ +# Generated by Django 3.2.4 on 2021-07-01 05:09 + +import InvenTree.fields +from django.db import migrations +import djmoney.models.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('order', '0046_purchaseorderlineitem_destination'), + ] + + operations = [ + migrations.AlterField( + model_name='purchaseorderlineitem', + name='purchase_price', + field=InvenTree.fields.InvenTreeModelMoneyField(blank=True, currency_choices=[], decimal_places=4, default_currency='', help_text='Unit purchase price', max_digits=19, null=True, verbose_name='Purchase Price'), + ), + migrations.AlterField( + model_name='purchaseorderlineitem', + name='purchase_price_currency', + field=djmoney.models.fields.CurrencyField(choices=[], default='', editable=False, max_length=3), + ), + migrations.AlterField( + model_name='salesorderlineitem', + name='sale_price', + field=InvenTree.fields.InvenTreeModelMoneyField(blank=True, currency_choices=[], decimal_places=4, default_currency='', help_text='Unit sale price', max_digits=19, null=True, verbose_name='Sale Price'), + ), + migrations.AlterField( + model_name='salesorderlineitem', + name='sale_price_currency', + field=djmoney.models.fields.CurrencyField(choices=[], default='', editable=False, max_length=3), + ), + ] diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index e294e95030..c35e93757a 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -17,19 +17,15 @@ from django.contrib.auth.models import User from django.urls import reverse from django.utils.translation import ugettext_lazy as _ -from common.settings import currency_code_default - from markdownx.models import MarkdownxField from mptt.models import TreeForeignKey -from djmoney.models.fields import MoneyField - from users import models as UserModels from part import models as PartModels from stock import models as stock_models from company.models import Company, SupplierPart -from InvenTree.fields import RoundingDecimalField +from InvenTree.fields import InvenTreeModelMoneyField, RoundingDecimalField from InvenTree.helpers import decimal2string, increment, getSetting from InvenTree.status_codes import PurchaseOrderStatus, SalesOrderStatus, StockStatus, StockHistoryCode from InvenTree.models import InvenTreeAttachment @@ -684,10 +680,9 @@ class PurchaseOrderLineItem(OrderLineItem): received = models.DecimalField(decimal_places=5, max_digits=15, default=0, verbose_name=_('Received'), help_text=_('Number of items received')) - purchase_price = MoneyField( + purchase_price = InvenTreeModelMoneyField( max_digits=19, decimal_places=4, - default_currency=currency_code_default(), null=True, blank=True, verbose_name=_('Purchase Price'), help_text=_('Unit purchase price'), @@ -740,10 +735,9 @@ class SalesOrderLineItem(OrderLineItem): part = models.ForeignKey('part.Part', on_delete=models.SET_NULL, related_name='sales_order_line_items', null=True, verbose_name=_('Part'), help_text=_('Part'), limit_choices_to={'salable': True}) - sale_price = MoneyField( + sale_price = InvenTreeModelMoneyField( max_digits=19, decimal_places=4, - default_currency=currency_code_default(), null=True, blank=True, verbose_name=_('Sale Price'), help_text=_('Unit sale price'), diff --git a/InvenTree/part/migrations/0069_auto_20210701_0509.py b/InvenTree/part/migrations/0069_auto_20210701_0509.py new file mode 100644 index 0000000000..86d8d2d44c --- /dev/null +++ b/InvenTree/part/migrations/0069_auto_20210701_0509.py @@ -0,0 +1,35 @@ +# Generated by Django 3.2.4 on 2021-07-01 05:09 + +import InvenTree.fields +from django.db import migrations +import djmoney.models.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('part', '0068_part_unique_part'), + ] + + operations = [ + migrations.AlterField( + model_name='partinternalpricebreak', + name='price', + field=InvenTree.fields.InvenTreeModelMoneyField(currency_choices=[], decimal_places=4, default_currency='', help_text='Unit price at specified quantity', max_digits=19, null=True, verbose_name='Price'), + ), + migrations.AlterField( + model_name='partinternalpricebreak', + name='price_currency', + field=djmoney.models.fields.CurrencyField(choices=[], default='', editable=False, max_length=3), + ), + migrations.AlterField( + model_name='partsellpricebreak', + name='price', + field=InvenTree.fields.InvenTreeModelMoneyField(currency_choices=[], decimal_places=4, default_currency='', help_text='Unit price at specified quantity', max_digits=19, null=True, verbose_name='Price'), + ), + migrations.AlterField( + model_name='partsellpricebreak', + name='price_currency', + field=djmoney.models.fields.CurrencyField(choices=[], default='', editable=False, max_length=3), + ), + ] diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 78fd0d7a27..38a534f1ec 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -2793,7 +2793,7 @@ class PartSalePriceBreakCreate(AjaxCreateView): initials['part'] = self.get_part() - default_currency = settings.BASE_CURRENCY + default_currency = inventree_settings.currency_code_default() currency = CURRENCIES.get(default_currency, None) if currency is not None: diff --git a/InvenTree/stock/migrations/0065_auto_20210701_0509.py b/InvenTree/stock/migrations/0065_auto_20210701_0509.py new file mode 100644 index 0000000000..99cde1e7f7 --- /dev/null +++ b/InvenTree/stock/migrations/0065_auto_20210701_0509.py @@ -0,0 +1,25 @@ +# Generated by Django 3.2.4 on 2021-07-01 05:09 + +import InvenTree.fields +from django.db import migrations +import djmoney.models.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('stock', '0064_auto_20210621_1724'), + ] + + operations = [ + migrations.AlterField( + model_name='stockitem', + name='purchase_price', + field=InvenTree.fields.InvenTreeModelMoneyField(blank=True, currency_choices=[], decimal_places=4, default_currency='', help_text='Single unit purchase price at time of purchase', max_digits=19, null=True, verbose_name='Purchase Price'), + ), + migrations.AlterField( + model_name='stockitem', + name='purchase_price_currency', + field=djmoney.models.fields.CurrencyField(choices=[], default='', editable=False, max_length=3), + ), + ] diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index a29630b4d7..9b3a95a20c 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -20,14 +20,10 @@ from django.contrib.auth.models import User from django.db.models.signals import pre_delete from django.dispatch import receiver -from common.settings import currency_code_default - from markdownx.models import MarkdownxField from mptt.models import MPTTModel, TreeForeignKey -from djmoney.models.fields import MoneyField - from decimal import Decimal, InvalidOperation from datetime import datetime, timedelta from InvenTree import helpers @@ -38,7 +34,7 @@ import label.models from InvenTree.status_codes import StockStatus, StockHistoryCode from InvenTree.models import InvenTreeTree, InvenTreeAttachment -from InvenTree.fields import InvenTreeURLField +from InvenTree.fields import InvenTreeModelMoneyField, InvenTreeURLField from users.models import Owner @@ -541,10 +537,9 @@ class StockItem(MPTTModel): help_text=_('Stock Item Notes') ) - purchase_price = MoneyField( + purchase_price = InvenTreeModelMoneyField( max_digits=19, decimal_places=4, - default_currency=currency_code_default(), blank=True, null=True, verbose_name=_('Purchase Price'), diff --git a/InvenTree/templates/InvenTree/settings/currencies.html b/InvenTree/templates/InvenTree/settings/currencies.html index 78598236f9..6e61533e6b 100644 --- a/InvenTree/templates/InvenTree/settings/currencies.html +++ b/InvenTree/templates/InvenTree/settings/currencies.html @@ -12,6 +12,13 @@ {% block settings %} + + {% include "InvenTree/settings/header.html" %} + + {% include "InvenTree/settings/setting.html" with key="INVENTREE_DEFAULT_CURRENCY" icon="fa-globe" %} + +
+