mirror of
https://github.com/inventree/InvenTree.git
synced 2025-04-29 12:06:44 +00:00
Refactor model helpers into own file (#4927)
* Refactor model helpers into own file to allow helper import when apps not loaded yet * Import helper functions at module level * Added missing imports where vscode couldnt help because its no explicit import
This commit is contained in:
parent
a196f443a1
commit
99d122baa9
@ -12,11 +12,14 @@ from djmoney.models.fields import MoneyField as ModelMoneyField
|
|||||||
from djmoney.models.validators import MinMoneyValidator
|
from djmoney.models.validators import MinMoneyValidator
|
||||||
from rest_framework.fields import URLField as RestURLField
|
from rest_framework.fields import URLField as RestURLField
|
||||||
|
|
||||||
|
import InvenTree.helpers
|
||||||
|
|
||||||
from .validators import AllowedURLValidator, allowable_url_schemes
|
from .validators import AllowedURLValidator, allowable_url_schemes
|
||||||
|
|
||||||
|
|
||||||
class InvenTreeRestURLField(RestURLField):
|
class InvenTreeRestURLField(RestURLField):
|
||||||
"""Custom field for DRF with custom scheme vaildators."""
|
"""Custom field for DRF with custom scheme vaildators."""
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
"""Update schemes."""
|
"""Update schemes."""
|
||||||
|
|
||||||
@ -109,6 +112,7 @@ class InvenTreeModelMoneyField(ModelMoneyField):
|
|||||||
|
|
||||||
class InvenTreeMoneyField(MoneyField):
|
class InvenTreeMoneyField(MoneyField):
|
||||||
"""Custom MoneyField for clean migrations while using dynamic currency settings."""
|
"""Custom MoneyField for clean migrations while using dynamic currency settings."""
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
"""Override initial values with the real info from database."""
|
"""Override initial values with the real info from database."""
|
||||||
kwargs.update(money_kwargs())
|
kwargs.update(money_kwargs())
|
||||||
@ -148,8 +152,6 @@ class DatePickerFormField(forms.DateField):
|
|||||||
def round_decimal(value, places, normalize=False):
|
def round_decimal(value, places, normalize=False):
|
||||||
"""Round value to the specified number of places."""
|
"""Round value to the specified number of places."""
|
||||||
|
|
||||||
import InvenTree.helpers
|
|
||||||
|
|
||||||
if type(value) in [Decimal, float]:
|
if type(value) in [Decimal, float]:
|
||||||
value = round(value, places)
|
value = round(value, places)
|
||||||
|
|
||||||
|
@ -14,23 +14,15 @@ from django.conf import settings
|
|||||||
from django.contrib.staticfiles.storage import StaticFilesStorage
|
from django.contrib.staticfiles.storage import StaticFilesStorage
|
||||||
from django.core.exceptions import FieldError, ValidationError
|
from django.core.exceptions import FieldError, ValidationError
|
||||||
from django.core.files.storage import default_storage
|
from django.core.files.storage import default_storage
|
||||||
from django.core.validators import URLValidator
|
|
||||||
from django.db.utils import OperationalError, ProgrammingError
|
|
||||||
from django.http import StreamingHttpResponse
|
from django.http import StreamingHttpResponse
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
import moneyed.localization
|
|
||||||
import regex
|
import regex
|
||||||
import requests
|
|
||||||
from bleach import clean
|
from bleach import clean
|
||||||
from djmoney.contrib.exchange.models import convert_money
|
|
||||||
from djmoney.money import Money
|
from djmoney.money import Money
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
import common.models
|
|
||||||
import InvenTree.version
|
import InvenTree.version
|
||||||
from common.notifications import (InvenTreeNotificationBodies,
|
|
||||||
NotificationBody, trigger_notification)
|
|
||||||
from common.settings import currency_code_default
|
from common.settings import currency_code_default
|
||||||
|
|
||||||
from .settings import MEDIA_URL, STATIC_URL
|
from .settings import MEDIA_URL, STATIC_URL
|
||||||
@ -38,11 +30,6 @@ from .settings import MEDIA_URL, STATIC_URL
|
|||||||
logger = logging.getLogger('inventree')
|
logger = logging.getLogger('inventree')
|
||||||
|
|
||||||
|
|
||||||
def getSetting(key, backup_value=None):
|
|
||||||
"""Shortcut for reading a setting value from the database."""
|
|
||||||
return common.models.InvenTreeSetting.get_setting(key, backup_value=backup_value)
|
|
||||||
|
|
||||||
|
|
||||||
def generateTestKey(test_name):
|
def generateTestKey(test_name):
|
||||||
"""Generate a test 'key' for a given test name. This must not have illegal chars as it will be used for dict lookup in a template.
|
"""Generate a test 'key' for a given test name. This must not have illegal chars as it will be used for dict lookup in a template.
|
||||||
|
|
||||||
@ -85,156 +72,6 @@ def getStaticUrl(filename):
|
|||||||
return os.path.join(STATIC_URL, str(filename))
|
return os.path.join(STATIC_URL, str(filename))
|
||||||
|
|
||||||
|
|
||||||
def construct_absolute_url(*arg, **kwargs):
|
|
||||||
"""Construct (or attempt to construct) an absolute URL from a relative URL.
|
|
||||||
|
|
||||||
This is useful when (for example) sending an email to a user with a link
|
|
||||||
to something in the InvenTree web framework.
|
|
||||||
|
|
||||||
A URL is constructed in the following order:
|
|
||||||
|
|
||||||
1. If setings.SITE_URL is set (e.g. in the Django settings), use that
|
|
||||||
2. If the InvenTree setting INVENTREE_BASE_URL is set, use that
|
|
||||||
3. Otherwise, use the current request URL (if available)
|
|
||||||
"""
|
|
||||||
|
|
||||||
relative_url = '/'.join(arg)
|
|
||||||
|
|
||||||
# If a site URL is provided, use that
|
|
||||||
site_url = getattr(settings, 'SITE_URL', None)
|
|
||||||
|
|
||||||
if not site_url:
|
|
||||||
# Otherwise, try to use the InvenTree setting
|
|
||||||
try:
|
|
||||||
site_url = common.models.InvenTreeSetting.get_setting('INVENTREE_BASE_URL', create=False, cache=False)
|
|
||||||
except ProgrammingError:
|
|
||||||
pass
|
|
||||||
except OperationalError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if not site_url:
|
|
||||||
# Otherwise, try to use the current request
|
|
||||||
request = kwargs.get('request', None)
|
|
||||||
|
|
||||||
if request:
|
|
||||||
site_url = request.build_absolute_uri('/')
|
|
||||||
|
|
||||||
if not site_url:
|
|
||||||
# No site URL available, return the relative URL
|
|
||||||
return relative_url
|
|
||||||
|
|
||||||
# Strip trailing slash from base url
|
|
||||||
if site_url.endswith('/'):
|
|
||||||
site_url = site_url[:-1]
|
|
||||||
|
|
||||||
if relative_url.startswith('/'):
|
|
||||||
relative_url = relative_url[1:]
|
|
||||||
|
|
||||||
return f"{site_url}/{relative_url}"
|
|
||||||
|
|
||||||
|
|
||||||
def get_base_url(**kwargs):
|
|
||||||
"""Return the base URL for the InvenTree server"""
|
|
||||||
return construct_absolute_url('', **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
def download_image_from_url(remote_url, timeout=2.5):
|
|
||||||
"""Download an image file from a remote URL.
|
|
||||||
|
|
||||||
This is a potentially dangerous operation, so we must perform some checks:
|
|
||||||
|
|
||||||
- The remote URL is available
|
|
||||||
- The Content-Length is provided, and is not too large
|
|
||||||
- The file is a valid image file
|
|
||||||
|
|
||||||
Arguments:
|
|
||||||
remote_url: The remote URL to retrieve image
|
|
||||||
max_size: Maximum allowed image size (default = 1MB)
|
|
||||||
timeout: Connection timeout in seconds (default = 5)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
An in-memory PIL image file, if the download was successful
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
requests.exceptions.ConnectionError: Connection could not be established
|
|
||||||
requests.exceptions.Timeout: Connection timed out
|
|
||||||
requests.exceptions.HTTPError: Server responded with invalid response code
|
|
||||||
ValueError: Server responded with invalid 'Content-Length' value
|
|
||||||
TypeError: Response is not a valid image
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Check that the provided URL at least looks valid
|
|
||||||
validator = URLValidator()
|
|
||||||
validator(remote_url)
|
|
||||||
|
|
||||||
# Calculate maximum allowable image size (in bytes)
|
|
||||||
max_size = int(common.models.InvenTreeSetting.get_setting('INVENTREE_DOWNLOAD_IMAGE_MAX_SIZE')) * 1024 * 1024
|
|
||||||
|
|
||||||
# Add user specified user-agent to request (if specified)
|
|
||||||
user_agent = common.models.InvenTreeSetting.get_setting('INVENTREE_DOWNLOAD_FROM_URL_USER_AGENT')
|
|
||||||
if user_agent:
|
|
||||||
headers = {"User-Agent": user_agent}
|
|
||||||
else:
|
|
||||||
headers = None
|
|
||||||
|
|
||||||
try:
|
|
||||||
response = requests.get(
|
|
||||||
remote_url,
|
|
||||||
timeout=timeout,
|
|
||||||
allow_redirects=True,
|
|
||||||
stream=True,
|
|
||||||
headers=headers,
|
|
||||||
)
|
|
||||||
# Throw an error if anything goes wrong
|
|
||||||
response.raise_for_status()
|
|
||||||
except requests.exceptions.ConnectionError as exc:
|
|
||||||
raise Exception(_("Connection error") + f": {str(exc)}")
|
|
||||||
except requests.exceptions.Timeout as exc:
|
|
||||||
raise exc
|
|
||||||
except requests.exceptions.HTTPError:
|
|
||||||
raise requests.exceptions.HTTPError(_("Server responded with invalid status code") + f": {response.status_code}")
|
|
||||||
except Exception as exc:
|
|
||||||
raise Exception(_("Exception occurred") + f": {str(exc)}")
|
|
||||||
|
|
||||||
if response.status_code != 200:
|
|
||||||
raise Exception(_("Server responded with invalid status code") + f": {response.status_code}")
|
|
||||||
|
|
||||||
try:
|
|
||||||
content_length = int(response.headers.get('Content-Length', 0))
|
|
||||||
except ValueError:
|
|
||||||
raise ValueError(_("Server responded with invalid Content-Length value"))
|
|
||||||
|
|
||||||
if content_length > max_size:
|
|
||||||
raise ValueError(_("Image size is too large"))
|
|
||||||
|
|
||||||
# Download the file, ensuring we do not exceed the reported size
|
|
||||||
fo = io.BytesIO()
|
|
||||||
|
|
||||||
dl_size = 0
|
|
||||||
chunk_size = 64 * 1024
|
|
||||||
|
|
||||||
for chunk in response.iter_content(chunk_size=chunk_size):
|
|
||||||
dl_size += len(chunk)
|
|
||||||
|
|
||||||
if dl_size > max_size:
|
|
||||||
raise ValueError(_("Image download exceeded maximum size"))
|
|
||||||
|
|
||||||
fo.write(chunk)
|
|
||||||
|
|
||||||
if dl_size == 0:
|
|
||||||
raise ValueError(_("Remote server returned empty response"))
|
|
||||||
|
|
||||||
# Now, attempt to convert the downloaded data to a valid image file
|
|
||||||
# img.verify() will throw an exception if the image is not valid
|
|
||||||
try:
|
|
||||||
img = Image.open(fo).convert()
|
|
||||||
img.verify()
|
|
||||||
except Exception:
|
|
||||||
raise TypeError(_("Supplied URL is not a valid image file"))
|
|
||||||
|
|
||||||
return img
|
|
||||||
|
|
||||||
|
|
||||||
def TestIfImage(img):
|
def TestIfImage(img):
|
||||||
"""Test if an image file is indeed an image."""
|
"""Test if an image file is indeed an image."""
|
||||||
try:
|
try:
|
||||||
@ -1016,120 +853,3 @@ def inheritors(cls):
|
|||||||
subcls.add(child)
|
subcls.add(child)
|
||||||
work.append(child)
|
work.append(child)
|
||||||
return subcls
|
return subcls
|
||||||
|
|
||||||
|
|
||||||
def notify_responsible(instance, sender, content: NotificationBody = InvenTreeNotificationBodies.NewOrder, exclude=None):
|
|
||||||
"""Notify all responsible parties of a change in an instance.
|
|
||||||
|
|
||||||
Parses the supplied content with the provided instance and sender and sends a notification to all responsible users,
|
|
||||||
excluding the optional excluded list.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
instance: The newly created instance
|
|
||||||
sender: Sender model reference
|
|
||||||
content (NotificationBody, optional): _description_. Defaults to InvenTreeNotificationBodies.NewOrder.
|
|
||||||
exclude (User, optional): User instance that should be excluded. Defaults to None.
|
|
||||||
"""
|
|
||||||
if instance.responsible is not None:
|
|
||||||
# Setup context for notification parsing
|
|
||||||
content_context = {
|
|
||||||
'instance': str(instance),
|
|
||||||
'verbose_name': sender._meta.verbose_name,
|
|
||||||
'app_label': sender._meta.app_label,
|
|
||||||
'model_name': sender._meta.model_name,
|
|
||||||
}
|
|
||||||
|
|
||||||
# Setup notification context
|
|
||||||
context = {
|
|
||||||
'instance': instance,
|
|
||||||
'name': content.name.format(**content_context),
|
|
||||||
'message': content.message.format(**content_context),
|
|
||||||
'link': InvenTree.helpers.construct_absolute_url(instance.get_absolute_url()),
|
|
||||||
'template': {
|
|
||||||
'html': content.template.format(**content_context),
|
|
||||||
'subject': content.name.format(**content_context),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Create notification
|
|
||||||
trigger_notification(
|
|
||||||
instance,
|
|
||||||
content.slug.format(**content_context),
|
|
||||||
targets=[instance.responsible],
|
|
||||||
target_exclude=[exclude],
|
|
||||||
context=context,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def render_currency(money, decimal_places=None, currency=None, include_symbol=True, min_decimal_places=None, max_decimal_places=None):
|
|
||||||
"""Render a currency / Money object to a formatted string (e.g. for reports)
|
|
||||||
|
|
||||||
Arguments:
|
|
||||||
money: The Money instance to be rendered
|
|
||||||
decimal_places: The number of decimal places to render to. If unspecified, uses the PRICING_DECIMAL_PLACES setting.
|
|
||||||
currency: Optionally convert to the specified currency
|
|
||||||
include_symbol: Render with the appropriate currency symbol
|
|
||||||
min_decimal_places: The minimum number of decimal places to render to. If unspecified, uses the PRICING_DECIMAL_PLACES_MIN setting.
|
|
||||||
max_decimal_places: The maximum number of decimal places to render to. If unspecified, uses the PRICING_DECIMAL_PLACES setting.
|
|
||||||
"""
|
|
||||||
|
|
||||||
if money in [None, '']:
|
|
||||||
return '-'
|
|
||||||
|
|
||||||
if type(money) is not Money:
|
|
||||||
return '-'
|
|
||||||
|
|
||||||
if currency is not None:
|
|
||||||
# Attempt to convert to the provided currency
|
|
||||||
# If cannot be done, leave the original
|
|
||||||
try:
|
|
||||||
money = convert_money(money, currency)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if decimal_places is None:
|
|
||||||
decimal_places = common.models.InvenTreeSetting.get_setting('PRICING_DECIMAL_PLACES', 6)
|
|
||||||
|
|
||||||
if min_decimal_places is None:
|
|
||||||
min_decimal_places = common.models.InvenTreeSetting.get_setting('PRICING_DECIMAL_PLACES_MIN', 0)
|
|
||||||
|
|
||||||
if max_decimal_places is None:
|
|
||||||
max_decimal_places = common.models.InvenTreeSetting.get_setting('PRICING_DECIMAL_PLACES', 6)
|
|
||||||
|
|
||||||
value = Decimal(str(money.amount)).normalize()
|
|
||||||
value = str(value)
|
|
||||||
|
|
||||||
if '.' in value:
|
|
||||||
decimals = len(value.split('.')[-1])
|
|
||||||
|
|
||||||
decimals = max(decimals, min_decimal_places)
|
|
||||||
decimals = min(decimals, decimal_places)
|
|
||||||
|
|
||||||
decimal_places = decimals
|
|
||||||
else:
|
|
||||||
decimal_places = max(decimal_places, 2)
|
|
||||||
|
|
||||||
decimal_places = max(decimal_places, max_decimal_places)
|
|
||||||
|
|
||||||
return moneyed.localization.format_money(
|
|
||||||
money,
|
|
||||||
decimal_places=decimal_places,
|
|
||||||
include_symbol=include_symbol,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def getModelsWithMixin(mixin_class) -> list:
|
|
||||||
"""Return a list of models that inherit from the given mixin class.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
mixin_class: The mixin class to search for
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
List of models that inherit from the given mixin class
|
|
||||||
"""
|
|
||||||
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
|
||||||
|
|
||||||
db_models = [x.model_class() for x in ContentType.objects.all() if x is not None]
|
|
||||||
|
|
||||||
return [x for x in db_models if x is not None and issubclass(x, mixin_class)]
|
|
||||||
|
293
InvenTree/InvenTree/helpers_model.py
Normal file
293
InvenTree/InvenTree/helpers_model.py
Normal file
@ -0,0 +1,293 @@
|
|||||||
|
"""Provides helper functions used throughout the InvenTree project that access the database."""
|
||||||
|
|
||||||
|
import io
|
||||||
|
import logging
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.validators import URLValidator
|
||||||
|
from django.db.utils import OperationalError, ProgrammingError
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
import moneyed.localization
|
||||||
|
import requests
|
||||||
|
from djmoney.contrib.exchange.models import convert_money
|
||||||
|
from djmoney.money import Money
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
import common.models
|
||||||
|
import InvenTree
|
||||||
|
import InvenTree.helpers_model
|
||||||
|
import InvenTree.version
|
||||||
|
from common.notifications import (InvenTreeNotificationBodies,
|
||||||
|
NotificationBody, trigger_notification)
|
||||||
|
|
||||||
|
logger = logging.getLogger('inventree')
|
||||||
|
|
||||||
|
|
||||||
|
def getSetting(key, backup_value=None):
|
||||||
|
"""Shortcut for reading a setting value from the database."""
|
||||||
|
return common.models.InvenTreeSetting.get_setting(key, backup_value=backup_value)
|
||||||
|
|
||||||
|
|
||||||
|
def construct_absolute_url(*arg, **kwargs):
|
||||||
|
"""Construct (or attempt to construct) an absolute URL from a relative URL.
|
||||||
|
|
||||||
|
This is useful when (for example) sending an email to a user with a link
|
||||||
|
to something in the InvenTree web framework.
|
||||||
|
A URL is constructed in the following order:
|
||||||
|
1. If setings.SITE_URL is set (e.g. in the Django settings), use that
|
||||||
|
2. If the InvenTree setting INVENTREE_BASE_URL is set, use that
|
||||||
|
3. Otherwise, use the current request URL (if available)
|
||||||
|
"""
|
||||||
|
|
||||||
|
relative_url = '/'.join(arg)
|
||||||
|
|
||||||
|
# If a site URL is provided, use that
|
||||||
|
site_url = getattr(settings, 'SITE_URL', None)
|
||||||
|
|
||||||
|
if not site_url:
|
||||||
|
# Otherwise, try to use the InvenTree setting
|
||||||
|
try:
|
||||||
|
site_url = common.models.InvenTreeSetting.get_setting('INVENTREE_BASE_URL', create=False, cache=False)
|
||||||
|
except ProgrammingError:
|
||||||
|
pass
|
||||||
|
except OperationalError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if not site_url:
|
||||||
|
# Otherwise, try to use the current request
|
||||||
|
request = kwargs.get('request', None)
|
||||||
|
|
||||||
|
if request:
|
||||||
|
site_url = request.build_absolute_uri('/')
|
||||||
|
|
||||||
|
if not site_url:
|
||||||
|
# No site URL available, return the relative URL
|
||||||
|
return relative_url
|
||||||
|
|
||||||
|
# Strip trailing slash from base url
|
||||||
|
if site_url.endswith('/'):
|
||||||
|
site_url = site_url[:-1]
|
||||||
|
|
||||||
|
if relative_url.startswith('/'):
|
||||||
|
relative_url = relative_url[1:]
|
||||||
|
|
||||||
|
return f"{site_url}/{relative_url}"
|
||||||
|
|
||||||
|
|
||||||
|
def get_base_url(**kwargs):
|
||||||
|
"""Return the base URL for the InvenTree server"""
|
||||||
|
return construct_absolute_url('', **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def download_image_from_url(remote_url, timeout=2.5):
|
||||||
|
"""Download an image file from a remote URL.
|
||||||
|
|
||||||
|
This is a potentially dangerous operation, so we must perform some checks:
|
||||||
|
- The remote URL is available
|
||||||
|
- The Content-Length is provided, and is not too large
|
||||||
|
- The file is a valid image file
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
remote_url: The remote URL to retrieve image
|
||||||
|
max_size: Maximum allowed image size (default = 1MB)
|
||||||
|
timeout: Connection timeout in seconds (default = 5)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
An in-memory PIL image file, if the download was successful
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
requests.exceptions.ConnectionError: Connection could not be established
|
||||||
|
requests.exceptions.Timeout: Connection timed out
|
||||||
|
requests.exceptions.HTTPError: Server responded with invalid response code
|
||||||
|
ValueError: Server responded with invalid 'Content-Length' value
|
||||||
|
TypeError: Response is not a valid image
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Check that the provided URL at least looks valid
|
||||||
|
validator = URLValidator()
|
||||||
|
validator(remote_url)
|
||||||
|
|
||||||
|
# Calculate maximum allowable image size (in bytes)
|
||||||
|
max_size = int(common.models.InvenTreeSetting.get_setting('INVENTREE_DOWNLOAD_IMAGE_MAX_SIZE')) * 1024 * 1024
|
||||||
|
|
||||||
|
# Add user specified user-agent to request (if specified)
|
||||||
|
user_agent = common.models.InvenTreeSetting.get_setting('INVENTREE_DOWNLOAD_FROM_URL_USER_AGENT')
|
||||||
|
if user_agent:
|
||||||
|
headers = {"User-Agent": user_agent}
|
||||||
|
else:
|
||||||
|
headers = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.get(
|
||||||
|
remote_url,
|
||||||
|
timeout=timeout,
|
||||||
|
allow_redirects=True,
|
||||||
|
stream=True,
|
||||||
|
headers=headers,
|
||||||
|
)
|
||||||
|
# Throw an error if anything goes wrong
|
||||||
|
response.raise_for_status()
|
||||||
|
except requests.exceptions.ConnectionError as exc:
|
||||||
|
raise Exception(_("Connection error") + f": {str(exc)}")
|
||||||
|
except requests.exceptions.Timeout as exc:
|
||||||
|
raise exc
|
||||||
|
except requests.exceptions.HTTPError:
|
||||||
|
raise requests.exceptions.HTTPError(_("Server responded with invalid status code") + f": {response.status_code}")
|
||||||
|
except Exception as exc:
|
||||||
|
raise Exception(_("Exception occurred") + f": {str(exc)}")
|
||||||
|
|
||||||
|
if response.status_code != 200:
|
||||||
|
raise Exception(_("Server responded with invalid status code") + f": {response.status_code}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
content_length = int(response.headers.get('Content-Length', 0))
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError(_("Server responded with invalid Content-Length value"))
|
||||||
|
|
||||||
|
if content_length > max_size:
|
||||||
|
raise ValueError(_("Image size is too large"))
|
||||||
|
|
||||||
|
# Download the file, ensuring we do not exceed the reported size
|
||||||
|
fo = io.BytesIO()
|
||||||
|
|
||||||
|
dl_size = 0
|
||||||
|
chunk_size = 64 * 1024
|
||||||
|
|
||||||
|
for chunk in response.iter_content(chunk_size=chunk_size):
|
||||||
|
dl_size += len(chunk)
|
||||||
|
|
||||||
|
if dl_size > max_size:
|
||||||
|
raise ValueError(_("Image download exceeded maximum size"))
|
||||||
|
|
||||||
|
fo.write(chunk)
|
||||||
|
|
||||||
|
if dl_size == 0:
|
||||||
|
raise ValueError(_("Remote server returned empty response"))
|
||||||
|
|
||||||
|
# Now, attempt to convert the downloaded data to a valid image file
|
||||||
|
# img.verify() will throw an exception if the image is not valid
|
||||||
|
try:
|
||||||
|
img = Image.open(fo).convert()
|
||||||
|
img.verify()
|
||||||
|
except Exception:
|
||||||
|
raise TypeError(_("Supplied URL is not a valid image file"))
|
||||||
|
|
||||||
|
return img
|
||||||
|
|
||||||
|
|
||||||
|
def render_currency(money, decimal_places=None, currency=None, include_symbol=True, min_decimal_places=None, max_decimal_places=None):
|
||||||
|
"""Render a currency / Money object to a formatted string (e.g. for reports)
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
money: The Money instance to be rendered
|
||||||
|
decimal_places: The number of decimal places to render to. If unspecified, uses the PRICING_DECIMAL_PLACES setting.
|
||||||
|
currency: Optionally convert to the specified currency
|
||||||
|
include_symbol: Render with the appropriate currency symbol
|
||||||
|
min_decimal_places: The minimum number of decimal places to render to. If unspecified, uses the PRICING_DECIMAL_PLACES_MIN setting.
|
||||||
|
max_decimal_places: The maximum number of decimal places to render to. If unspecified, uses the PRICING_DECIMAL_PLACES setting.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if money in [None, '']:
|
||||||
|
return '-'
|
||||||
|
|
||||||
|
if type(money) is not Money:
|
||||||
|
return '-'
|
||||||
|
|
||||||
|
if currency is not None:
|
||||||
|
# Attempt to convert to the provided currency
|
||||||
|
# If cannot be done, leave the original
|
||||||
|
try:
|
||||||
|
money = convert_money(money, currency)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if decimal_places is None:
|
||||||
|
decimal_places = common.models.InvenTreeSetting.get_setting('PRICING_DECIMAL_PLACES', 6)
|
||||||
|
|
||||||
|
if min_decimal_places is None:
|
||||||
|
min_decimal_places = common.models.InvenTreeSetting.get_setting('PRICING_DECIMAL_PLACES_MIN', 0)
|
||||||
|
|
||||||
|
if max_decimal_places is None:
|
||||||
|
max_decimal_places = common.models.InvenTreeSetting.get_setting('PRICING_DECIMAL_PLACES', 6)
|
||||||
|
|
||||||
|
value = Decimal(str(money.amount)).normalize()
|
||||||
|
value = str(value)
|
||||||
|
|
||||||
|
if '.' in value:
|
||||||
|
decimals = len(value.split('.')[-1])
|
||||||
|
|
||||||
|
decimals = max(decimals, min_decimal_places)
|
||||||
|
decimals = min(decimals, decimal_places)
|
||||||
|
|
||||||
|
decimal_places = decimals
|
||||||
|
else:
|
||||||
|
decimal_places = max(decimal_places, 2)
|
||||||
|
|
||||||
|
decimal_places = max(decimal_places, max_decimal_places)
|
||||||
|
|
||||||
|
return moneyed.localization.format_money(
|
||||||
|
money,
|
||||||
|
decimal_places=decimal_places,
|
||||||
|
include_symbol=include_symbol,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def getModelsWithMixin(mixin_class) -> list:
|
||||||
|
"""Return a list of models that inherit from the given mixin class.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
mixin_class: The mixin class to search for
|
||||||
|
Returns:
|
||||||
|
List of models that inherit from the given mixin class
|
||||||
|
"""
|
||||||
|
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
|
||||||
|
db_models = [x.model_class() for x in ContentType.objects.all() if x is not None]
|
||||||
|
|
||||||
|
return [x for x in db_models if x is not None and issubclass(x, mixin_class)]
|
||||||
|
|
||||||
|
|
||||||
|
def notify_responsible(instance, sender, content: NotificationBody = InvenTreeNotificationBodies.NewOrder, exclude=None):
|
||||||
|
"""Notify all responsible parties of a change in an instance.
|
||||||
|
|
||||||
|
Parses the supplied content with the provided instance and sender and sends a notification to all responsible users,
|
||||||
|
excluding the optional excluded list.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
instance: The newly created instance
|
||||||
|
sender: Sender model reference
|
||||||
|
content (NotificationBody, optional): _description_. Defaults to InvenTreeNotificationBodies.NewOrder.
|
||||||
|
exclude (User, optional): User instance that should be excluded. Defaults to None.
|
||||||
|
"""
|
||||||
|
if instance.responsible is not None:
|
||||||
|
# Setup context for notification parsing
|
||||||
|
content_context = {
|
||||||
|
'instance': str(instance),
|
||||||
|
'verbose_name': sender._meta.verbose_name,
|
||||||
|
'app_label': sender._meta.app_label,
|
||||||
|
'model_name': sender._meta.model_name,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Setup notification context
|
||||||
|
context = {
|
||||||
|
'instance': instance,
|
||||||
|
'name': content.name.format(**content_context),
|
||||||
|
'message': content.message.format(**content_context),
|
||||||
|
'link': InvenTree.helpers_model.construct_absolute_url(instance.get_absolute_url()),
|
||||||
|
'template': {
|
||||||
|
'html': content.template.format(**content_context),
|
||||||
|
'subject': content.name.format(**content_context),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create notification
|
||||||
|
trigger_notification(
|
||||||
|
instance,
|
||||||
|
content.slug.format(**content_context),
|
||||||
|
targets=[instance.responsible],
|
||||||
|
target_exclude=[exclude],
|
||||||
|
context=context,
|
||||||
|
)
|
@ -21,10 +21,10 @@ from error_report.models import Error
|
|||||||
from mptt.exceptions import InvalidMove
|
from mptt.exceptions import InvalidMove
|
||||||
from mptt.models import MPTTModel, TreeForeignKey
|
from mptt.models import MPTTModel, TreeForeignKey
|
||||||
|
|
||||||
import common.models
|
|
||||||
import InvenTree.fields
|
import InvenTree.fields
|
||||||
import InvenTree.format
|
import InvenTree.format
|
||||||
import InvenTree.helpers
|
import InvenTree.helpers
|
||||||
|
import InvenTree.helpers_model
|
||||||
from InvenTree.sanitizer import sanitize_svg
|
from InvenTree.sanitizer import sanitize_svg
|
||||||
|
|
||||||
logger = logging.getLogger('inventree')
|
logger = logging.getLogger('inventree')
|
||||||
@ -207,7 +207,9 @@ class ReferenceIndexingMixin(models.Model):
|
|||||||
if cls.REFERENCE_PATTERN_SETTING is None:
|
if cls.REFERENCE_PATTERN_SETTING is None:
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
return common.models.InvenTreeSetting.get_setting(cls.REFERENCE_PATTERN_SETTING, create=False).strip()
|
# import at function level to prevent cyclic imports
|
||||||
|
from common.models import InvenTreeSetting
|
||||||
|
return InvenTreeSetting.get_setting(cls.REFERENCE_PATTERN_SETTING, create=False).strip()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_reference_context(cls):
|
def get_reference_context(cls):
|
||||||
@ -889,7 +891,7 @@ def after_error_logged(sender, instance: Error, created: bool, **kwargs):
|
|||||||
|
|
||||||
users = get_user_model().objects.filter(is_staff=True)
|
users = get_user_model().objects.filter(is_staff=True)
|
||||||
|
|
||||||
link = InvenTree.helpers.construct_absolute_url(
|
link = InvenTree.helpers_model.construct_absolute_url(
|
||||||
reverse('admin:error_report_error_change', kwargs={'object_id': instance.pk})
|
reverse('admin:error_report_error_change', kwargs={'object_id': instance.pk})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@ from taggit.serializers import TaggitSerializer
|
|||||||
from common.models import InvenTreeSetting
|
from common.models import InvenTreeSetting
|
||||||
from common.settings import currency_code_default, currency_code_mappings
|
from common.settings import currency_code_default, currency_code_mappings
|
||||||
from InvenTree.fields import InvenTreeRestURLField, InvenTreeURLField
|
from InvenTree.fields import InvenTreeRestURLField, InvenTreeURLField
|
||||||
from InvenTree.helpers import download_image_from_url
|
from InvenTree.helpers_model import download_image_from_url
|
||||||
|
|
||||||
|
|
||||||
class InvenTreeMoneySerializer(MoneyField):
|
class InvenTreeMoneySerializer(MoneyField):
|
||||||
|
@ -21,6 +21,7 @@ from djmoney.money import Money
|
|||||||
import InvenTree.conversion
|
import InvenTree.conversion
|
||||||
import InvenTree.format
|
import InvenTree.format
|
||||||
import InvenTree.helpers
|
import InvenTree.helpers
|
||||||
|
import InvenTree.helpers_model
|
||||||
import InvenTree.tasks
|
import InvenTree.tasks
|
||||||
from common.models import InvenTreeSetting
|
from common.models import InvenTreeSetting
|
||||||
from common.settings import currency_codes
|
from common.settings import currency_codes
|
||||||
@ -282,7 +283,7 @@ class TestHelpers(TestCase):
|
|||||||
"\\invalid-url"
|
"\\invalid-url"
|
||||||
]:
|
]:
|
||||||
with self.assertRaises(django_exceptions.ValidationError):
|
with self.assertRaises(django_exceptions.ValidationError):
|
||||||
helpers.download_image_from_url(url)
|
InvenTree.helpers_model.download_image_from_url(url)
|
||||||
|
|
||||||
def dl_helper(url, expected_error, timeout=2.5, retries=3):
|
def dl_helper(url, expected_error, timeout=2.5, retries=3):
|
||||||
"""Helper function for unit testing downloads.
|
"""Helper function for unit testing downloads.
|
||||||
@ -297,7 +298,7 @@ class TestHelpers(TestCase):
|
|||||||
while tries < retries:
|
while tries < retries:
|
||||||
|
|
||||||
try:
|
try:
|
||||||
helpers.download_image_from_url(url, timeout=timeout)
|
InvenTree.helpers_model.download_image_from_url(url, timeout=timeout)
|
||||||
break
|
break
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
if type(exc) is expected_error:
|
if type(exc) is expected_error:
|
||||||
@ -323,20 +324,20 @@ class TestHelpers(TestCase):
|
|||||||
|
|
||||||
# Attempt to download an image which is too large
|
# Attempt to download an image which is too large
|
||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(ValueError):
|
||||||
helpers.download_image_from_url(large_img, timeout=10)
|
InvenTree.helpers_model.download_image_from_url(large_img, timeout=10)
|
||||||
|
|
||||||
# Increase allowable download size
|
# Increase allowable download size
|
||||||
InvenTreeSetting.set_setting('INVENTREE_DOWNLOAD_IMAGE_MAX_SIZE', 5, change_user=None)
|
InvenTreeSetting.set_setting('INVENTREE_DOWNLOAD_IMAGE_MAX_SIZE', 5, change_user=None)
|
||||||
|
|
||||||
# Download a valid image (should not throw an error)
|
# Download a valid image (should not throw an error)
|
||||||
helpers.download_image_from_url(large_img, timeout=10)
|
InvenTree.helpers_model.download_image_from_url(large_img, timeout=10)
|
||||||
|
|
||||||
def test_model_mixin(self):
|
def test_model_mixin(self):
|
||||||
"""Test the getModelsWithMixin function"""
|
"""Test the getModelsWithMixin function"""
|
||||||
|
|
||||||
from InvenTree.models import InvenTreeBarcodeMixin
|
from InvenTree.models import InvenTreeBarcodeMixin
|
||||||
|
|
||||||
models = helpers.getModelsWithMixin(InvenTreeBarcodeMixin)
|
models = InvenTree.helpers_model.getModelsWithMixin(InvenTreeBarcodeMixin)
|
||||||
|
|
||||||
self.assertIn(Part, models)
|
self.assertIn(Part, models)
|
||||||
self.assertIn(StockLocation, models)
|
self.assertIn(StockLocation, models)
|
||||||
|
@ -27,6 +27,7 @@ from build.validators import generate_next_build_reference, validate_build_order
|
|||||||
|
|
||||||
import InvenTree.fields
|
import InvenTree.fields
|
||||||
import InvenTree.helpers
|
import InvenTree.helpers
|
||||||
|
import InvenTree.helpers_model
|
||||||
import InvenTree.models
|
import InvenTree.models
|
||||||
import InvenTree.ready
|
import InvenTree.ready
|
||||||
import InvenTree.tasks
|
import InvenTree.tasks
|
||||||
@ -539,7 +540,7 @@ class Build(MPTTModel, InvenTree.models.InvenTreeBarcodeMixin, InvenTree.models.
|
|||||||
'name': name,
|
'name': name,
|
||||||
'slug': 'build.completed',
|
'slug': 'build.completed',
|
||||||
'message': _('A build order has been completed'),
|
'message': _('A build order has been completed'),
|
||||||
'link': InvenTree.helpers.construct_absolute_url(self.get_absolute_url()),
|
'link': InvenTree.helpers_model.construct_absolute_url(self.get_absolute_url()),
|
||||||
'template': {
|
'template': {
|
||||||
'html': 'email/build_order_completed.html',
|
'html': 'email/build_order_completed.html',
|
||||||
'subject': name,
|
'subject': name,
|
||||||
@ -1210,7 +1211,7 @@ def after_save_build(sender, instance: Build, created: bool, **kwargs):
|
|||||||
InvenTree.tasks.offload_task(build_tasks.check_build_stock, instance)
|
InvenTree.tasks.offload_task(build_tasks.check_build_stock, instance)
|
||||||
|
|
||||||
# Notify the responsible users that the build order has been created
|
# Notify the responsible users that the build order has been created
|
||||||
InvenTree.helpers.notify_responsible(instance, sender, exclude=instance.issued_by)
|
InvenTree.helpers_model.notify_responsible(instance, sender, exclude=instance.issued_by)
|
||||||
|
|
||||||
|
|
||||||
class BuildOrderAttachment(InvenTree.models.InvenTreeAttachment):
|
class BuildOrderAttachment(InvenTree.models.InvenTreeAttachment):
|
||||||
|
@ -13,7 +13,7 @@ from plugin.events import trigger_event
|
|||||||
import common.notifications
|
import common.notifications
|
||||||
import build.models
|
import build.models
|
||||||
import InvenTree.email
|
import InvenTree.email
|
||||||
import InvenTree.helpers
|
import InvenTree.helpers_model
|
||||||
import InvenTree.tasks
|
import InvenTree.tasks
|
||||||
from InvenTree.status_codes import BuildStatus
|
from InvenTree.status_codes import BuildStatus
|
||||||
from InvenTree.ready import isImportingData
|
from InvenTree.ready import isImportingData
|
||||||
@ -65,7 +65,7 @@ def check_build_stock(build: build.models.Build):
|
|||||||
# There is not sufficient stock for this part
|
# There is not sufficient stock for this part
|
||||||
|
|
||||||
lines.append({
|
lines.append({
|
||||||
'link': InvenTree.helpers.construct_absolute_url(sub_part.get_absolute_url()),
|
'link': InvenTree.helpers_model.construct_absolute_url(sub_part.get_absolute_url()),
|
||||||
'part': sub_part,
|
'part': sub_part,
|
||||||
'in_stock': in_stock,
|
'in_stock': in_stock,
|
||||||
'allocated': allocated,
|
'allocated': allocated,
|
||||||
@ -89,7 +89,7 @@ def check_build_stock(build: build.models.Build):
|
|||||||
logger.info(f"Notifying users of stock required for build {build.pk}")
|
logger.info(f"Notifying users of stock required for build {build.pk}")
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
'link': InvenTree.helpers.construct_absolute_url(build.get_absolute_url()),
|
'link': InvenTree.helpers_model.construct_absolute_url(build.get_absolute_url()),
|
||||||
'build': build,
|
'build': build,
|
||||||
'part': build.part,
|
'part': build.part,
|
||||||
'lines': lines,
|
'lines': lines,
|
||||||
@ -122,7 +122,7 @@ def notify_overdue_build_order(bo: build.models.Build):
|
|||||||
'order': bo,
|
'order': bo,
|
||||||
'name': name,
|
'name': name,
|
||||||
'message': _(f"Build order {bo} is now overdue"),
|
'message': _(f"Build order {bo} is now overdue"),
|
||||||
'link': InvenTree.helpers.construct_absolute_url(
|
'link': InvenTree.helpers_model.construct_absolute_url(
|
||||||
bo.get_absolute_url(),
|
bo.get_absolute_url(),
|
||||||
),
|
),
|
||||||
'template': {
|
'template': {
|
||||||
|
@ -7,7 +7,8 @@ from rest_framework import serializers
|
|||||||
from common.models import (InvenTreeSetting, InvenTreeUserSetting,
|
from common.models import (InvenTreeSetting, InvenTreeUserSetting,
|
||||||
NewsFeedEntry, NotesImage, NotificationMessage,
|
NewsFeedEntry, NotesImage, NotificationMessage,
|
||||||
ProjectCode)
|
ProjectCode)
|
||||||
from InvenTree.helpers import construct_absolute_url, get_objectreference
|
from InvenTree.helpers import get_objectreference
|
||||||
|
from InvenTree.helpers_model import construct_absolute_url
|
||||||
from InvenTree.serializers import (InvenTreeImageSerializerField,
|
from InvenTree.serializers import (InvenTreeImageSerializerField,
|
||||||
InvenTreeModelSerializer)
|
InvenTreeModelSerializer)
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ from django.utils import timezone
|
|||||||
|
|
||||||
import feedparser
|
import feedparser
|
||||||
|
|
||||||
from InvenTree.helpers import getModelsWithMixin
|
from InvenTree.helpers_model import getModelsWithMixin
|
||||||
from InvenTree.models import InvenTreeNotesMixin
|
from InvenTree.models import InvenTreeNotesMixin
|
||||||
from InvenTree.tasks import ScheduledTask, scheduled_task
|
from InvenTree.tasks import ScheduledTask, scheduled_task
|
||||||
|
|
||||||
|
@ -15,7 +15,8 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
|
|
||||||
import part.models
|
import part.models
|
||||||
import stock.models
|
import stock.models
|
||||||
from InvenTree.helpers import get_base_url, normalize, validateFilterString
|
from InvenTree.helpers import normalize, validateFilterString
|
||||||
|
from InvenTree.helpers_model import get_base_url
|
||||||
from InvenTree.models import MetadataMixin
|
from InvenTree.models import MetadataMixin
|
||||||
from plugin.registry import registry
|
from plugin.registry import registry
|
||||||
|
|
||||||
|
@ -19,8 +19,8 @@ from company.models import SupplierPart
|
|||||||
from InvenTree.api import (APIDownloadMixin, AttachmentMixin,
|
from InvenTree.api import (APIDownloadMixin, AttachmentMixin,
|
||||||
ListCreateDestroyAPIView, MetadataView, StatusView)
|
ListCreateDestroyAPIView, MetadataView, StatusView)
|
||||||
from InvenTree.filters import SEARCH_ORDER_FILTER, SEARCH_ORDER_FILTER_ALIAS
|
from InvenTree.filters import SEARCH_ORDER_FILTER, SEARCH_ORDER_FILTER_ALIAS
|
||||||
from InvenTree.helpers import (DownloadFile, construct_absolute_url,
|
from InvenTree.helpers import DownloadFile, str2bool
|
||||||
get_base_url, str2bool)
|
from InvenTree.helpers_model import construct_absolute_url, get_base_url
|
||||||
from InvenTree.mixins import (CreateAPI, ListAPI, ListCreateAPI,
|
from InvenTree.mixins import (CreateAPI, ListAPI, ListCreateAPI,
|
||||||
RetrieveUpdateDestroyAPI)
|
RetrieveUpdateDestroyAPI)
|
||||||
from InvenTree.status_codes import (PurchaseOrderStatus, ReturnOrderLineStatus,
|
from InvenTree.status_codes import (PurchaseOrderStatus, ReturnOrderLineStatus,
|
||||||
|
@ -36,7 +36,8 @@ from company.models import Company, Contact, SupplierPart
|
|||||||
from InvenTree.exceptions import log_error
|
from InvenTree.exceptions import log_error
|
||||||
from InvenTree.fields import (InvenTreeModelMoneyField, InvenTreeURLField,
|
from InvenTree.fields import (InvenTreeModelMoneyField, InvenTreeURLField,
|
||||||
RoundingDecimalField)
|
RoundingDecimalField)
|
||||||
from InvenTree.helpers import decimal2string, getSetting, notify_responsible
|
from InvenTree.helpers import decimal2string
|
||||||
|
from InvenTree.helpers_model import getSetting, notify_responsible
|
||||||
from InvenTree.models import (InvenTreeAttachment, InvenTreeBarcodeMixin,
|
from InvenTree.models import (InvenTreeAttachment, InvenTreeBarcodeMixin,
|
||||||
InvenTreeNotesMixin, MetadataMixin,
|
InvenTreeNotesMixin, MetadataMixin,
|
||||||
ReferenceIndexingMixin)
|
ReferenceIndexingMixin)
|
||||||
|
@ -5,7 +5,7 @@ from datetime import datetime, timedelta
|
|||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
import common.notifications
|
import common.notifications
|
||||||
import InvenTree.helpers
|
import InvenTree.helpers_model
|
||||||
import order.models
|
import order.models
|
||||||
from InvenTree.status_codes import PurchaseOrderStatus, SalesOrderStatus
|
from InvenTree.status_codes import PurchaseOrderStatus, SalesOrderStatus
|
||||||
from InvenTree.tasks import ScheduledTask, scheduled_task
|
from InvenTree.tasks import ScheduledTask, scheduled_task
|
||||||
@ -29,7 +29,7 @@ def notify_overdue_purchase_order(po: order.models.PurchaseOrder):
|
|||||||
'order': po,
|
'order': po,
|
||||||
'name': name,
|
'name': name,
|
||||||
'message': _(f'Purchase order {po} is now overdue'),
|
'message': _(f'Purchase order {po} is now overdue'),
|
||||||
'link': InvenTree.helpers.construct_absolute_url(
|
'link': InvenTree.helpers_model.construct_absolute_url(
|
||||||
po.get_absolute_url(),
|
po.get_absolute_url(),
|
||||||
),
|
),
|
||||||
'template': {
|
'template': {
|
||||||
@ -92,7 +92,7 @@ def notify_overdue_sales_order(so: order.models.SalesOrder):
|
|||||||
'order': so,
|
'order': so,
|
||||||
'name': name,
|
'name': name,
|
||||||
'message': _(f"Sales order {so} is now overdue"),
|
'message': _(f"Sales order {so} is now overdue"),
|
||||||
'link': InvenTree.helpers.construct_absolute_url(
|
'link': InvenTree.helpers_model.construct_absolute_url(
|
||||||
so.get_absolute_url(),
|
so.get_absolute_url(),
|
||||||
),
|
),
|
||||||
'template': {
|
'template': {
|
||||||
|
@ -21,6 +21,7 @@ import common.notifications
|
|||||||
import common.settings
|
import common.settings
|
||||||
import company.models
|
import company.models
|
||||||
import InvenTree.helpers
|
import InvenTree.helpers
|
||||||
|
import InvenTree.helpers_model
|
||||||
import InvenTree.tasks
|
import InvenTree.tasks
|
||||||
import part.models
|
import part.models
|
||||||
import stock.models
|
import stock.models
|
||||||
@ -41,7 +42,7 @@ def notify_low_stock(part: part.models.Part):
|
|||||||
'part': part,
|
'part': part,
|
||||||
'name': name,
|
'name': name,
|
||||||
'message': message,
|
'message': message,
|
||||||
'link': InvenTree.helpers.construct_absolute_url(part.get_absolute_url()),
|
'link': InvenTree.helpers_model.construct_absolute_url(part.get_absolute_url()),
|
||||||
'template': {
|
'template': {
|
||||||
'html': 'email/low_stock_notification.html',
|
'html': 'email/low_stock_notification.html',
|
||||||
'subject': name,
|
'subject': name,
|
||||||
|
@ -14,6 +14,7 @@ from django.utils.safestring import mark_safe
|
|||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
import InvenTree.helpers
|
import InvenTree.helpers
|
||||||
|
import InvenTree.helpers_model
|
||||||
from common.models import ColorTheme, InvenTreeSetting, InvenTreeUserSetting
|
from common.models import ColorTheme, InvenTreeSetting, InvenTreeUserSetting
|
||||||
from common.settings import currency_code_default
|
from common.settings import currency_code_default
|
||||||
from InvenTree import settings, version
|
from InvenTree import settings, version
|
||||||
@ -105,7 +106,7 @@ def render_date(context, date_object):
|
|||||||
def render_currency(money, **kwargs):
|
def render_currency(money, **kwargs):
|
||||||
"""Render a currency / Money object"""
|
"""Render a currency / Money object"""
|
||||||
|
|
||||||
return InvenTree.helpers.render_currency(money, **kwargs)
|
return InvenTree.helpers_model.render_currency(money, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag()
|
@register.simple_tag()
|
||||||
@ -224,7 +225,7 @@ def inventree_splash(**kwargs):
|
|||||||
@register.simple_tag()
|
@register.simple_tag()
|
||||||
def inventree_base_url(*args, **kwargs):
|
def inventree_base_url(*args, **kwargs):
|
||||||
"""Return the base URL of the InvenTree server"""
|
"""Return the base URL of the InvenTree server"""
|
||||||
return InvenTree.helpers.get_base_url()
|
return InvenTree.helpers_model.get_base_url()
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag()
|
@register.simple_tag()
|
||||||
|
@ -7,6 +7,7 @@ import requests
|
|||||||
|
|
||||||
import part.models
|
import part.models
|
||||||
import stock.models
|
import stock.models
|
||||||
|
from InvenTree.helpers import generateTestKey
|
||||||
from plugin.helpers import (MixinNotImplementedError, render_template,
|
from plugin.helpers import (MixinNotImplementedError, render_template,
|
||||||
render_text)
|
render_text)
|
||||||
|
|
||||||
@ -444,7 +445,6 @@ class PanelMixin:
|
|||||||
Returns:
|
Returns:
|
||||||
Array of panels
|
Array of panels
|
||||||
"""
|
"""
|
||||||
import InvenTree.helpers
|
|
||||||
|
|
||||||
panels = []
|
panels = []
|
||||||
|
|
||||||
@ -482,7 +482,7 @@ class PanelMixin:
|
|||||||
panel['slug'] = self.slug
|
panel['slug'] = self.slug
|
||||||
|
|
||||||
# Add a 'key' for the panel, which is mostly guaranteed to be unique
|
# Add a 'key' for the panel, which is mostly guaranteed to be unique
|
||||||
panel['key'] = InvenTree.helpers.generateTestKey(self.slug + panel.get('title', 'panel'))
|
panel['key'] = generateTestKey(self.slug + panel.get('title', 'panel'))
|
||||||
|
|
||||||
panels.append(panel)
|
panels.append(panel)
|
||||||
|
|
||||||
|
@ -11,7 +11,8 @@ import json
|
|||||||
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from InvenTree.helpers import getModelsWithMixin, hash_barcode
|
from InvenTree.helpers import hash_barcode
|
||||||
|
from InvenTree.helpers_model import getModelsWithMixin
|
||||||
from InvenTree.models import InvenTreeBarcodeMixin
|
from InvenTree.models import InvenTreeBarcodeMixin
|
||||||
from plugin import InvenTreePlugin
|
from plugin import InvenTreePlugin
|
||||||
from plugin.mixins import BarcodeMixin
|
from plugin.mixins import BarcodeMixin
|
||||||
|
@ -20,7 +20,8 @@ import common.models
|
|||||||
import order.models
|
import order.models
|
||||||
import part.models
|
import part.models
|
||||||
import stock.models
|
import stock.models
|
||||||
from InvenTree.helpers import get_base_url, validateFilterString
|
from InvenTree.helpers import validateFilterString
|
||||||
|
from InvenTree.helpers_model import get_base_url
|
||||||
from InvenTree.models import MetadataMixin
|
from InvenTree.models import MetadataMixin
|
||||||
from plugin.registry import registry
|
from plugin.registry import registry
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@ from django.conf import settings
|
|||||||
from django.utils.safestring import SafeString, mark_safe
|
from django.utils.safestring import SafeString, mark_safe
|
||||||
|
|
||||||
import InvenTree.helpers
|
import InvenTree.helpers
|
||||||
|
import InvenTree.helpers_model
|
||||||
from common.models import InvenTreeSetting
|
from common.models import InvenTreeSetting
|
||||||
from company.models import Company
|
from company.models import Company
|
||||||
from part.models import Part
|
from part.models import Part
|
||||||
@ -205,11 +206,11 @@ def logo_image(**kwargs):
|
|||||||
def internal_link(link, text):
|
def internal_link(link, text):
|
||||||
"""Make a <a></a> href which points to an InvenTree URL.
|
"""Make a <a></a> href which points to an InvenTree URL.
|
||||||
|
|
||||||
Uses the InvenTree.helpers.construct_absolute_url function to build the URL.
|
Uses the InvenTree.helpers_model.construct_absolute_url function to build the URL.
|
||||||
"""
|
"""
|
||||||
text = str(text)
|
text = str(text)
|
||||||
|
|
||||||
url = InvenTree.helpers.construct_absolute_url(link)
|
url = InvenTree.helpers_model.construct_absolute_url(link)
|
||||||
|
|
||||||
# If the base URL is not set, just return the text
|
# If the base URL is not set, just return the text
|
||||||
if not url:
|
if not url:
|
||||||
@ -246,7 +247,7 @@ def divide(x, y):
|
|||||||
def render_currency(money, **kwargs):
|
def render_currency(money, **kwargs):
|
||||||
"""Render a currency / Money object"""
|
"""Render a currency / Money object"""
|
||||||
|
|
||||||
return InvenTree.helpers.render_currency(money, **kwargs)
|
return InvenTree.helpers_model.render_currency(money, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag
|
@register.simple_tag
|
||||||
|
Loading…
x
Reference in New Issue
Block a user