diff --git a/InvenTree/InvenTree/fields.py b/InvenTree/InvenTree/fields.py index d00a419ce7..f4194274ae 100644 --- a/InvenTree/InvenTree/fields.py +++ b/InvenTree/InvenTree/fields.py @@ -12,11 +12,14 @@ from djmoney.models.fields import MoneyField as ModelMoneyField from djmoney.models.validators import MinMoneyValidator from rest_framework.fields import URLField as RestURLField +import InvenTree.helpers + from .validators import AllowedURLValidator, allowable_url_schemes class InvenTreeRestURLField(RestURLField): """Custom field for DRF with custom scheme vaildators.""" + def __init__(self, **kwargs): """Update schemes.""" @@ -109,6 +112,7 @@ class InvenTreeModelMoneyField(ModelMoneyField): class InvenTreeMoneyField(MoneyField): """Custom MoneyField for clean migrations while using dynamic currency settings.""" + def __init__(self, *args, **kwargs): """Override initial values with the real info from database.""" kwargs.update(money_kwargs()) @@ -148,8 +152,6 @@ class DatePickerFormField(forms.DateField): def round_decimal(value, places, normalize=False): """Round value to the specified number of places.""" - import InvenTree.helpers - if type(value) in [Decimal, float]: value = round(value, places) diff --git a/InvenTree/InvenTree/helpers.py b/InvenTree/InvenTree/helpers.py index 0bdf722b36..8254a9281e 100644 --- a/InvenTree/InvenTree/helpers.py +++ b/InvenTree/InvenTree/helpers.py @@ -14,23 +14,15 @@ from django.conf import settings from django.contrib.staticfiles.storage import StaticFilesStorage from django.core.exceptions import FieldError, ValidationError 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.utils.translation import gettext_lazy as _ -import moneyed.localization import regex -import requests from bleach import clean -from djmoney.contrib.exchange.models import convert_money from djmoney.money import Money from PIL import Image -import common.models import InvenTree.version -from common.notifications import (InvenTreeNotificationBodies, - NotificationBody, trigger_notification) from common.settings import currency_code_default from .settings import MEDIA_URL, STATIC_URL @@ -38,11 +30,6 @@ from .settings import MEDIA_URL, STATIC_URL 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): """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)) -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): """Test if an image file is indeed an image.""" try: @@ -1016,120 +853,3 @@ def inheritors(cls): subcls.add(child) work.append(child) 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)] diff --git a/InvenTree/InvenTree/helpers_model.py b/InvenTree/InvenTree/helpers_model.py new file mode 100644 index 0000000000..35ba9d8f9f --- /dev/null +++ b/InvenTree/InvenTree/helpers_model.py @@ -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, + ) diff --git a/InvenTree/InvenTree/models.py b/InvenTree/InvenTree/models.py index 5f17a031a5..0f3cdcc424 100644 --- a/InvenTree/InvenTree/models.py +++ b/InvenTree/InvenTree/models.py @@ -21,10 +21,10 @@ from error_report.models import Error from mptt.exceptions import InvalidMove from mptt.models import MPTTModel, TreeForeignKey -import common.models import InvenTree.fields import InvenTree.format import InvenTree.helpers +import InvenTree.helpers_model from InvenTree.sanitizer import sanitize_svg logger = logging.getLogger('inventree') @@ -207,7 +207,9 @@ class ReferenceIndexingMixin(models.Model): if cls.REFERENCE_PATTERN_SETTING is None: 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 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) - link = InvenTree.helpers.construct_absolute_url( + link = InvenTree.helpers_model.construct_absolute_url( reverse('admin:error_report_error_change', kwargs={'object_id': instance.pk}) ) diff --git a/InvenTree/InvenTree/serializers.py b/InvenTree/InvenTree/serializers.py index 9d86af03fb..cede1ecbe7 100644 --- a/InvenTree/InvenTree/serializers.py +++ b/InvenTree/InvenTree/serializers.py @@ -24,7 +24,7 @@ from taggit.serializers import TaggitSerializer from common.models import InvenTreeSetting from common.settings import currency_code_default, currency_code_mappings 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): diff --git a/InvenTree/InvenTree/tests.py b/InvenTree/InvenTree/tests.py index a107bb934a..5d459f018e 100644 --- a/InvenTree/InvenTree/tests.py +++ b/InvenTree/InvenTree/tests.py @@ -21,6 +21,7 @@ from djmoney.money import Money import InvenTree.conversion import InvenTree.format import InvenTree.helpers +import InvenTree.helpers_model import InvenTree.tasks from common.models import InvenTreeSetting from common.settings import currency_codes @@ -282,7 +283,7 @@ class TestHelpers(TestCase): "\\invalid-url" ]: 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): """Helper function for unit testing downloads. @@ -297,7 +298,7 @@ class TestHelpers(TestCase): while tries < retries: try: - helpers.download_image_from_url(url, timeout=timeout) + InvenTree.helpers_model.download_image_from_url(url, timeout=timeout) break except Exception as exc: if type(exc) is expected_error: @@ -323,20 +324,20 @@ class TestHelpers(TestCase): # Attempt to download an image which is too large 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 InvenTreeSetting.set_setting('INVENTREE_DOWNLOAD_IMAGE_MAX_SIZE', 5, change_user=None) # 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): """Test the getModelsWithMixin function""" from InvenTree.models import InvenTreeBarcodeMixin - models = helpers.getModelsWithMixin(InvenTreeBarcodeMixin) + models = InvenTree.helpers_model.getModelsWithMixin(InvenTreeBarcodeMixin) self.assertIn(Part, models) self.assertIn(StockLocation, models) diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index ee9c01fcf3..41b1babd86 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -27,6 +27,7 @@ from build.validators import generate_next_build_reference, validate_build_order import InvenTree.fields import InvenTree.helpers +import InvenTree.helpers_model import InvenTree.models import InvenTree.ready import InvenTree.tasks @@ -539,7 +540,7 @@ class Build(MPTTModel, InvenTree.models.InvenTreeBarcodeMixin, InvenTree.models. 'name': name, 'slug': 'build.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': { 'html': 'email/build_order_completed.html', '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) # 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): diff --git a/InvenTree/build/tasks.py b/InvenTree/build/tasks.py index 0acf05d5da..26ee2f1cda 100644 --- a/InvenTree/build/tasks.py +++ b/InvenTree/build/tasks.py @@ -13,7 +13,7 @@ from plugin.events import trigger_event import common.notifications import build.models import InvenTree.email -import InvenTree.helpers +import InvenTree.helpers_model import InvenTree.tasks from InvenTree.status_codes import BuildStatus 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 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, 'in_stock': in_stock, '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}") context = { - 'link': InvenTree.helpers.construct_absolute_url(build.get_absolute_url()), + 'link': InvenTree.helpers_model.construct_absolute_url(build.get_absolute_url()), 'build': build, 'part': build.part, 'lines': lines, @@ -122,7 +122,7 @@ def notify_overdue_build_order(bo: build.models.Build): 'order': bo, 'name': name, '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(), ), 'template': { diff --git a/InvenTree/common/serializers.py b/InvenTree/common/serializers.py index dff7a570e4..c74dc76fd4 100644 --- a/InvenTree/common/serializers.py +++ b/InvenTree/common/serializers.py @@ -7,7 +7,8 @@ from rest_framework import serializers from common.models import (InvenTreeSetting, InvenTreeUserSetting, NewsFeedEntry, NotesImage, NotificationMessage, 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, InvenTreeModelSerializer) diff --git a/InvenTree/common/tasks.py b/InvenTree/common/tasks.py index 11dd5d4441..fea45f0b3f 100644 --- a/InvenTree/common/tasks.py +++ b/InvenTree/common/tasks.py @@ -11,7 +11,7 @@ from django.utils import timezone import feedparser -from InvenTree.helpers import getModelsWithMixin +from InvenTree.helpers_model import getModelsWithMixin from InvenTree.models import InvenTreeNotesMixin from InvenTree.tasks import ScheduledTask, scheduled_task diff --git a/InvenTree/label/models.py b/InvenTree/label/models.py index f36cf70234..8bbd1b19a0 100644 --- a/InvenTree/label/models.py +++ b/InvenTree/label/models.py @@ -15,7 +15,8 @@ from django.utils.translation import gettext_lazy as _ import part.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 plugin.registry import registry diff --git a/InvenTree/order/api.py b/InvenTree/order/api.py index 0f1b05db4b..c0c4718971 100644 --- a/InvenTree/order/api.py +++ b/InvenTree/order/api.py @@ -19,8 +19,8 @@ from company.models import SupplierPart from InvenTree.api import (APIDownloadMixin, AttachmentMixin, ListCreateDestroyAPIView, MetadataView, StatusView) from InvenTree.filters import SEARCH_ORDER_FILTER, SEARCH_ORDER_FILTER_ALIAS -from InvenTree.helpers import (DownloadFile, construct_absolute_url, - get_base_url, str2bool) +from InvenTree.helpers import DownloadFile, str2bool +from InvenTree.helpers_model import construct_absolute_url, get_base_url from InvenTree.mixins import (CreateAPI, ListAPI, ListCreateAPI, RetrieveUpdateDestroyAPI) from InvenTree.status_codes import (PurchaseOrderStatus, ReturnOrderLineStatus, diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index 8dc71aa011..2ff767e404 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -36,7 +36,8 @@ from company.models import Company, Contact, SupplierPart from InvenTree.exceptions import log_error from InvenTree.fields import (InvenTreeModelMoneyField, InvenTreeURLField, 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, InvenTreeNotesMixin, MetadataMixin, ReferenceIndexingMixin) diff --git a/InvenTree/order/tasks.py b/InvenTree/order/tasks.py index 0de2b033ed..e04906bd00 100644 --- a/InvenTree/order/tasks.py +++ b/InvenTree/order/tasks.py @@ -5,7 +5,7 @@ from datetime import datetime, timedelta from django.utils.translation import gettext_lazy as _ import common.notifications -import InvenTree.helpers +import InvenTree.helpers_model import order.models from InvenTree.status_codes import PurchaseOrderStatus, SalesOrderStatus from InvenTree.tasks import ScheduledTask, scheduled_task @@ -29,7 +29,7 @@ def notify_overdue_purchase_order(po: order.models.PurchaseOrder): 'order': po, 'name': name, '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(), ), 'template': { @@ -92,7 +92,7 @@ def notify_overdue_sales_order(so: order.models.SalesOrder): 'order': so, 'name': name, '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(), ), 'template': { diff --git a/InvenTree/part/tasks.py b/InvenTree/part/tasks.py index 69ec85485f..198147ce6a 100644 --- a/InvenTree/part/tasks.py +++ b/InvenTree/part/tasks.py @@ -21,6 +21,7 @@ import common.notifications import common.settings import company.models import InvenTree.helpers +import InvenTree.helpers_model import InvenTree.tasks import part.models import stock.models @@ -41,7 +42,7 @@ def notify_low_stock(part: part.models.Part): 'part': part, 'name': name, 'message': message, - 'link': InvenTree.helpers.construct_absolute_url(part.get_absolute_url()), + 'link': InvenTree.helpers_model.construct_absolute_url(part.get_absolute_url()), 'template': { 'html': 'email/low_stock_notification.html', 'subject': name, diff --git a/InvenTree/part/templatetags/inventree_extras.py b/InvenTree/part/templatetags/inventree_extras.py index b90c61e588..ab4255c9fe 100644 --- a/InvenTree/part/templatetags/inventree_extras.py +++ b/InvenTree/part/templatetags/inventree_extras.py @@ -14,6 +14,7 @@ from django.utils.safestring import mark_safe from django.utils.translation import gettext_lazy as _ import InvenTree.helpers +import InvenTree.helpers_model from common.models import ColorTheme, InvenTreeSetting, InvenTreeUserSetting from common.settings import currency_code_default from InvenTree import settings, version @@ -105,7 +106,7 @@ def render_date(context, date_object): def render_currency(money, **kwargs): """Render a currency / Money object""" - return InvenTree.helpers.render_currency(money, **kwargs) + return InvenTree.helpers_model.render_currency(money, **kwargs) @register.simple_tag() @@ -224,7 +225,7 @@ def inventree_splash(**kwargs): @register.simple_tag() def inventree_base_url(*args, **kwargs): """Return the base URL of the InvenTree server""" - return InvenTree.helpers.get_base_url() + return InvenTree.helpers_model.get_base_url() @register.simple_tag() diff --git a/InvenTree/plugin/base/integration/mixins.py b/InvenTree/plugin/base/integration/mixins.py index 46a394fa3a..be3e8c8ba5 100644 --- a/InvenTree/plugin/base/integration/mixins.py +++ b/InvenTree/plugin/base/integration/mixins.py @@ -7,6 +7,7 @@ import requests import part.models import stock.models +from InvenTree.helpers import generateTestKey from plugin.helpers import (MixinNotImplementedError, render_template, render_text) @@ -444,7 +445,6 @@ class PanelMixin: Returns: Array of panels """ - import InvenTree.helpers panels = [] @@ -482,7 +482,7 @@ class PanelMixin: panel['slug'] = self.slug # 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) diff --git a/InvenTree/plugin/builtin/barcodes/inventree_barcode.py b/InvenTree/plugin/builtin/barcodes/inventree_barcode.py index 398ad4cdd5..cdfa756779 100644 --- a/InvenTree/plugin/builtin/barcodes/inventree_barcode.py +++ b/InvenTree/plugin/builtin/barcodes/inventree_barcode.py @@ -11,7 +11,8 @@ import json 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 plugin import InvenTreePlugin from plugin.mixins import BarcodeMixin diff --git a/InvenTree/report/models.py b/InvenTree/report/models.py index 3f6e482570..b9577e6c98 100644 --- a/InvenTree/report/models.py +++ b/InvenTree/report/models.py @@ -20,7 +20,8 @@ import common.models import order.models import part.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 plugin.registry import registry diff --git a/InvenTree/report/templatetags/report.py b/InvenTree/report/templatetags/report.py index e617264f8e..4f293eb81d 100644 --- a/InvenTree/report/templatetags/report.py +++ b/InvenTree/report/templatetags/report.py @@ -8,6 +8,7 @@ from django.conf import settings from django.utils.safestring import SafeString, mark_safe import InvenTree.helpers +import InvenTree.helpers_model from common.models import InvenTreeSetting from company.models import Company from part.models import Part @@ -205,11 +206,11 @@ def logo_image(**kwargs): def internal_link(link, text): """Make 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) - 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 not url: @@ -246,7 +247,7 @@ def divide(x, y): def render_currency(money, **kwargs): """Render a currency / Money object""" - return InvenTree.helpers.render_currency(money, **kwargs) + return InvenTree.helpers_model.render_currency(money, **kwargs) @register.simple_tag