mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Currency setting (#7390)
* Add new global setting for currency options - Moving away from external configuration - Refactor currency support code into new file * Refactoring - Move functions into currency.py * Limit choices for default currency * Improve validation * Adds data migration for existing currency selection * Docs updates * Remove currency config from external settings * bump api version * Add debug message * Add unit tests * Fix after_change_currency func * Fix after_change_currency func * Revert change to after_chance_currency * Revert other change
This commit is contained in:
parent
7108bc48bd
commit
e83feb9414
@ -6,13 +6,13 @@ title: Currency Support
|
||||
|
||||
InvenTree provides support for multiple currencies, allowing pricing information to be stored with base currency rates.
|
||||
|
||||
### Configuration
|
||||
### Supported Currencies
|
||||
|
||||
To specify which currencies are supported, refer to the [currency configuration](../start/config.md#supported-currencies) section
|
||||
InvenTree uses the [django-money](https://github.com/django-money/django-money) library, which in turn uses the [py-moneyed library](https://py-moneyed.readthedocs.io/en/latest/index.html). `py-moneyed` supports any currency which is defined in the [ISO 3166 standard](https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes) standard.
|
||||
|
||||
### Currency Conversion
|
||||
|
||||
Currency conversion is provided via the [django-money](https://github.com/django-money/django-money) library. Pricing data can be converted seamlessly between the available currencies.
|
||||
Currency conversion is provided via the `django-money` library. Pricing data can be converted seamlessly between the available currencies.
|
||||
|
||||
### Currency Rate Updates
|
||||
|
||||
@ -26,6 +26,11 @@ If a different currency exchange backend is needed, or a custom implementation i
|
||||
|
||||
In the [settings screen](./global.md), under the *Pricing* section, the following currency settings are available:
|
||||
|
||||
{% with id="currency-settings", url="settings/currency.png", description="Currency Exchange Settings" %}
|
||||
{% include 'img.html' %}
|
||||
{% endwith %}
|
||||
| Setting | Description | Default Value |
|
||||
| --- | --- |
|
||||
| Default Currency | The selected *default* currency for the system. | USD |
|
||||
| Supported Currencies | The list of supported currencies for the system. | AUD, CAD, CNY, EUR, GBP, JPY, NZD, USD |
|
||||
|
||||
#### Supported Currencies
|
||||
|
||||
While InvenTree can support any of the currencies defined in the ISO 3166 standard, the list of supported currencies can be limited to only those which are relevant to the user. The supported currencies are used to populate the currency selection dropdowns throughout the InvenTree interface.
|
||||
|
@ -256,19 +256,6 @@ The "sender" email address is the address from which InvenTree emails are sent (
|
||||
!!! info "Fallback"
|
||||
If `INVENTREE_EMAIL_SENDER` is not provided, the system will fall back to `INVENTREE_EMAIL_USERNAME` (if the username is a valid email address)
|
||||
|
||||
## Supported Currencies
|
||||
|
||||
The currencies supported by InvenTree must be specified in the [configuration file](#configuration-file).
|
||||
|
||||
A list of currency codes (e.g. *AUD*, *CAD*, *JPY*, *USD*) can be specified using the `currencies` variable (or using the `INVENTREE_CURRENCIES` environment variable).
|
||||
|
||||
| Environment Variable | Configuration File | Description | Default |
|
||||
| --- | --- | --- | --- |
|
||||
| INVENTREE_CURRENCIES | currencies | List of supported currencies| `AUD`, `CAD`, `CNY`, `EUR`, `GBP`, `JPY`, `NZD`, `USD` |
|
||||
|
||||
!!! tip "More Info"
|
||||
Read the [currencies documentation](../settings/currency.md) for more information on currency support in InvenTree
|
||||
|
||||
## File Storage Locations
|
||||
|
||||
InvenTree requires some external directories for storing files:
|
||||
|
@ -1,12 +1,15 @@
|
||||
"""InvenTree API version information."""
|
||||
|
||||
# InvenTree API version
|
||||
INVENTREE_API_VERSION = 202
|
||||
INVENTREE_API_VERSION = 203
|
||||
|
||||
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
||||
|
||||
INVENTREE_API_TEXT = """
|
||||
|
||||
v203 - 2024-06-03 : https://github.com/inventree/InvenTree/pull/7390
|
||||
- Adjust default currency codes
|
||||
|
||||
v202 - 2024-05-27 : https://github.com/inventree/InvenTree/pull/7343
|
||||
- Adjust "required" attribute of Part.category field to be optional
|
||||
|
||||
|
@ -185,7 +185,7 @@ class InvenTreeConfig(AppConfig):
|
||||
try:
|
||||
from djmoney.contrib.exchange.models import ExchangeBackend
|
||||
|
||||
from common.settings import currency_code_default
|
||||
from common.currency import currency_code_default
|
||||
from InvenTree.tasks import update_exchange_rates
|
||||
except AppRegistryNotReady: # pragma: no cover
|
||||
pass
|
||||
|
@ -7,7 +7,7 @@ from django.db.transaction import atomic
|
||||
from djmoney.contrib.exchange.backends.base import SimpleExchangeBackend
|
||||
from djmoney.contrib.exchange.models import ExchangeBackend, Rate
|
||||
|
||||
from common.settings import currency_code_default, currency_codes
|
||||
from common.currency import currency_code_default, currency_codes
|
||||
|
||||
logger = logging.getLogger('inventree')
|
||||
|
||||
|
@ -59,7 +59,7 @@ class InvenTreeURLField(models.URLField):
|
||||
|
||||
def money_kwargs(**kwargs):
|
||||
"""Returns the database settings for MoneyFields."""
|
||||
from common.settings import currency_code_default, currency_code_mappings
|
||||
from common.currency import currency_code_default, currency_code_mappings
|
||||
|
||||
# Default values (if not specified)
|
||||
if 'max_digits' not in kwargs:
|
||||
|
@ -28,7 +28,7 @@ from djmoney.money import Money
|
||||
from PIL import Image
|
||||
|
||||
import InvenTree.version
|
||||
from common.settings import currency_code_default
|
||||
from common.currency import currency_code_default
|
||||
|
||||
from .settings import MEDIA_URL, STATIC_URL
|
||||
|
||||
|
@ -23,7 +23,7 @@ from rest_framework.utils import model_meta
|
||||
from taggit.serializers import TaggitSerializer
|
||||
|
||||
import common.models as common_models
|
||||
from common.settings import currency_code_default, currency_code_mappings
|
||||
from common.currency import currency_code_default, currency_code_mappings
|
||||
from InvenTree.fields import InvenTreeRestURLField, InvenTreeURLField
|
||||
|
||||
|
||||
|
@ -20,7 +20,6 @@ from django.core.validators import URLValidator
|
||||
from django.http import Http404
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
import moneyed
|
||||
import pytz
|
||||
from dotenv import load_dotenv
|
||||
|
||||
@ -904,28 +903,9 @@ if get_boolean_setting('TEST_TRANSLATIONS', default_value=False): # pragma: no
|
||||
LANG_INFO = dict(django.conf.locale.LANG_INFO, **EXTRA_LANG_INFO)
|
||||
django.conf.locale.LANG_INFO = LANG_INFO
|
||||
|
||||
# Currencies available for use
|
||||
CURRENCIES = get_setting(
|
||||
'INVENTREE_CURRENCIES',
|
||||
'currencies',
|
||||
['AUD', 'CAD', 'CNY', 'EUR', 'GBP', 'JPY', 'NZD', 'USD'],
|
||||
typecast=list,
|
||||
)
|
||||
|
||||
# Ensure that at least one currency value is available
|
||||
if len(CURRENCIES) == 0: # pragma: no cover
|
||||
logger.warning('No currencies selected: Defaulting to USD')
|
||||
CURRENCIES = ['USD']
|
||||
|
||||
# Maximum number of decimal places for currency rendering
|
||||
CURRENCY_DECIMAL_PLACES = 6
|
||||
|
||||
# Check that each provided currency is supported
|
||||
for currency in CURRENCIES:
|
||||
if currency not in moneyed.CURRENCIES: # pragma: no cover
|
||||
logger.error("Currency code '%s' is not supported", currency)
|
||||
sys.exit(1)
|
||||
|
||||
# Custom currency exchange backend
|
||||
EXCHANGE_BACKEND = 'InvenTree.exchange.InvenTreeExchange'
|
||||
|
||||
|
@ -571,8 +571,8 @@ def update_exchange_rates(force: bool = False):
|
||||
try:
|
||||
from djmoney.contrib.exchange.models import Rate
|
||||
|
||||
from common.currency import currency_code_default, currency_codes
|
||||
from common.models import InvenTreeSetting
|
||||
from common.settings import currency_code_default, currency_codes
|
||||
from InvenTree.exchange import InvenTreeExchange
|
||||
except AppRegistryNotReady: # pragma: no cover
|
||||
# Apps not yet loaded!
|
||||
|
@ -16,7 +16,7 @@ import common.models
|
||||
import InvenTree.helpers
|
||||
import InvenTree.helpers_model
|
||||
import plugin.models
|
||||
from common.settings import currency_code_default
|
||||
from common.currency import currency_code_default
|
||||
from InvenTree import settings, version
|
||||
from plugin import registry
|
||||
from plugin.plugin import InvenTreePlugin
|
||||
|
@ -29,8 +29,8 @@ import InvenTree.format
|
||||
import InvenTree.helpers
|
||||
import InvenTree.helpers_model
|
||||
import InvenTree.tasks
|
||||
from common.currency import currency_codes
|
||||
from common.models import CustomUnit, InvenTreeSetting
|
||||
from common.settings import currency_codes
|
||||
from InvenTree.helpers_mixin import ClassProviderMixin, ClassValidationMixin
|
||||
from InvenTree.sanitizer import sanitize_svg
|
||||
from InvenTree.unit_test import InvenTreeTestCase
|
||||
|
@ -25,8 +25,8 @@ from allauth.socialaccount.views import ConnectionsView
|
||||
from djmoney.contrib.exchange.models import ExchangeBackend, Rate
|
||||
from user_sessions.views import SessionDeleteOtherView, SessionDeleteView
|
||||
|
||||
import common.currency
|
||||
import common.models as common_models
|
||||
import common.settings as common_settings
|
||||
from part.models import PartCategory
|
||||
from users.models import RuleSet, check_user_role
|
||||
|
||||
@ -506,8 +506,8 @@ class SettingsView(TemplateView):
|
||||
|
||||
ctx['settings'] = common_models.InvenTreeSetting.objects.all().order_by('key')
|
||||
|
||||
ctx['base_currency'] = common_settings.currency_code_default()
|
||||
ctx['currencies'] = common_settings.currency_codes
|
||||
ctx['base_currency'] = common.currency.currency_code_default()
|
||||
ctx['currencies'] = common.currency.currency_codes
|
||||
|
||||
ctx['rates'] = Rate.objects.filter(backend='InvenTreeExchange')
|
||||
|
||||
|
226
src/backend/InvenTree/common/currency.py
Normal file
226
src/backend/InvenTree/common/currency.py
Normal file
@ -0,0 +1,226 @@
|
||||
"""Helper functions for currency support."""
|
||||
|
||||
import decimal
|
||||
import logging
|
||||
import math
|
||||
|
||||
from django.core.cache import cache
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from moneyed import CURRENCIES
|
||||
|
||||
import InvenTree.helpers
|
||||
|
||||
logger = logging.getLogger('inventree')
|
||||
|
||||
|
||||
def currency_code_default():
|
||||
"""Returns the default currency code (or USD if not specified)."""
|
||||
from common.models import InvenTreeSetting
|
||||
|
||||
try:
|
||||
cached_value = cache.get('currency_code_default', '')
|
||||
except Exception:
|
||||
cached_value = None
|
||||
|
||||
if cached_value:
|
||||
return cached_value
|
||||
|
||||
try:
|
||||
code = InvenTreeSetting.get_setting(
|
||||
'INVENTREE_DEFAULT_CURRENCY', backup_value='', create=True, cache=True
|
||||
)
|
||||
except Exception: # pragma: no cover
|
||||
# Database may not yet be ready, no need to throw an error here
|
||||
code = ''
|
||||
|
||||
if code not in CURRENCIES:
|
||||
code = 'USD' # pragma: no cover
|
||||
|
||||
# Cache the value for a short amount of time
|
||||
try:
|
||||
cache.set('currency_code_default', code, 30)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return code
|
||||
|
||||
|
||||
def all_currency_codes() -> list:
|
||||
"""Returns a list of all currency codes."""
|
||||
return [(a, CURRENCIES[a].name) for a in CURRENCIES]
|
||||
|
||||
|
||||
def currency_codes_default_list() -> str:
|
||||
"""Return a comma-separated list of default currency codes."""
|
||||
return 'AUD,CAD,CNY,EUR,GBP,JPY,NZD,USD'
|
||||
|
||||
|
||||
def currency_codes() -> list:
|
||||
"""Returns the current currency codes."""
|
||||
from common.models import InvenTreeSetting
|
||||
|
||||
codes = InvenTreeSetting.get_setting('CURRENCY_CODES', '', create=False).strip()
|
||||
|
||||
if not codes:
|
||||
codes = currency_codes_default_list()
|
||||
|
||||
codes = codes.split(',')
|
||||
|
||||
valid_codes = set()
|
||||
|
||||
for code in codes:
|
||||
code = code.strip().upper()
|
||||
|
||||
if code in CURRENCIES:
|
||||
valid_codes.add(code)
|
||||
else:
|
||||
logger.warning(f"Invalid currency code: '{code}'")
|
||||
|
||||
if len(valid_codes) == 0:
|
||||
valid_codes = set(currency_codes_default_list().split(','))
|
||||
|
||||
return list(valid_codes)
|
||||
|
||||
|
||||
def currency_code_mappings() -> list:
|
||||
"""Returns the current currency choices."""
|
||||
return [(a, CURRENCIES[a].name) for a in currency_codes()]
|
||||
|
||||
|
||||
def after_change_currency(setting) -> None:
|
||||
"""Callback function when base currency is changed.
|
||||
|
||||
- Update exchange rates
|
||||
- Recalculate prices for all parts
|
||||
"""
|
||||
import InvenTree.ready
|
||||
import InvenTree.tasks
|
||||
|
||||
if InvenTree.ready.isImportingData():
|
||||
return
|
||||
|
||||
if not InvenTree.ready.canAppAccessDatabase():
|
||||
return
|
||||
|
||||
from part import tasks as part_tasks
|
||||
|
||||
# Immediately update exchange rates
|
||||
InvenTree.tasks.update_exchange_rates(force=True)
|
||||
|
||||
# Offload update of part prices to a background task
|
||||
InvenTree.tasks.offload_task(part_tasks.check_missing_pricing, force_async=True)
|
||||
|
||||
|
||||
def validate_currency_codes(value):
|
||||
"""Validate the currency codes."""
|
||||
values = value.strip().split(',')
|
||||
|
||||
valid_currencies = set()
|
||||
|
||||
for code in values:
|
||||
code = code.strip().upper()
|
||||
|
||||
if not code:
|
||||
continue
|
||||
|
||||
if code not in CURRENCIES:
|
||||
raise ValidationError(_('Invalid currency code') + f": '{code}'")
|
||||
elif code in valid_currencies:
|
||||
raise ValidationError(_('Duplicate currency code') + f": '{code}'")
|
||||
else:
|
||||
valid_currencies.add(code)
|
||||
|
||||
if len(valid_currencies) == 0:
|
||||
raise ValidationError(_('No valid currency codes provided'))
|
||||
|
||||
return list(valid_currencies)
|
||||
|
||||
|
||||
def currency_exchange_plugins() -> list:
|
||||
"""Return a list of plugin choices which can be used for currency exchange."""
|
||||
try:
|
||||
from plugin import registry
|
||||
|
||||
plugs = registry.with_mixin('currencyexchange', active=True)
|
||||
except Exception:
|
||||
plugs = []
|
||||
|
||||
return [('', _('No plugin'))] + [(plug.slug, plug.human_name) for plug in plugs]
|
||||
|
||||
|
||||
def get_price(
|
||||
instance,
|
||||
quantity,
|
||||
moq=True,
|
||||
multiples=True,
|
||||
currency=None,
|
||||
break_name: str = 'price_breaks',
|
||||
):
|
||||
"""Calculate the price based on quantity price breaks.
|
||||
|
||||
- Don't forget to add in flat-fee cost (base_cost field)
|
||||
- If MOQ (minimum order quantity) is required, bump quantity
|
||||
- If order multiples are to be observed, then we need to calculate based on that, too
|
||||
"""
|
||||
from common.currency import currency_code_default
|
||||
|
||||
if hasattr(instance, break_name):
|
||||
price_breaks = getattr(instance, break_name).all()
|
||||
else:
|
||||
price_breaks = []
|
||||
|
||||
# No price break information available?
|
||||
if len(price_breaks) == 0:
|
||||
return None
|
||||
|
||||
# Check if quantity is fraction and disable multiples
|
||||
multiples = quantity % 1 == 0
|
||||
|
||||
# Order multiples
|
||||
if multiples:
|
||||
quantity = int(math.ceil(quantity / instance.multiple) * instance.multiple)
|
||||
|
||||
pb_found = False
|
||||
pb_quantity = -1
|
||||
pb_cost = 0.0
|
||||
|
||||
if currency is None:
|
||||
# Default currency selection
|
||||
currency = currency_code_default()
|
||||
|
||||
pb_min = None
|
||||
for pb in price_breaks:
|
||||
# Store smallest price break
|
||||
if not pb_min:
|
||||
pb_min = pb
|
||||
|
||||
# Ignore this pricebreak (quantity is too high)
|
||||
if pb.quantity > quantity:
|
||||
continue
|
||||
|
||||
pb_found = True
|
||||
|
||||
# If this price-break quantity is the largest so far, use it!
|
||||
if pb.quantity > pb_quantity:
|
||||
pb_quantity = pb.quantity
|
||||
|
||||
# Convert everything to the selected currency
|
||||
pb_cost = pb.convert_to(currency)
|
||||
|
||||
# Use smallest price break
|
||||
if not pb_found and pb_min:
|
||||
# Update price break information
|
||||
pb_quantity = pb_min.quantity
|
||||
pb_cost = pb_min.convert_to(currency)
|
||||
# Trigger cost calculation using smallest price break
|
||||
pb_found = True
|
||||
|
||||
# Convert quantity to decimal.Decimal format
|
||||
quantity = decimal.Decimal(f'{quantity}')
|
||||
|
||||
if pb_found:
|
||||
cost = pb_cost * quantity
|
||||
return InvenTree.helpers.normalize(cost + instance.base_cost)
|
||||
return None
|
@ -9,7 +9,7 @@ def set_default_currency(apps, schema_editor):
|
||||
# get value from settings-file
|
||||
base_currency = get_setting('INVENTREE_BASE_CURRENCY', 'base_currency', 'USD')
|
||||
|
||||
from common.settings import currency_codes
|
||||
from common.currency import currency_codes
|
||||
|
||||
# check if value is valid
|
||||
if base_currency not in currency_codes():
|
||||
|
@ -0,0 +1,73 @@
|
||||
# Generated by Django 4.2.12 on 2024-06-02 13:32
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
from moneyed import CURRENCIES
|
||||
|
||||
import InvenTree.config
|
||||
|
||||
|
||||
def set_currencies(apps, schema_editor):
|
||||
"""Set the default currency codes.
|
||||
|
||||
Ref: https://github.com/inventree/InvenTree/pull/7390
|
||||
|
||||
Previously, the allowed currency codes were set in the external configuration
|
||||
(e.g via the configuration file or environment variables).
|
||||
|
||||
Now, they are set in the database (via the InvenTreeSetting model).
|
||||
|
||||
So, this data migration exists to transfer any configured currency codes,
|
||||
from the external configuration, into the database settings model.
|
||||
"""
|
||||
|
||||
InvenTreeSetting = apps.get_model('common', 'InvenTreeSetting')
|
||||
|
||||
key = 'CURRENCY_CODES'
|
||||
|
||||
codes = InvenTree.config.get_setting('INVENTREE_CURRENCIES', 'currencies', None)
|
||||
|
||||
if codes is None:
|
||||
# No currency codes are defined in the configuration file
|
||||
return
|
||||
|
||||
if type(codes) == str:
|
||||
codes = codes.split(',')
|
||||
|
||||
valid_codes = set()
|
||||
|
||||
for code in codes:
|
||||
code = code.strip().upper()
|
||||
|
||||
if code in CURRENCIES:
|
||||
valid_codes.add(code)
|
||||
|
||||
if len(valid_codes) == 0:
|
||||
print(f"No valid currency codes found in configuration file")
|
||||
return
|
||||
|
||||
value = ','.join(valid_codes)
|
||||
print(f"Found existing currency codes:", value)
|
||||
|
||||
setting = InvenTreeSetting.objects.filter(key=key).first()
|
||||
|
||||
if setting:
|
||||
print(f"Updating existing setting for currency codes")
|
||||
setting.value = value
|
||||
setting.save()
|
||||
else:
|
||||
print(f"Creating new setting for currency codes")
|
||||
setting = InvenTreeSetting(key=key, value=value)
|
||||
setting.save()
|
||||
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('common', '0022_projectcode_responsible'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(set_currencies, reverse_code=migrations.RunPython.noop)
|
||||
]
|
@ -4,12 +4,10 @@ These models are 'generic' and do not fit a particular business logic object.
|
||||
"""
|
||||
|
||||
import base64
|
||||
import decimal
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import logging
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
import uuid
|
||||
@ -41,6 +39,7 @@ from djmoney.settings import CURRENCY_CHOICES
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
|
||||
import build.validators
|
||||
import common.currency
|
||||
import InvenTree.fields
|
||||
import InvenTree.helpers
|
||||
import InvenTree.models
|
||||
@ -1160,39 +1159,6 @@ def validate_email_domains(setting):
|
||||
raise ValidationError(_(f'Invalid domain name: {domain}'))
|
||||
|
||||
|
||||
def currency_exchange_plugins():
|
||||
"""Return a set of plugin choices which can be used for currency exchange."""
|
||||
try:
|
||||
from plugin import registry
|
||||
|
||||
plugs = registry.with_mixin('currencyexchange', active=True)
|
||||
except Exception:
|
||||
plugs = []
|
||||
|
||||
return [('', _('No plugin'))] + [(plug.slug, plug.human_name) for plug in plugs]
|
||||
|
||||
|
||||
def after_change_currency(setting):
|
||||
"""Callback function when base currency is changed.
|
||||
|
||||
- Update exchange rates
|
||||
- Recalculate prices for all parts
|
||||
"""
|
||||
if InvenTree.ready.isImportingData():
|
||||
return
|
||||
|
||||
if not InvenTree.ready.canAppAccessDatabase():
|
||||
return
|
||||
|
||||
from part import tasks as part_tasks
|
||||
|
||||
# Immediately update exchange rates
|
||||
InvenTree.tasks.update_exchange_rates(force=True)
|
||||
|
||||
# Offload update of part prices to a background task
|
||||
InvenTree.tasks.offload_task(part_tasks.check_missing_pricing, force_async=True)
|
||||
|
||||
|
||||
def reload_plugin_registry(setting):
|
||||
"""When a core plugin setting is changed, reload the plugin registry."""
|
||||
from plugin import registry
|
||||
@ -1305,8 +1271,15 @@ class InvenTreeSetting(BaseInvenTreeSetting):
|
||||
'name': _('Default Currency'),
|
||||
'description': _('Select base currency for pricing calculations'),
|
||||
'default': 'USD',
|
||||
'choices': CURRENCY_CHOICES,
|
||||
'after_save': after_change_currency,
|
||||
'choices': common.currency.currency_code_mappings,
|
||||
'after_save': common.currency.after_change_currency,
|
||||
},
|
||||
'CURRENCY_CODES': {
|
||||
'name': _('Supported Currencies'),
|
||||
'description': _('List of supported currency codes'),
|
||||
'default': common.currency.currency_codes_default_list(),
|
||||
'validator': common.currency.validate_currency_codes,
|
||||
'after_save': common.currency.after_change_currency,
|
||||
},
|
||||
'CURRENCY_UPDATE_INTERVAL': {
|
||||
'name': _('Currency Update Interval'),
|
||||
@ -1320,7 +1293,7 @@ class InvenTreeSetting(BaseInvenTreeSetting):
|
||||
'CURRENCY_UPDATE_PLUGIN': {
|
||||
'name': _('Currency Update Plugin'),
|
||||
'description': _('Currency update plugin to use'),
|
||||
'choices': currency_exchange_plugins,
|
||||
'choices': common.currency.currency_exchange_plugins,
|
||||
'default': 'inventreecurrencyexchange',
|
||||
},
|
||||
'INVENTREE_DOWNLOAD_FROM_URL': {
|
||||
@ -2567,82 +2540,6 @@ class PriceBreak(MetaMixin):
|
||||
return converted.amount
|
||||
|
||||
|
||||
def get_price(
|
||||
instance,
|
||||
quantity,
|
||||
moq=True,
|
||||
multiples=True,
|
||||
currency=None,
|
||||
break_name: str = 'price_breaks',
|
||||
):
|
||||
"""Calculate the price based on quantity price breaks.
|
||||
|
||||
- Don't forget to add in flat-fee cost (base_cost field)
|
||||
- If MOQ (minimum order quantity) is required, bump quantity
|
||||
- If order multiples are to be observed, then we need to calculate based on that, too
|
||||
"""
|
||||
from common.settings import currency_code_default
|
||||
|
||||
if hasattr(instance, break_name):
|
||||
price_breaks = getattr(instance, break_name).all()
|
||||
else:
|
||||
price_breaks = []
|
||||
|
||||
# No price break information available?
|
||||
if len(price_breaks) == 0:
|
||||
return None
|
||||
|
||||
# Check if quantity is fraction and disable multiples
|
||||
multiples = quantity % 1 == 0
|
||||
|
||||
# Order multiples
|
||||
if multiples:
|
||||
quantity = int(math.ceil(quantity / instance.multiple) * instance.multiple)
|
||||
|
||||
pb_found = False
|
||||
pb_quantity = -1
|
||||
pb_cost = 0.0
|
||||
|
||||
if currency is None:
|
||||
# Default currency selection
|
||||
currency = currency_code_default()
|
||||
|
||||
pb_min = None
|
||||
for pb in price_breaks:
|
||||
# Store smallest price break
|
||||
if not pb_min:
|
||||
pb_min = pb
|
||||
|
||||
# Ignore this pricebreak (quantity is too high)
|
||||
if pb.quantity > quantity:
|
||||
continue
|
||||
|
||||
pb_found = True
|
||||
|
||||
# If this price-break quantity is the largest so far, use it!
|
||||
if pb.quantity > pb_quantity:
|
||||
pb_quantity = pb.quantity
|
||||
|
||||
# Convert everything to the selected currency
|
||||
pb_cost = pb.convert_to(currency)
|
||||
|
||||
# Use smallest price break
|
||||
if not pb_found and pb_min:
|
||||
# Update price break information
|
||||
pb_quantity = pb_min.quantity
|
||||
pb_cost = pb_min.convert_to(currency)
|
||||
# Trigger cost calculation using smallest price break
|
||||
pb_found = True
|
||||
|
||||
# Convert quantity to decimal.Decimal format
|
||||
quantity = decimal.Decimal(f'{quantity}')
|
||||
|
||||
if pb_found:
|
||||
cost = pb_cost * quantity
|
||||
return InvenTree.helpers.normalize(cost + instance.base_cost)
|
||||
return None
|
||||
|
||||
|
||||
class ColorTheme(models.Model):
|
||||
"""Color Theme Setting."""
|
||||
|
||||
|
@ -1,61 +1,5 @@
|
||||
"""User-configurable settings for the common app."""
|
||||
|
||||
import logging
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
|
||||
from moneyed import CURRENCIES
|
||||
|
||||
logger = logging.getLogger('inventree')
|
||||
|
||||
|
||||
def currency_code_default():
|
||||
"""Returns the default currency code (or USD if not specified)."""
|
||||
from common.models import InvenTreeSetting
|
||||
|
||||
try:
|
||||
cached_value = cache.get('currency_code_default', '')
|
||||
except Exception:
|
||||
cached_value = None
|
||||
|
||||
if cached_value:
|
||||
return cached_value
|
||||
|
||||
try:
|
||||
code = InvenTreeSetting.get_setting(
|
||||
'INVENTREE_DEFAULT_CURRENCY', backup_value='', create=True, cache=True
|
||||
)
|
||||
except Exception: # pragma: no cover
|
||||
# Database may not yet be ready, no need to throw an error here
|
||||
code = ''
|
||||
|
||||
if code not in CURRENCIES:
|
||||
code = 'USD' # pragma: no cover
|
||||
|
||||
# Cache the value for a short amount of time
|
||||
try:
|
||||
cache.set('currency_code_default', code, 30)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return code
|
||||
|
||||
|
||||
def all_currency_codes():
|
||||
"""Returns a list of all currency codes."""
|
||||
return [(a, CURRENCIES[a].name) for a in CURRENCIES]
|
||||
|
||||
|
||||
def currency_code_mappings():
|
||||
"""Returns the current currency choices."""
|
||||
return [(a, CURRENCIES[a].name) for a in settings.CURRENCIES]
|
||||
|
||||
|
||||
def currency_codes():
|
||||
"""Returns the current currency codes."""
|
||||
return list(settings.CURRENCIES)
|
||||
|
||||
|
||||
def stock_expiry_enabled():
|
||||
"""Returns True if the stock expiry feature is enabled."""
|
||||
|
@ -372,6 +372,30 @@ class GlobalSettingsApiTest(InvenTreeAPITestCase):
|
||||
# Number of results should match the number of settings
|
||||
self.assertEqual(len(response.data), n_public_settings)
|
||||
|
||||
def test_currency_settings(self):
|
||||
"""Run tests for currency specific settings."""
|
||||
url = reverse('api-global-setting-detail', kwargs={'key': 'CURRENCY_CODES'})
|
||||
|
||||
response = self.patch(url, data={'value': 'USD,XYZ'}, expected_code=400)
|
||||
|
||||
self.assertIn("Invalid currency code: 'XYZ'", str(response.data))
|
||||
|
||||
response = self.patch(
|
||||
url, data={'value': 'AUD,USD, AUD,AUD,'}, expected_code=400
|
||||
)
|
||||
|
||||
self.assertIn("Duplicate currency code: 'AUD'", str(response.data))
|
||||
|
||||
response = self.patch(url, data={'value': ',,,,,'}, expected_code=400)
|
||||
|
||||
self.assertIn('No valid currency codes provided', str(response.data))
|
||||
|
||||
response = self.patch(url, data={'value': 'AUD,USD,GBP'}, expected_code=200)
|
||||
|
||||
codes = InvenTreeSetting.get_setting('CURRENCY_CODES')
|
||||
|
||||
self.assertEqual(codes, 'AUD,USD,GBP')
|
||||
|
||||
def test_company_name(self):
|
||||
"""Test a settings object lifecycle e2e."""
|
||||
setting = InvenTreeSetting.get_setting_object('INVENTREE_COMPANY_NAME')
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
from django.db import migrations, connection
|
||||
import djmoney.models.fields
|
||||
import common.currency
|
||||
import common.settings
|
||||
|
||||
|
||||
@ -16,11 +17,11 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name='supplierpricebreak',
|
||||
name='price',
|
||||
field=djmoney.models.fields.MoneyField(decimal_places=4, default_currency=common.settings.currency_code_default(), help_text='Unit price at specified quantity', max_digits=19, null=True, verbose_name='Price'),
|
||||
field=djmoney.models.fields.MoneyField(decimal_places=4, default_currency=common.currency.currency_code_default(), help_text='Unit price at specified quantity', max_digits=19, null=True, verbose_name='Price'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='supplierpricebreak',
|
||||
name='price_currency',
|
||||
field=djmoney.models.fields.CurrencyField(choices=common.settings.currency_code_mappings(), default=common.settings.currency_code_default(), editable=False, max_length=3),
|
||||
field=djmoney.models.fields.CurrencyField(choices=common.currency.currency_code_mappings(), default=common.currency.currency_code_default(), editable=False, max_length=3),
|
||||
),
|
||||
]
|
||||
|
@ -1,6 +1,7 @@
|
||||
# Generated by Django 3.2.4 on 2021-07-02 13:21
|
||||
|
||||
import InvenTree.validators
|
||||
import common.currency
|
||||
import common.settings
|
||||
from django.db import migrations, models
|
||||
|
||||
@ -15,6 +16,6 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name='company',
|
||||
name='currency',
|
||||
field=models.CharField(blank=True, default=common.settings.currency_code_default, help_text='Default currency used for this company', max_length=3, validators=[InvenTree.validators.validate_currency_code], verbose_name='Currency'),
|
||||
field=models.CharField(blank=True, default=common.currency.currency_code_default, help_text='Default currency used for this company', max_length=3, validators=[InvenTree.validators.validate_currency_code], verbose_name='Currency'),
|
||||
),
|
||||
]
|
||||
|
@ -1,7 +1,6 @@
|
||||
"""Company database model definitions."""
|
||||
|
||||
import os
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
|
||||
from django.apps import apps
|
||||
@ -20,6 +19,7 @@ from moneyed import CURRENCIES
|
||||
from stdimage.models import StdImageField
|
||||
from taggit.managers import TaggableManager
|
||||
|
||||
import common.currency
|
||||
import common.models
|
||||
import common.settings
|
||||
import InvenTree.conversion
|
||||
@ -29,7 +29,7 @@ import InvenTree.models
|
||||
import InvenTree.ready
|
||||
import InvenTree.tasks
|
||||
import InvenTree.validators
|
||||
from common.settings import currency_code_default
|
||||
from common.currency import currency_code_default
|
||||
from InvenTree.fields import InvenTreeURLField, RoundingDecimalField
|
||||
from order.status_codes import PurchaseOrderStatusGroups
|
||||
|
||||
@ -212,7 +212,7 @@ class Company(
|
||||
code = self.currency
|
||||
|
||||
if code not in CURRENCIES:
|
||||
code = common.settings.currency_code_default()
|
||||
code = common.currency.currency_code_default()
|
||||
|
||||
return code
|
||||
|
||||
@ -967,7 +967,7 @@ class SupplierPart(
|
||||
|
||||
SupplierPriceBreak.objects.create(part=self, quantity=quantity, price=price)
|
||||
|
||||
get_price = common.models.get_price
|
||||
get_price = common.currency.get_price
|
||||
|
||||
def open_orders(self):
|
||||
"""Return a database query for PurchaseOrder line items for this SupplierPart, limited to purchase orders that are open / outstanding."""
|
||||
|
@ -46,10 +46,7 @@ language: en-us
|
||||
timezone: UTC
|
||||
|
||||
# Base URL for the InvenTree server (or use the environment variable INVENTREE_SITE_URL)
|
||||
# site_url: 'http://localhost:8000'
|
||||
|
||||
# Base currency code (or use env var INVENTREE_BASE_CURRENCY)
|
||||
base_currency: USD
|
||||
site_url: 'http://localhost:8000'
|
||||
|
||||
# Add new user on first startup by either adding values here or from a file
|
||||
#admin_user: admin
|
||||
@ -57,17 +54,6 @@ base_currency: USD
|
||||
#admin_password: inventree
|
||||
#admin_password_file: '/etc/inventree/admin_password.txt'
|
||||
|
||||
# List of currencies supported by default. Add other currencies here to allow use in InvenTree
|
||||
currencies:
|
||||
- AUD
|
||||
- CAD
|
||||
- CNY
|
||||
- EUR
|
||||
- GBP
|
||||
- JPY
|
||||
- NZD
|
||||
- USD
|
||||
|
||||
# Email backend configuration
|
||||
# Ref: https://docs.djangoproject.com/en/dev/topics/email/
|
||||
# Alternatively, these options can all be set using environment variables,
|
||||
|
@ -3,6 +3,7 @@
|
||||
from decimal import Decimal
|
||||
from typing import cast
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import authenticate, login
|
||||
from django.db import transaction
|
||||
from django.db.models import F, Q
|
||||
@ -17,7 +18,6 @@ from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.response import Response
|
||||
|
||||
import common.models as common_models
|
||||
from common.settings import settings
|
||||
from company.models import SupplierPart
|
||||
from generic.states.api import StatusView
|
||||
from InvenTree.api import (
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
from django.db import migrations
|
||||
import djmoney.models.fields
|
||||
import common.currency
|
||||
import common.settings
|
||||
|
||||
|
||||
@ -16,11 +17,11 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name='purchaseorderlineitem',
|
||||
name='purchase_price',
|
||||
field=djmoney.models.fields.MoneyField(decimal_places=4, default_currency=common.settings.currency_code_default(), help_text='Unit purchase price', max_digits=19, null=True, verbose_name='Purchase Price'),
|
||||
field=djmoney.models.fields.MoneyField(decimal_places=4, default_currency=common.currency.currency_code_default(), help_text='Unit purchase price', max_digits=19, null=True, verbose_name='Purchase Price'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='purchaseorderlineitem',
|
||||
name='purchase_price_currency',
|
||||
field=djmoney.models.fields.CurrencyField(choices=common.settings.currency_code_mappings(), default=common.settings.currency_code_default(), editable=False, max_length=3),
|
||||
field=djmoney.models.fields.CurrencyField(choices=common.currency.currency_code_mappings(), default=common.currency.currency_code_default(), editable=False, max_length=3),
|
||||
),
|
||||
]
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
from django.db import migrations
|
||||
import djmoney.models.fields
|
||||
import common.currency
|
||||
import common.settings
|
||||
|
||||
|
||||
@ -15,6 +16,6 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name='purchaseorderlineitem',
|
||||
name='purchase_price',
|
||||
field=djmoney.models.fields.MoneyField(blank=True, decimal_places=4, default_currency=common.settings.currency_code_default(), help_text='Unit purchase price', max_digits=19, null=True, verbose_name='Purchase Price'),
|
||||
field=djmoney.models.fields.MoneyField(blank=True, decimal_places=4, default_currency=common.currency.currency_code_default(), help_text='Unit purchase price', max_digits=19, null=True, verbose_name='Purchase Price'),
|
||||
),
|
||||
]
|
||||
|
@ -1,6 +1,7 @@
|
||||
# Generated by Django 3.2 on 2021-05-04 19:46
|
||||
|
||||
from django.db import migrations
|
||||
import common.currency
|
||||
import common.settings
|
||||
import djmoney.models.fields
|
||||
|
||||
@ -15,11 +16,11 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name='salesorderlineitem',
|
||||
name='sale_price',
|
||||
field=djmoney.models.fields.MoneyField(blank=True, decimal_places=4, default_currency=common.settings.currency_code_default(), help_text='Unit sale price', max_digits=19, null=True, verbose_name='Sale Price'),
|
||||
field=djmoney.models.fields.MoneyField(blank=True, decimal_places=4, default_currency=common.currency.currency_code_default(), help_text='Unit sale price', max_digits=19, null=True, verbose_name='Sale Price'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='salesorderlineitem',
|
||||
name='sale_price_currency',
|
||||
field=djmoney.models.fields.CurrencyField(choices=common.settings.currency_code_mappings(), default=common.settings.currency_code_default(), editable=False, max_length=3),
|
||||
field=djmoney.models.fields.CurrencyField(choices=common.currency.currency_code_mappings(), default=common.currency.currency_code_default(), editable=False, max_length=3),
|
||||
),
|
||||
]
|
||||
|
@ -8,7 +8,7 @@ from djmoney.contrib.exchange.exceptions import MissingRate
|
||||
from djmoney.contrib.exchange.models import convert_money
|
||||
from djmoney.money import Money
|
||||
|
||||
from common.settings import currency_code_default
|
||||
from common.currency import currency_code_default
|
||||
|
||||
|
||||
logger = logging.getLogger('inventree')
|
||||
|
@ -33,8 +33,8 @@ import order.validators
|
||||
import report.mixins
|
||||
import stock.models
|
||||
import users.models as UserModels
|
||||
from common.currency import currency_code_default
|
||||
from common.notifications import InvenTreeNotificationBodies
|
||||
from common.settings import currency_code_default
|
||||
from company.models import Address, Company, Contact, SupplierPart
|
||||
from generic.states import StateTransitionMixin
|
||||
from InvenTree.exceptions import log_error
|
||||
|
@ -13,8 +13,8 @@ from djmoney.money import Money
|
||||
from icalendar import Calendar
|
||||
from rest_framework import status
|
||||
|
||||
from common.currency import currency_codes
|
||||
from common.models import InvenTreeSetting
|
||||
from common.settings import currency_codes
|
||||
from company.models import Company, SupplierPart, SupplierPriceBreak
|
||||
from InvenTree.unit_test import InvenTreeAPITestCase
|
||||
from order import models
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
from django.db import migrations
|
||||
import djmoney.models.fields
|
||||
import common.settings
|
||||
import common.currency
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
@ -16,11 +16,11 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name='partsellpricebreak',
|
||||
name='price',
|
||||
field=djmoney.models.fields.MoneyField(decimal_places=4, default_currency=common.settings.currency_code_default(), help_text='Unit price at specified quantity', max_digits=19, null=True, verbose_name='Price'),
|
||||
field=djmoney.models.fields.MoneyField(decimal_places=4, default_currency=common.currency.currency_code_default(), help_text='Unit price at specified quantity', max_digits=19, null=True, verbose_name='Price'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='partsellpricebreak',
|
||||
name='price_currency',
|
||||
field=djmoney.models.fields.CurrencyField(choices=common.settings.currency_code_mappings(), default=common.settings.currency_code_default(), editable=False, max_length=3),
|
||||
field=djmoney.models.fields.CurrencyField(choices=common.currency.currency_code_mappings(), default=common.currency.currency_code_default(), editable=False, max_length=3),
|
||||
),
|
||||
]
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
import InvenTree.fields
|
||||
import django.core.validators
|
||||
import common.currency
|
||||
import common.settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
@ -20,8 +21,8 @@ class Migration(migrations.Migration):
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('quantity', InvenTree.fields.RoundingDecimalField(decimal_places=5, default=1, help_text='Price break quantity', max_digits=15, validators=[django.core.validators.MinValueValidator(1)], verbose_name='Quantity')),
|
||||
('price_currency', djmoney.models.fields.CurrencyField(choices=common.settings.currency_code_mappings(), default=common.settings.currency_code_default(), editable=False, max_length=3)),
|
||||
('price', djmoney.models.fields.MoneyField(decimal_places=4, default_currency=common.settings.currency_code_default(), help_text='Unit price at specified quantity', max_digits=19, null=True, verbose_name='Price')),
|
||||
('price_currency', djmoney.models.fields.CurrencyField(choices=common.currency.currency_code_mappings(), default=common.currency.currency_code_default(), editable=False, max_length=3)),
|
||||
('price', djmoney.models.fields.MoneyField(decimal_places=4, default_currency=common.currency.currency_code_default(), help_text='Unit price at specified quantity', max_digits=19, null=True, verbose_name='Price')),
|
||||
('part', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='internalpricebreaks', to='part.part', verbose_name='Part')),
|
||||
],
|
||||
options={
|
||||
|
@ -1,13 +1,14 @@
|
||||
# Generated by Django 3.2.16 on 2022-11-12 01:28
|
||||
|
||||
import InvenTree.fields
|
||||
import common.settings
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import djmoney.models.fields
|
||||
import djmoney.models.validators
|
||||
|
||||
import InvenTree.fields
|
||||
import common.currency
|
||||
import common.settings
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
@ -35,7 +36,7 @@ class Migration(migrations.Migration):
|
||||
name='PartPricing',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('currency', models.CharField(choices=common.settings.currency_code_mappings(), default=common.settings.currency_code_default, help_text='Currency used to cache pricing calculations', max_length=10, verbose_name='Currency')),
|
||||
('currency', models.CharField(choices=common.currency.currency_code_mappings(), default=common.currency.currency_code_default, help_text='Currency used to cache pricing calculations', max_length=10, verbose_name='Currency')),
|
||||
('updated', models.DateTimeField(auto_now=True, help_text='Timestamp of last pricing update', verbose_name='Updated')),
|
||||
('scheduled_for_update', models.BooleanField(default=False)),
|
||||
('bom_cost_min_currency', djmoney.models.fields.CurrencyField(choices=[], default='', editable=False, max_length=3)),
|
||||
|
@ -33,6 +33,7 @@ from mptt.models import MPTTModel, TreeForeignKey
|
||||
from stdimage.models import StdImageField
|
||||
from taggit.managers import TaggableManager
|
||||
|
||||
import common.currency
|
||||
import common.models
|
||||
import common.settings
|
||||
import InvenTree.conversion
|
||||
@ -47,8 +48,8 @@ import report.mixins
|
||||
import users.models
|
||||
from build import models as BuildModels
|
||||
from build.status_codes import BuildStatusGroups
|
||||
from common.currency import currency_code_default
|
||||
from common.models import InvenTreeSetting
|
||||
from common.settings import currency_code_default
|
||||
from company.models import SupplierPart
|
||||
from InvenTree import helpers, validators
|
||||
from InvenTree.fields import InvenTreeURLField
|
||||
@ -2018,7 +2019,7 @@ class Part(
|
||||
help_text=_('Sell multiple'),
|
||||
)
|
||||
|
||||
get_price = common.models.get_price
|
||||
get_price = common.currency.get_price
|
||||
|
||||
@property
|
||||
def has_price_breaks(self):
|
||||
@ -2050,7 +2051,7 @@ class Part(
|
||||
|
||||
def get_internal_price(self, quantity, moq=True, multiples=True, currency=None):
|
||||
"""Return the internal price of this Part at the specified quantity."""
|
||||
return common.models.get_price(
|
||||
return common.currency.get_price(
|
||||
self, quantity, moq, multiples, currency, break_name='internal_price_breaks'
|
||||
)
|
||||
|
||||
@ -2646,7 +2647,7 @@ class PartPricing(common.models.MetaMixin):
|
||||
# Short circuit - no further operations required
|
||||
return
|
||||
|
||||
currency_code = common.settings.currency_code_default()
|
||||
currency_code = common.currency.currency_code_default()
|
||||
|
||||
cumulative_min = Money(0, currency_code)
|
||||
cumulative_max = Money(0, currency_code)
|
||||
@ -3025,7 +3026,7 @@ class PartPricing(common.models.MetaMixin):
|
||||
max_length=10,
|
||||
verbose_name=_('Currency'),
|
||||
help_text=_('Currency used to cache pricing calculations'),
|
||||
choices=common.settings.currency_code_mappings(),
|
||||
choices=common.currency.currency_code_mappings(),
|
||||
)
|
||||
|
||||
scheduled_for_update = models.BooleanField(default=False)
|
||||
|
@ -21,6 +21,7 @@ from rest_framework import serializers
|
||||
from sql_util.utils import SubqueryCount, SubquerySum
|
||||
from taggit.serializers import TagListSerializerField
|
||||
|
||||
import common.currency
|
||||
import common.models
|
||||
import common.settings
|
||||
import company.models
|
||||
@ -1286,7 +1287,7 @@ class PartPricingSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
||||
label=_('Minimum price currency'),
|
||||
read_only=False,
|
||||
required=False,
|
||||
choices=common.settings.currency_code_mappings(),
|
||||
choices=common.currency.currency_code_mappings(),
|
||||
)
|
||||
|
||||
override_max = InvenTree.serializers.InvenTreeMoneySerializer(
|
||||
@ -1301,7 +1302,7 @@ class PartPricingSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
||||
label=_('Maximum price currency'),
|
||||
read_only=False,
|
||||
required=False,
|
||||
choices=common.settings.currency_code_mappings(),
|
||||
choices=common.currency.currency_code_mappings(),
|
||||
)
|
||||
|
||||
overall_min = InvenTree.serializers.InvenTreeMoneySerializer(
|
||||
@ -1342,7 +1343,7 @@ class PartPricingSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
||||
override_min = data.get('override_min', None)
|
||||
override_max = data.get('override_max', None)
|
||||
|
||||
default_currency = common.settings.currency_code_default()
|
||||
default_currency = common.currency.currency_code_default()
|
||||
|
||||
if override_min is not None and override_max is not None:
|
||||
try:
|
||||
|
@ -13,6 +13,7 @@ import tablib
|
||||
from djmoney.contrib.exchange.models import convert_money
|
||||
from djmoney.money import Money
|
||||
|
||||
import common.currency
|
||||
import common.models
|
||||
import InvenTree.helpers
|
||||
import part.models
|
||||
@ -67,7 +68,7 @@ def perform_stocktake(
|
||||
pricing.update_pricing(cascade=False)
|
||||
pricing.refresh_from_db()
|
||||
|
||||
base_currency = common.settings.currency_code_default()
|
||||
base_currency = common.currency.currency_code_default()
|
||||
|
||||
# Keep track of total quantity and cost for this part
|
||||
total_quantity = 0
|
||||
@ -210,7 +211,7 @@ def generate_stocktake_report(**kwargs):
|
||||
|
||||
logger.info('Generating new stocktake report for %s parts', n_parts)
|
||||
|
||||
base_currency = common.settings.currency_code_default()
|
||||
base_currency = common.currency.currency_code_default()
|
||||
|
||||
# Construct an initial dataset for the stocktake report
|
||||
dataset = tablib.Dataset(
|
||||
|
@ -8,6 +8,7 @@ from datetime import datetime, timedelta
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
import common.currency
|
||||
import common.models
|
||||
import common.notifications
|
||||
import common.settings
|
||||
@ -110,7 +111,7 @@ def check_missing_pricing(limit=250):
|
||||
pp.schedule_for_update()
|
||||
|
||||
# Find any pricing data which is in the wrong currency
|
||||
currency = common.settings.currency_code_default()
|
||||
currency = common.currency.currency_code_default()
|
||||
results = part.models.PartPricing.objects.exclude(currency=currency)
|
||||
|
||||
if results.count() > 0:
|
||||
|
@ -5,6 +5,7 @@ from django.core.exceptions import ObjectDoesNotExist
|
||||
from djmoney.contrib.exchange.models import convert_money
|
||||
from djmoney.money import Money
|
||||
|
||||
import common.currency
|
||||
import common.models
|
||||
import common.settings
|
||||
import company.models
|
||||
@ -179,7 +180,7 @@ class PartPricingTests(InvenTreeTestCase):
|
||||
self.assertIsNone(pricing.internal_cost_min)
|
||||
self.assertIsNone(pricing.internal_cost_max)
|
||||
|
||||
currency = common.settings.currency_code_default()
|
||||
currency = common.currency.currency_code_default()
|
||||
|
||||
for ii in range(5):
|
||||
# Let's add some internal price breaks
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
from django.db import migrations
|
||||
import djmoney.models.fields
|
||||
import common.settings
|
||||
import common.currency
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
@ -16,11 +16,11 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name='stockitem',
|
||||
name='purchase_price',
|
||||
field=djmoney.models.fields.MoneyField(decimal_places=4, default_currency=common.settings.currency_code_default(), help_text='Single unit purchase price at time of purchase', max_digits=19, null=True, verbose_name='Purchase Price'),
|
||||
field=djmoney.models.fields.MoneyField(decimal_places=4, default_currency=common.currency.currency_code_default(), help_text='Single unit purchase price at time of purchase', max_digits=19, null=True, verbose_name='Purchase Price'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='stockitem',
|
||||
name='purchase_price_currency',
|
||||
field=djmoney.models.fields.CurrencyField(choices=common.settings.all_currency_codes(), default=common.settings.currency_code_default(), editable=False, max_length=3),
|
||||
field=djmoney.models.fields.CurrencyField(choices=common.currency.all_currency_codes(), default=common.currency.currency_code_default(), editable=False, max_length=3),
|
||||
),
|
||||
]
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
from django.db import migrations
|
||||
import djmoney.models.fields
|
||||
import common.settings
|
||||
import common.currency
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
@ -15,6 +15,6 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name='stockitem',
|
||||
name='purchase_price',
|
||||
field=djmoney.models.fields.MoneyField(blank=True, decimal_places=4, default_currency=common.settings.currency_code_default(), help_text='Single unit purchase price at time of purchase', max_digits=19, null=True, verbose_name='Purchase Price'),
|
||||
field=djmoney.models.fields.MoneyField(blank=True, decimal_places=4, default_currency=common.currency.currency_code_default(), help_text='Single unit purchase price at time of purchase', max_digits=19, null=True, verbose_name='Purchase Price'),
|
||||
),
|
||||
]
|
||||
|
@ -12,6 +12,7 @@
|
||||
<table class='table table-striped table-condensed'>
|
||||
<tbody>
|
||||
{% include "InvenTree/settings/setting.html" with key="INVENTREE_DEFAULT_CURRENCY" icon="fa-globe" %}
|
||||
{% include "InvenTree/settings/setting.html" with key="CURRENCY_CODES" %}
|
||||
{% include "InvenTree/settings/setting.html" with key="PART_INTERNAL_PRICE" %}
|
||||
{% include "InvenTree/settings/setting.html" with key="PART_BOM_USE_INTERNAL_PRICE" %}
|
||||
{% include "InvenTree/settings/setting.html" with key="PRICING_DECIMAL_PLACES_MIN" icon='fa-dollar-sign' %}
|
||||
|
@ -109,6 +109,7 @@ export default function SystemSettings() {
|
||||
<GlobalSettingList
|
||||
keys={[
|
||||
'INVENTREE_DEFAULT_CURRENCY',
|
||||
'CURRENCY_CODES',
|
||||
'PART_INTERNAL_PRICE',
|
||||
'PART_BOM_USE_INTERNAL_PRICE',
|
||||
'PRICING_DECIMAL_PLACES_MIN',
|
||||
|
Loading…
Reference in New Issue
Block a user