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:
Lukas 2023-05-31 01:18:42 +02:00 committed by GitHub
parent a196f443a1
commit 99d122baa9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 343 additions and 316 deletions

View File

@ -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)

View File

@ -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)]

View 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,
)

View File

@ -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})
)

View File

@ -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):

View File

@ -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)

View File

@ -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):

View File

@ -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': {

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -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)

View File

@ -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': {

View File

@ -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,

View File

@ -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()

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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 <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)
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