Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/docs/settings/global.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ Configuration of pricing data and currency support:
{{ globalsetting("CURRENCY_CODES") }}
{{ globalsetting("CURRENCY_UPDATE_PLUGIN") }}
{{ globalsetting("CURRENCY_UPDATE_INTERVAL") }}
{{ globalsetting("PRICING_PLUGIN") }}
{{ globalsetting("PRICING_DECIMAL_PLACES_MIN") }}
{{ globalsetting("PRICING_DECIMAL_PLACES") }}
{{ globalsetting("PRICING_AUTO_UPDATE") }}
Expand Down
14 changes: 7 additions & 7 deletions src/backend/InvenTree/InvenTree/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,6 @@
from djmoney.money import Money
from PIL import Image

from common.currency import currency_code_default

from .settings import MEDIA_URL, STATIC_URL

logger = structlog.get_logger('inventree')
Expand Down Expand Up @@ -399,7 +397,7 @@ def decimal2string(d):
return s.rstrip('0').rstrip('.')


def decimal2money(d, currency=None):
def decimal2money(d, currency: Optional[str] = None) -> Money:
"""Format a Decimal number as Money.

Args:
Expand All @@ -409,12 +407,12 @@ def decimal2money(d, currency=None):
Returns:
A Money object from the input(s)
"""
if not currency:
currency = currency_code_default()
return Money(d, currency)
from common.currency import currency_code_default

return Money(d, currency or currency_code_default())


def WrapWithQuotes(text, quote='"'):
def WrapWithQuotes(text: str, quote: str = '"') -> str:
"""Wrap the supplied text with quotes.

Args:
Expand All @@ -424,6 +422,8 @@ def WrapWithQuotes(text, quote='"'):
Returns:
Supplied text wrapped in quote char
"""
text = str(text)

if not text.startswith(quote):
text = quote + text

Expand Down
43 changes: 34 additions & 9 deletions src/backend/InvenTree/common/currency.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
from django.utils.translation import gettext_lazy as _

import structlog
from djmoney.contrib.exchange.exceptions import MissingRate
from djmoney.contrib.exchange.models import convert_money
from djmoney.money import Money
from moneyed import CURRENCIES

import InvenTree.helpers
Expand Down Expand Up @@ -131,19 +134,41 @@ def validate_currency_codes(value):
return list(valid_currencies)


def currency_exchange_plugins() -> Optional[list]:
"""Return a list of plugin choices which can be used for currency exchange."""
try:
from plugin import PluginMixinEnum, registry
def convert_currency(
money: Money, currency: Optional[str] = None, raise_error: bool = False
) -> Money:
"""Convert a Money object to the specified currency.

plugs = registry.with_mixin(PluginMixinEnum.CURRENCY_EXCHANGE, active=True)
except Exception:
plugs = []
Arguments:
money: The Money object to convert
currency: The target currency code (e.g. 'USD').
raise_error: If True, raise an exception if conversion fails.

if len(plugs) == 0:
If no currency is specified, convert to the default currency.
"""
if money is None:
return None

return [('', _('No plugin'))] + [(plug.slug, plug.human_name) for plug in plugs]
if currency is None:
currency = currency_code_default()

target_currency = currency_code_default()

try:
result = convert_money(money, target_currency)
except MissingRate as exc:
logger.warning(
'No currency conversion rate available for %s -> %s',
money.currency,
target_currency,
)

if raise_error:
raise exc

result = None

return result


def get_price(
Expand Down
38 changes: 38 additions & 0 deletions src/backend/InvenTree/common/pricing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"""Helper functions for pricing support."""

from dataclasses import dataclass

from djmoney.money import Money

from common.settings import get_global_setting
from plugin import PluginMixinEnum, registry
from plugin.models import InvenTreePlugin


@dataclass
class PriceRangeTuple:
"""Dataclass representing a price range."""

min: Money
max: Money


def get_pricing_plugin() -> InvenTreePlugin:
"""Return the selected pricing plugin.

Attempts to retrieve the currently selected pricing plugin from the plugin registry.
If the plugin is not available, or is disabled,
then return the default InvenTreePricing plugin.
"""
default_slug = 'inventree-pricing'

plugin_slug = get_global_setting('PRICING_PLUGIN', backup_value=default_slug)

plugin = registry.get_plugin(plugin_slug, with_mixin=PluginMixinEnum.PRICING)

if plugin is None:
plugin = registry.get_plugin(default_slug, with_mixin=PluginMixinEnum.PRICING)

# TODO: Handle case where default plugin is missing?

return plugin
38 changes: 27 additions & 11 deletions src/backend/InvenTree/common/setting/system.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import os
import re
import uuid
from typing import Optional

from django.conf import settings as django_settings
from django.contrib.auth.models import Group
Expand All @@ -15,7 +16,6 @@

import build.validators
import common.currency
import common.models
import common.validators
import order.validators
import report.helpers
Expand Down Expand Up @@ -107,18 +107,28 @@ def reload_plugin_registry(setting):
registry.reload_plugins(full_reload=True, force_reload=True, collect=True)


def barcode_plugins() -> list:
def currency_exchange_plugins() -> Optional[list]:
"""Return a list of plugin choices which can be used for currency exchange."""
from plugin import PluginMixinEnum
from plugin.registry import get_plugin_options

return get_plugin_options(PluginMixinEnum.CURRENCY_EXCHANGE, allow_null=True)


def barcode_plugins() -> Optional[list]:
"""Return a list of plugin choices which can be used for barcode generation."""
try:
from plugin import PluginMixinEnum, registry
from plugin import PluginMixinEnum
from plugin.registry import get_plugin_options

plugins = registry.with_mixin(PluginMixinEnum.BARCODE, active=True)
except Exception: # pragma: no cover
plugins = []
return get_plugin_options(PluginMixinEnum.BARCODE, allow_null=False)

return [
(plug.slug, plug.human_name) for plug in plugins if plug.has_barcode_generation
]

def pricing_plugins() -> Optional[list]:
"""Return a list of plugin choices which can be used for pricing calculations."""
from plugin import PluginMixinEnum
from plugin.registry import get_plugin_options

return get_plugin_options(PluginMixinEnum.PRICING, allow_null=False)


def default_uuid4() -> str:
Expand Down Expand Up @@ -257,7 +267,7 @@ class SystemSetId:
'CURRENCY_UPDATE_PLUGIN': {
'name': _('Currency Update Plugin'),
'description': _('Currency update plugin to use'),
'choices': common.currency.currency_exchange_plugins,
'choices': currency_exchange_plugins,
'default': 'inventreecurrencyexchange',
},
'INVENTREE_DOWNLOAD_FROM_URL': {
Expand Down Expand Up @@ -530,6 +540,12 @@ class SystemSetId:
'default': True,
'validator': bool,
},
'PRICING_PLUGIN': {
'name': _('Pricing Plugin'),
'description': _('Plugin to use for pricing calculations'),
'choices': pricing_plugins,
'default': 'inventree-pricing',
},
'PRICING_DECIMAL_PLACES_MIN': {
'name': _('Minimum Pricing Decimal Places'),
'description': _(
Expand Down
Loading
Loading