mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-11-03 22:55:43 +00:00 
			
		
		
		
	Merge branch 'master' into build-output-complete
This commit is contained in:
		@@ -34,18 +34,47 @@ class InvenTreeOrderingFilter(OrderingFilter):
 | 
			
		||||
            Ordering fields should be mapped to separate fields
 | 
			
		||||
            """
 | 
			
		||||
 | 
			
		||||
            for idx, field in enumerate(ordering):
 | 
			
		||||
            ordering_initial = ordering
 | 
			
		||||
            ordering = []
 | 
			
		||||
 | 
			
		||||
                reverse = False
 | 
			
		||||
            for field in ordering_initial:
 | 
			
		||||
                
 | 
			
		||||
                if field.startswith('-'):
 | 
			
		||||
                    field = field[1:]
 | 
			
		||||
                    reverse = True
 | 
			
		||||
 | 
			
		||||
                if field in aliases:
 | 
			
		||||
                    ordering[idx] = aliases[field]
 | 
			
		||||
                reverse = field.startswith('-')
 | 
			
		||||
 | 
			
		||||
                if reverse:
 | 
			
		||||
                        ordering[idx] = '-' + ordering[idx]
 | 
			
		||||
                    field = field[1:]
 | 
			
		||||
 | 
			
		||||
                # Are aliases defined for this field?
 | 
			
		||||
                if field in aliases:
 | 
			
		||||
                    alias = aliases[field]
 | 
			
		||||
                else:
 | 
			
		||||
                    alias = field
 | 
			
		||||
 | 
			
		||||
                """
 | 
			
		||||
                Potentially, a single field could be "aliased" to multiple field,
 | 
			
		||||
                
 | 
			
		||||
                (For example to enforce a particular ordering sequence)
 | 
			
		||||
 | 
			
		||||
                e.g. to filter first by the integer value...
 | 
			
		||||
 | 
			
		||||
                ordering_field_aliases = {
 | 
			
		||||
                    "reference": ["integer_ref", "reference"]
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                """
 | 
			
		||||
 | 
			
		||||
                if type(alias) is str:
 | 
			
		||||
                    alias = [alias]
 | 
			
		||||
                elif type(alias) in [list, tuple]:
 | 
			
		||||
                    pass
 | 
			
		||||
                else:
 | 
			
		||||
                    # Unsupported alias type
 | 
			
		||||
                    continue
 | 
			
		||||
 | 
			
		||||
                for a in alias:
 | 
			
		||||
                    if reverse:
 | 
			
		||||
                        a = '-' + a
 | 
			
		||||
 | 
			
		||||
                    ordering.append(a)
 | 
			
		||||
 | 
			
		||||
        return ordering
 | 
			
		||||
 
 | 
			
		||||
@@ -4,10 +4,12 @@ Helper forms which subclass Django forms to provide additional functionality
 | 
			
		||||
 | 
			
		||||
# -*- coding: utf-8 -*-
 | 
			
		||||
from __future__ import unicode_literals
 | 
			
		||||
import logging
 | 
			
		||||
 | 
			
		||||
from django.utils.translation import ugettext_lazy as _
 | 
			
		||||
from django import forms
 | 
			
		||||
from django.contrib.auth.models import User
 | 
			
		||||
from django.contrib.auth.models import User, Group
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
 | 
			
		||||
from crispy_forms.helper import FormHelper
 | 
			
		||||
from crispy_forms.layout import Layout, Field
 | 
			
		||||
@@ -20,6 +22,8 @@ from allauth.socialaccount.adapter import DefaultSocialAccountAdapter
 | 
			
		||||
from part.models import PartCategory
 | 
			
		||||
from common.models import InvenTreeSetting
 | 
			
		||||
 | 
			
		||||
logger = logging.getLogger('inventree')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class HelperForm(forms.ModelForm):
 | 
			
		||||
    """ Provides simple integration of crispy_forms extension. """
 | 
			
		||||
@@ -223,11 +227,11 @@ class CustomSignupForm(SignupForm):
 | 
			
		||||
        # check for two mail fields
 | 
			
		||||
        if InvenTreeSetting.get_setting('LOGIN_SIGNUP_MAIL_TWICE'):
 | 
			
		||||
            self.fields["email2"] = forms.EmailField(
 | 
			
		||||
                label=_("E-mail (again)"),
 | 
			
		||||
                label=_("Email (again)"),
 | 
			
		||||
                widget=forms.TextInput(
 | 
			
		||||
                    attrs={
 | 
			
		||||
                        "type": "email",
 | 
			
		||||
                        "placeholder": _("E-mail address confirmation"),
 | 
			
		||||
                        "placeholder": _("Email address confirmation"),
 | 
			
		||||
                    }
 | 
			
		||||
                ),
 | 
			
		||||
            )
 | 
			
		||||
@@ -256,11 +260,23 @@ class RegistratonMixin:
 | 
			
		||||
    """
 | 
			
		||||
    Mixin to check if registration should be enabled
 | 
			
		||||
    """
 | 
			
		||||
    def is_open_for_signup(self, request):
 | 
			
		||||
        if InvenTreeSetting.get_setting('EMAIL_HOST', None) and InvenTreeSetting.get_setting('LOGIN_ENABLE_REG', True):
 | 
			
		||||
            return super().is_open_for_signup(request)
 | 
			
		||||
    def is_open_for_signup(self, request, *args, **kwargs):
 | 
			
		||||
        if settings.EMAIL_HOST and InvenTreeSetting.get_setting('LOGIN_ENABLE_REG', True):
 | 
			
		||||
            return super().is_open_for_signup(request, *args, **kwargs)
 | 
			
		||||
        return False
 | 
			
		||||
 | 
			
		||||
    def save_user(self, request, user, form, commit=True):
 | 
			
		||||
        user = super().save_user(request, user, form, commit=commit)
 | 
			
		||||
        start_group = InvenTreeSetting.get_setting('SIGNUP_GROUP')
 | 
			
		||||
        if start_group:
 | 
			
		||||
            try:
 | 
			
		||||
                group = Group.objects.get(id=start_group)
 | 
			
		||||
                user.groups.add(group)
 | 
			
		||||
            except Group.DoesNotExist:
 | 
			
		||||
                logger.error('The setting `SIGNUP_GROUP` contains an non existant group', start_group)
 | 
			
		||||
        user.save()
 | 
			
		||||
        return user
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CustomAccountAdapter(RegistratonMixin, DefaultAccountAdapter):
 | 
			
		||||
    """
 | 
			
		||||
@@ -268,7 +284,7 @@ class CustomAccountAdapter(RegistratonMixin, DefaultAccountAdapter):
 | 
			
		||||
    """
 | 
			
		||||
    def send_mail(self, template_prefix, email, context):
 | 
			
		||||
        """only send mail if backend configured"""
 | 
			
		||||
        if InvenTreeSetting.get_setting('EMAIL_HOST', None):
 | 
			
		||||
        if settings.EMAIL_HOST:
 | 
			
		||||
            return super().send_mail(template_prefix, email, context)
 | 
			
		||||
        return False
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,7 @@ Generic models which provide extra functionality over base Django model types.
 | 
			
		||||
 | 
			
		||||
from __future__ import unicode_literals
 | 
			
		||||
 | 
			
		||||
import re
 | 
			
		||||
import os
 | 
			
		||||
import logging
 | 
			
		||||
 | 
			
		||||
@@ -43,6 +44,48 @@ def rename_attachment(instance, filename):
 | 
			
		||||
    return os.path.join(instance.getSubdir(), filename)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ReferenceIndexingMixin(models.Model):
 | 
			
		||||
    """
 | 
			
		||||
    A mixin for keeping track of numerical copies of the "reference" field.
 | 
			
		||||
 | 
			
		||||
    Here, we attempt to convert a "reference" field value (char) to an integer,
 | 
			
		||||
    for performing fast natural sorting.
 | 
			
		||||
 | 
			
		||||
    This requires extra database space (due to the extra table column),
 | 
			
		||||
    but is required as not all supported database backends provide equivalent casting.
 | 
			
		||||
 | 
			
		||||
    This mixin adds a field named 'reference_int'.
 | 
			
		||||
 | 
			
		||||
    - If the 'reference' field can be cast to an integer, it is stored here
 | 
			
		||||
    - If the 'reference' field *starts* with an integer, it is stored here
 | 
			
		||||
    - Otherwise, we store zero
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        abstract = True
 | 
			
		||||
 | 
			
		||||
    def rebuild_reference_field(self):
 | 
			
		||||
 | 
			
		||||
        reference = getattr(self, 'reference', '')
 | 
			
		||||
 | 
			
		||||
        # Default value if we cannot convert to an integer
 | 
			
		||||
        ref_int = 0
 | 
			
		||||
 | 
			
		||||
        # Look at the start of the string - can it be "integerized"?
 | 
			
		||||
        result = re.match(r"^(\d+)", reference)
 | 
			
		||||
 | 
			
		||||
        if result and len(result.groups()) == 1:
 | 
			
		||||
            ref = result.groups()[0]
 | 
			
		||||
            try:
 | 
			
		||||
                ref_int = int(ref)
 | 
			
		||||
            except:
 | 
			
		||||
                ref_int = 0
 | 
			
		||||
 | 
			
		||||
        self.reference_int = ref_int
 | 
			
		||||
 | 
			
		||||
    reference_int = models.IntegerField(default=0)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class InvenTreeAttachment(models.Model):
 | 
			
		||||
    """ Provides an abstracted class for managing file attachments.
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -385,39 +385,6 @@ Q_CLUSTER = {
 | 
			
		||||
    'sync': False,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
# Markdownx configuration
 | 
			
		||||
# Ref: https://neutronx.github.io/django-markdownx/customization/
 | 
			
		||||
MARKDOWNX_MEDIA_PATH = datetime.now().strftime('markdownx/%Y/%m/%d')
 | 
			
		||||
 | 
			
		||||
# Markdownify configuration
 | 
			
		||||
# Ref: https://django-markdownify.readthedocs.io/en/latest/settings.html
 | 
			
		||||
 | 
			
		||||
MARKDOWNIFY_WHITELIST_TAGS = [
 | 
			
		||||
    'a',
 | 
			
		||||
    'abbr',
 | 
			
		||||
    'b',
 | 
			
		||||
    'blockquote',
 | 
			
		||||
    'em',
 | 
			
		||||
    'h1', 'h2', 'h3',
 | 
			
		||||
    'i',
 | 
			
		||||
    'img',
 | 
			
		||||
    'li',
 | 
			
		||||
    'ol',
 | 
			
		||||
    'p',
 | 
			
		||||
    'strong',
 | 
			
		||||
    'ul'
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
MARKDOWNIFY_WHITELIST_ATTRS = [
 | 
			
		||||
    'href',
 | 
			
		||||
    'src',
 | 
			
		||||
    'alt',
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
MARKDOWNIFY_BLEACH = False
 | 
			
		||||
 | 
			
		||||
DATABASES = {}
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
Configure the database backend based on the user-specified values.
 | 
			
		||||
 | 
			
		||||
@@ -484,7 +451,47 @@ logger.info(f"DB_ENGINE: {db_engine}")
 | 
			
		||||
logger.info(f"DB_NAME: {db_name}")
 | 
			
		||||
logger.info(f"DB_HOST: {db_host}")
 | 
			
		||||
 | 
			
		||||
DATABASES['default'] = db_config
 | 
			
		||||
"""
 | 
			
		||||
In addition to base-level database configuration, we may wish to specify specific options to the database backend
 | 
			
		||||
Ref: https://docs.djangoproject.com/en/3.2/ref/settings/#std:setting-OPTIONS
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
# 'OPTIONS' or 'options' can be specified in config.yaml
 | 
			
		||||
db_options = db_config.get('OPTIONS', db_config.get('options', {}))
 | 
			
		||||
 | 
			
		||||
# Specific options for postgres backend
 | 
			
		||||
if 'postgres' in db_engine:
 | 
			
		||||
    from psycopg2.extensions import ISOLATION_LEVEL_READ_COMMITTED, ISOLATION_LEVEL_SERIALIZABLE
 | 
			
		||||
 | 
			
		||||
    # Connection timeout
 | 
			
		||||
    if 'connect_timeout' not in db_options:
 | 
			
		||||
        db_options['connect_timeout'] = int(os.getenv('INVENTREE_DB_TIMEOUT', 2))
 | 
			
		||||
 | 
			
		||||
    # Postgres's default isolation level is Read Committed which is
 | 
			
		||||
    # normally fine, but most developers think the database server is
 | 
			
		||||
    # actually going to do Serializable type checks on the queries to
 | 
			
		||||
    # protect against simultaneous changes.
 | 
			
		||||
    if 'isolation_level' not in db_options:
 | 
			
		||||
        serializable = _is_true(os.getenv("PG_ISOLATION_SERIALIZABLE", "true"))
 | 
			
		||||
        db_options['isolation_level'] = ISOLATION_LEVEL_SERIALIZABLE if serializable else ISOLATION_LEVEL_READ_COMMITTED
 | 
			
		||||
 | 
			
		||||
# Specific options for MySql / MariaDB backend
 | 
			
		||||
if 'mysql' in db_engine:
 | 
			
		||||
    # TODO
 | 
			
		||||
    pass
 | 
			
		||||
 | 
			
		||||
# Specific options for sqlite backend
 | 
			
		||||
if 'sqlite' in db_engine:
 | 
			
		||||
    # TODO
 | 
			
		||||
    pass
 | 
			
		||||
 | 
			
		||||
# Provide OPTIONS dict back to the database configuration dict
 | 
			
		||||
db_config['OPTIONS'] = db_options
 | 
			
		||||
 | 
			
		||||
DATABASES = {
 | 
			
		||||
    'default': db_config
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
CACHES = {
 | 
			
		||||
    'default': {
 | 
			
		||||
@@ -683,3 +690,34 @@ ACCOUNT_FORMS = {
 | 
			
		||||
 | 
			
		||||
SOCIALACCOUNT_ADAPTER = 'InvenTree.forms.CustomSocialAccountAdapter'
 | 
			
		||||
ACCOUNT_ADAPTER = 'InvenTree.forms.CustomAccountAdapter'
 | 
			
		||||
 | 
			
		||||
# Markdownx configuration
 | 
			
		||||
# Ref: https://neutronx.github.io/django-markdownx/customization/
 | 
			
		||||
MARKDOWNX_MEDIA_PATH = datetime.now().strftime('markdownx/%Y/%m/%d')
 | 
			
		||||
 | 
			
		||||
# Markdownify configuration
 | 
			
		||||
# Ref: https://django-markdownify.readthedocs.io/en/latest/settings.html
 | 
			
		||||
 | 
			
		||||
MARKDOWNIFY_WHITELIST_TAGS = [
 | 
			
		||||
    'a',
 | 
			
		||||
    'abbr',
 | 
			
		||||
    'b',
 | 
			
		||||
    'blockquote',
 | 
			
		||||
    'em',
 | 
			
		||||
    'h1', 'h2', 'h3',
 | 
			
		||||
    'i',
 | 
			
		||||
    'img',
 | 
			
		||||
    'li',
 | 
			
		||||
    'ol',
 | 
			
		||||
    'p',
 | 
			
		||||
    'strong',
 | 
			
		||||
    'ul'
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
MARKDOWNIFY_WHITELIST_ATTRS = [
 | 
			
		||||
    'href',
 | 
			
		||||
    'src',
 | 
			
		||||
    'alt',
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
MARKDOWNIFY_BLEACH = False
 | 
			
		||||
 
 | 
			
		||||
@@ -9,6 +9,10 @@ from .models import Build, BuildItem
 | 
			
		||||
 | 
			
		||||
class BuildAdmin(ImportExportModelAdmin):
 | 
			
		||||
 | 
			
		||||
    exclude = [
 | 
			
		||||
        'reference_int',
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    list_display = (
 | 
			
		||||
        'reference',
 | 
			
		||||
        'title',
 | 
			
		||||
 
 | 
			
		||||
@@ -15,6 +15,7 @@ from django_filters import rest_framework as rest_filters
 | 
			
		||||
 | 
			
		||||
from InvenTree.api import AttachmentMixin
 | 
			
		||||
from InvenTree.helpers import str2bool, isNull
 | 
			
		||||
from InvenTree.filters import InvenTreeOrderingFilter
 | 
			
		||||
from InvenTree.status_codes import BuildStatus
 | 
			
		||||
 | 
			
		||||
from .models import Build, BuildItem, BuildOrderAttachment
 | 
			
		||||
@@ -66,7 +67,7 @@ class BuildList(generics.ListCreateAPIView):
 | 
			
		||||
    filter_backends = [
 | 
			
		||||
        DjangoFilterBackend,
 | 
			
		||||
        filters.SearchFilter,
 | 
			
		||||
        filters.OrderingFilter,
 | 
			
		||||
        InvenTreeOrderingFilter,
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    ordering_fields = [
 | 
			
		||||
@@ -81,6 +82,10 @@ class BuildList(generics.ListCreateAPIView):
 | 
			
		||||
        'responsible',
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    ordering_field_aliases = {
 | 
			
		||||
        'reference': ['reference_int', 'reference'],
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    search_fields = [
 | 
			
		||||
        'reference',
 | 
			
		||||
        'part__name',
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										18
									
								
								InvenTree/build/migrations/0031_build_reference_int.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								InvenTree/build/migrations/0031_build_reference_int.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,18 @@
 | 
			
		||||
# Generated by Django 3.2.5 on 2021-10-14 06:23
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('build', '0030_alter_build_reference'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='build',
 | 
			
		||||
            name='reference_int',
 | 
			
		||||
            field=models.IntegerField(default=0),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
							
								
								
									
										50
									
								
								InvenTree/build/migrations/0032_auto_20211014_0632.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								InvenTree/build/migrations/0032_auto_20211014_0632.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,50 @@
 | 
			
		||||
# Generated by Django 3.2.5 on 2021-10-14 06:32
 | 
			
		||||
 | 
			
		||||
import re
 | 
			
		||||
 | 
			
		||||
from django.db import migrations
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def build_refs(apps, schema_editor):
 | 
			
		||||
    """
 | 
			
		||||
    Rebuild the integer "reference fields" for existing Build objects
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    BuildOrder = apps.get_model('build', 'build')
 | 
			
		||||
 | 
			
		||||
    for build in BuildOrder.objects.all():
 | 
			
		||||
 | 
			
		||||
        ref = 0
 | 
			
		||||
 | 
			
		||||
        result = re.match(r"^(\d+)", build.reference)
 | 
			
		||||
 | 
			
		||||
        if result and len(result.groups()) == 1:
 | 
			
		||||
            try:
 | 
			
		||||
                ref = int(result.groups()[0])
 | 
			
		||||
            except:
 | 
			
		||||
                ref = 0
 | 
			
		||||
 | 
			
		||||
        build.reference_int = ref
 | 
			
		||||
        build.save()
 | 
			
		||||
 | 
			
		||||
def unbuild_refs(apps, schema_editor):
 | 
			
		||||
    """
 | 
			
		||||
    Provided only for reverse migration compatibility
 | 
			
		||||
    """
 | 
			
		||||
    pass
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    atomic = False
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('build', '0031_build_reference_int'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.RunPython(
 | 
			
		||||
            build_refs,
 | 
			
		||||
            reverse_code=unbuild_refs
 | 
			
		||||
        )
 | 
			
		||||
    ]
 | 
			
		||||
@@ -28,7 +28,7 @@ from mptt.exceptions import InvalidMove
 | 
			
		||||
from InvenTree.status_codes import BuildStatus, StockStatus, StockHistoryCode
 | 
			
		||||
from InvenTree.helpers import increment, getSetting, normalize, MakeBarcode
 | 
			
		||||
from InvenTree.validators import validate_build_order_reference
 | 
			
		||||
from InvenTree.models import InvenTreeAttachment
 | 
			
		||||
from InvenTree.models import InvenTreeAttachment, ReferenceIndexingMixin
 | 
			
		||||
 | 
			
		||||
import common.models
 | 
			
		||||
 | 
			
		||||
@@ -69,7 +69,7 @@ def get_next_build_number():
 | 
			
		||||
    return reference
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Build(MPTTModel):
 | 
			
		||||
class Build(MPTTModel, ReferenceIndexingMixin):
 | 
			
		||||
    """ A Build object organises the creation of new StockItem objects from other existing StockItem objects.
 | 
			
		||||
 | 
			
		||||
    Attributes:
 | 
			
		||||
@@ -108,6 +108,8 @@ class Build(MPTTModel):
 | 
			
		||||
 | 
			
		||||
    def save(self, *args, **kwargs):
 | 
			
		||||
 | 
			
		||||
        self.rebuild_reference_field()
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            super().save(*args, **kwargs)
 | 
			
		||||
        except InvalidMove:
 | 
			
		||||
 
 | 
			
		||||
@@ -118,6 +118,26 @@ class BuildTest(TestCase):
 | 
			
		||||
 | 
			
		||||
        self.stock_3_1 = StockItem.objects.create(part=self.sub_part_3, quantity=1000)
 | 
			
		||||
 | 
			
		||||
    def test_ref_int(self):
 | 
			
		||||
        """
 | 
			
		||||
        Test the "integer reference" field used for natural sorting
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
        for ii in range(10):
 | 
			
		||||
            build = Build(
 | 
			
		||||
                reference=f"{ii}_abcde",
 | 
			
		||||
                quantity=1,
 | 
			
		||||
                part=self.assembly,
 | 
			
		||||
                title="Making some parts"
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            self.assertEqual(build.reference_int, 0)
 | 
			
		||||
 | 
			
		||||
            build.save()
 | 
			
		||||
 | 
			
		||||
            # After saving, the integer reference should have been updated
 | 
			
		||||
            self.assertEqual(build.reference_int, ii)
 | 
			
		||||
 | 
			
		||||
    def test_init(self):
 | 
			
		||||
        # Perform some basic tests before we start the ball rolling
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,7 @@ import decimal
 | 
			
		||||
import math
 | 
			
		||||
 | 
			
		||||
from django.db import models, transaction
 | 
			
		||||
from django.contrib.auth.models import User
 | 
			
		||||
from django.contrib.auth.models import User, Group
 | 
			
		||||
from django.db.utils import IntegrityError, OperationalError
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
 | 
			
		||||
@@ -182,12 +182,9 @@ class BaseInvenTreeSetting(models.Model):
 | 
			
		||||
        else:
 | 
			
		||||
            choices = None
 | 
			
		||||
 | 
			
		||||
        """
 | 
			
		||||
        TODO:
 | 
			
		||||
        if type(choices) is function:
 | 
			
		||||
        if callable(choices):
 | 
			
		||||
            # Evaluate the function (we expect it will return a list of tuples...)
 | 
			
		||||
            return choices()
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
        return choices
 | 
			
		||||
 | 
			
		||||
@@ -479,6 +476,11 @@ class BaseInvenTreeSetting(models.Model):
 | 
			
		||||
        return value
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def settings_group_options():
 | 
			
		||||
    """build up group tuple for settings based on gour choices"""
 | 
			
		||||
    return [('', _('No group')), *[(str(a.id), str(a)) for a in Group.objects.all()]]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class InvenTreeSetting(BaseInvenTreeSetting):
 | 
			
		||||
    """
 | 
			
		||||
    An InvenTreeSetting object is a key:value pair used for storing
 | 
			
		||||
@@ -822,7 +824,7 @@ class InvenTreeSetting(BaseInvenTreeSetting):
 | 
			
		||||
            'validator': bool,
 | 
			
		||||
        },
 | 
			
		||||
        'LOGIN_MAIL_REQUIRED': {
 | 
			
		||||
            'name': _('E-Mail required'),
 | 
			
		||||
            'name': _('Email required'),
 | 
			
		||||
            'description': _('Require user to supply mail on signup'),
 | 
			
		||||
            'default': False,
 | 
			
		||||
            'validator': bool,
 | 
			
		||||
@@ -845,6 +847,12 @@ class InvenTreeSetting(BaseInvenTreeSetting):
 | 
			
		||||
            'default': True,
 | 
			
		||||
            'validator': bool,
 | 
			
		||||
        },
 | 
			
		||||
        'SIGNUP_GROUP': {
 | 
			
		||||
            'name': _('Group on signup'),
 | 
			
		||||
            'description': _('Group new user are asigned on registration'),
 | 
			
		||||
            'default': '',
 | 
			
		||||
            'choices': settings_group_options
 | 
			
		||||
        },
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
 
 | 
			
		||||
@@ -20,6 +20,10 @@ class PurchaseOrderLineItemInlineAdmin(admin.StackedInline):
 | 
			
		||||
 | 
			
		||||
class PurchaseOrderAdmin(ImportExportModelAdmin):
 | 
			
		||||
 | 
			
		||||
    exclude = [
 | 
			
		||||
        'reference_int',
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    list_display = (
 | 
			
		||||
        'reference',
 | 
			
		||||
        'supplier',
 | 
			
		||||
@@ -41,6 +45,10 @@ class PurchaseOrderAdmin(ImportExportModelAdmin):
 | 
			
		||||
 | 
			
		||||
class SalesOrderAdmin(ImportExportModelAdmin):
 | 
			
		||||
 | 
			
		||||
    exclude = [
 | 
			
		||||
        'reference_int',
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    list_display = (
 | 
			
		||||
        'reference',
 | 
			
		||||
        'customer',
 | 
			
		||||
 
 | 
			
		||||
@@ -151,9 +151,13 @@ class POList(generics.ListCreateAPIView):
 | 
			
		||||
    filter_backends = [
 | 
			
		||||
        rest_filters.DjangoFilterBackend,
 | 
			
		||||
        filters.SearchFilter,
 | 
			
		||||
        filters.OrderingFilter,
 | 
			
		||||
        InvenTreeOrderingFilter,
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    ordering_field_aliases = {
 | 
			
		||||
        'reference': ['reference_int', 'reference'],
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    filter_fields = [
 | 
			
		||||
        'supplier',
 | 
			
		||||
    ]
 | 
			
		||||
@@ -489,9 +493,13 @@ class SOList(generics.ListCreateAPIView):
 | 
			
		||||
    filter_backends = [
 | 
			
		||||
        rest_filters.DjangoFilterBackend,
 | 
			
		||||
        filters.SearchFilter,
 | 
			
		||||
        filters.OrderingFilter,
 | 
			
		||||
        InvenTreeOrderingFilter,
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    ordering_field_aliases = {
 | 
			
		||||
        'reference': ['reference_int', 'reference'],
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    filter_fields = [
 | 
			
		||||
        'customer',
 | 
			
		||||
    ]
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										23
									
								
								InvenTree/order/migrations/0051_auto_20211014_0623.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								InvenTree/order/migrations/0051_auto_20211014_0623.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,23 @@
 | 
			
		||||
# Generated by Django 3.2.5 on 2021-10-14 06:23
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('order', '0050_alter_purchaseorderlineitem_destination'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='purchaseorder',
 | 
			
		||||
            name='reference_int',
 | 
			
		||||
            field=models.IntegerField(default=0),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='salesorder',
 | 
			
		||||
            name='reference_int',
 | 
			
		||||
            field=models.IntegerField(default=0),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
							
								
								
									
										66
									
								
								InvenTree/order/migrations/0052_auto_20211014_0631.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								InvenTree/order/migrations/0052_auto_20211014_0631.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,66 @@
 | 
			
		||||
# Generated by Django 3.2.5 on 2021-10-14 06:31
 | 
			
		||||
 | 
			
		||||
import re
 | 
			
		||||
 | 
			
		||||
from django.db import migrations
 | 
			
		||||
 | 
			
		||||
def build_refs(apps, schema_editor):
 | 
			
		||||
    """
 | 
			
		||||
    Rebuild the integer "reference fields" for existing Build objects
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    PurchaseOrder = apps.get_model('order', 'purchaseorder')
 | 
			
		||||
 | 
			
		||||
    for order in PurchaseOrder.objects.all():
 | 
			
		||||
 | 
			
		||||
        ref = 0
 | 
			
		||||
 | 
			
		||||
        result = re.match(r"^(\d+)", order.reference)
 | 
			
		||||
 | 
			
		||||
        if result and len(result.groups()) == 1:
 | 
			
		||||
            try:
 | 
			
		||||
                ref = int(result.groups()[0])
 | 
			
		||||
            except:
 | 
			
		||||
                ref = 0
 | 
			
		||||
 | 
			
		||||
        order.reference_int = ref
 | 
			
		||||
        order.save()
 | 
			
		||||
 | 
			
		||||
    SalesOrder = apps.get_model('order', 'salesorder')
 | 
			
		||||
 | 
			
		||||
    for order in SalesOrder.objects.all():
 | 
			
		||||
 | 
			
		||||
        ref = 0
 | 
			
		||||
 | 
			
		||||
        result = re.match(r"^(\d+)", order.reference)
 | 
			
		||||
 | 
			
		||||
        if result and len(result.groups()) == 1:
 | 
			
		||||
            try:
 | 
			
		||||
                ref = int(result.groups()[0])
 | 
			
		||||
            except:
 | 
			
		||||
                ref = 0
 | 
			
		||||
 | 
			
		||||
        order.reference_int = ref
 | 
			
		||||
        order.save()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def unbuild_refs(apps, schema_editor):
 | 
			
		||||
    """
 | 
			
		||||
    Provided only for reverse migration compatibility
 | 
			
		||||
    """
 | 
			
		||||
    pass
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('order', '0051_auto_20211014_0623'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.RunPython(
 | 
			
		||||
            build_refs,
 | 
			
		||||
            reverse_code=unbuild_refs
 | 
			
		||||
        )
 | 
			
		||||
    ]
 | 
			
		||||
@@ -28,7 +28,7 @@ from company.models import Company, SupplierPart
 | 
			
		||||
from InvenTree.fields import InvenTreeModelMoneyField, RoundingDecimalField
 | 
			
		||||
from InvenTree.helpers import decimal2string, increment, getSetting
 | 
			
		||||
from InvenTree.status_codes import PurchaseOrderStatus, SalesOrderStatus, StockStatus, StockHistoryCode
 | 
			
		||||
from InvenTree.models import InvenTreeAttachment
 | 
			
		||||
from InvenTree.models import InvenTreeAttachment, ReferenceIndexingMixin
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_next_po_number():
 | 
			
		||||
@@ -89,7 +89,7 @@ def get_next_so_number():
 | 
			
		||||
    return reference
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Order(models.Model):
 | 
			
		||||
class Order(ReferenceIndexingMixin):
 | 
			
		||||
    """ Abstract model for an order.
 | 
			
		||||
 | 
			
		||||
    Instances of this class:
 | 
			
		||||
@@ -147,6 +147,9 @@ class Order(models.Model):
 | 
			
		||||
        return new_ref
 | 
			
		||||
 | 
			
		||||
    def save(self, *args, **kwargs):
 | 
			
		||||
 | 
			
		||||
        self.rebuild_reference_field()
 | 
			
		||||
 | 
			
		||||
        if not self.creation_date:
 | 
			
		||||
            self.creation_date = datetime.now().date()
 | 
			
		||||
 | 
			
		||||
@@ -531,6 +534,12 @@ class SalesOrder(Order):
 | 
			
		||||
 | 
			
		||||
        return queryset
 | 
			
		||||
 | 
			
		||||
    def save(self, *args, **kwargs):
 | 
			
		||||
 | 
			
		||||
        self.rebuild_reference_field()
 | 
			
		||||
 | 
			
		||||
        super().save(*args, **kwargs)
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
 | 
			
		||||
        prefix = getSetting('SALESORDER_REFERENCE_PREFIX')
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										59
									
								
								InvenTree/order/test_migrations.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								InvenTree/order/test_migrations.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,59 @@
 | 
			
		||||
"""
 | 
			
		||||
Unit tests for the 'order' model data migrations
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
from django_test_migrations.contrib.unittest_case import MigratorTestCase
 | 
			
		||||
 | 
			
		||||
from InvenTree import helpers
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestForwardMigrations(MigratorTestCase):
 | 
			
		||||
    """
 | 
			
		||||
    Test entire schema migration
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    migrate_from = ('order', helpers.getOldestMigrationFile('order'))
 | 
			
		||||
    migrate_to = ('order', helpers.getNewestMigrationFile('order'))
 | 
			
		||||
 | 
			
		||||
    def prepare(self):
 | 
			
		||||
        """
 | 
			
		||||
        Create initial data set
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
        # Create a purchase order from a supplier
 | 
			
		||||
        Company = self.old_state.apps.get_model('company', 'company')
 | 
			
		||||
 | 
			
		||||
        supplier = Company.objects.create(
 | 
			
		||||
            name='Supplier A',
 | 
			
		||||
            description='A great supplier!',
 | 
			
		||||
            is_supplier=True
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        PurchaseOrder = self.old_state.apps.get_model('order', 'purchaseorder')
 | 
			
		||||
 | 
			
		||||
        # Create some orders
 | 
			
		||||
        for ii in range(10):
 | 
			
		||||
 | 
			
		||||
            order = PurchaseOrder.objects.create(
 | 
			
		||||
                supplier=supplier,
 | 
			
		||||
                reference=f"{ii}-abcde",
 | 
			
		||||
                description="Just a test order"
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            # Initially, the 'reference_int' field is unavailable
 | 
			
		||||
            with self.assertRaises(AttributeError):
 | 
			
		||||
                print(order.reference_int)
 | 
			
		||||
 | 
			
		||||
    def test_ref_field(self):
 | 
			
		||||
        """
 | 
			
		||||
        Test that the 'reference_int' field has been created and is filled out correctly
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
        PurchaseOrder = self.new_state.apps.get_model('order', 'purchaseorder')
 | 
			
		||||
 | 
			
		||||
        for ii in range(10):
 | 
			
		||||
 | 
			
		||||
            order = PurchaseOrder.objects.get(reference=f"{ii}-abcde")
 | 
			
		||||
 | 
			
		||||
            # The integer reference field must have been correctly updated
 | 
			
		||||
        self.assertEqual(order.reference_int, ii)
 | 
			
		||||
@@ -814,6 +814,27 @@ class PartList(generics.ListCreateAPIView):
 | 
			
		||||
            except (ValueError, Part.DoesNotExist):
 | 
			
		||||
                pass
 | 
			
		||||
 | 
			
		||||
        # Exclude specific part ID values?
 | 
			
		||||
        exclude_id = []
 | 
			
		||||
 | 
			
		||||
        for key in ['exclude_id', 'exclude_id[]']:
 | 
			
		||||
            if key in params:
 | 
			
		||||
                exclude_id += params.getlist(key, [])
 | 
			
		||||
 | 
			
		||||
        if exclude_id:
 | 
			
		||||
 | 
			
		||||
            id_values = []
 | 
			
		||||
 | 
			
		||||
            for val in exclude_id:
 | 
			
		||||
                try:
 | 
			
		||||
                    # pk values must be integer castable
 | 
			
		||||
                    val = int(val)
 | 
			
		||||
                    id_values.append(val)
 | 
			
		||||
                except ValueError:
 | 
			
		||||
                    pass
 | 
			
		||||
            
 | 
			
		||||
            queryset = queryset.exclude(pk__in=id_values)
 | 
			
		||||
 | 
			
		||||
        # Exclude part variant tree?
 | 
			
		||||
        exclude_tree = params.get('exclude_tree', None)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -14,7 +14,6 @@
 | 
			
		||||
<table class='table table-striped table-condensed'>
 | 
			
		||||
    {% include "InvenTree/settings/header.html" %}
 | 
			
		||||
    <tbody>
 | 
			
		||||
        {% include "InvenTree/settings/setting.html" with key="LOGIN_ENABLE_REG" icon="fa-info-circle" %}
 | 
			
		||||
        {% include "InvenTree/settings/setting.html" with key="LOGIN_ENABLE_SSO" icon="fa-info-circle" %}
 | 
			
		||||
        {% include "InvenTree/settings/setting.html" with key="LOGIN_ENABLE_PWD_FORGOT" icon="fa-info-circle" %}
 | 
			
		||||
        {% include "InvenTree/settings/setting.html" with key="LOGIN_MAIL_REQUIRED" icon="fa-info-circle" %}
 | 
			
		||||
@@ -22,9 +21,11 @@
 | 
			
		||||
            <td>{% trans 'Signup' %}</td>
 | 
			
		||||
            <td colspan='4'></td>
 | 
			
		||||
        </tr>
 | 
			
		||||
        {% include "InvenTree/settings/setting.html" with key="LOGIN_ENABLE_REG" icon="fa-info-circle" %}
 | 
			
		||||
        {% include "InvenTree/settings/setting.html" with key="LOGIN_SIGNUP_MAIL_TWICE" icon="fa-info-circle" %}
 | 
			
		||||
        {% include "InvenTree/settings/setting.html" with key="LOGIN_SIGNUP_PWD_TWICE" icon="fa-info-circle" %}
 | 
			
		||||
        {% include "InvenTree/settings/setting.html" with key="LOGIN_SIGNUP_SSO_AUTO" icon="fa-info-circle" %}
 | 
			
		||||
        {% include "InvenTree/settings/setting.html" with key="SIGNUP_GROUP" %}
 | 
			
		||||
    </tbody>
 | 
			
		||||
</table>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -39,12 +39,12 @@
 | 
			
		||||
</table>
 | 
			
		||||
 | 
			
		||||
<div class='panel-heading'>
 | 
			
		||||
    <h4>{% trans "E-Mail" %}</h4>
 | 
			
		||||
    <h4>{% trans "Email" %}</h4>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<div>
 | 
			
		||||
    {% if user.emailaddress_set.all %}
 | 
			
		||||
    <p>{% trans 'The following e-mail addresses are associated with your account:' %}</p>
 | 
			
		||||
    <p>{% trans 'The following email addresses are associated with your account:' %}</p>
 | 
			
		||||
 | 
			
		||||
    <form action="{% url 'account_email' %}" class="email_list" method="post">
 | 
			
		||||
    {% csrf_token %}
 | 
			
		||||
@@ -78,19 +78,19 @@
 | 
			
		||||
 | 
			
		||||
    {% else %}
 | 
			
		||||
    <p><strong>{% trans 'Warning:'%}</strong>
 | 
			
		||||
        {% trans "You currently do not have any e-mail address set up. You should really add an e-mail address so you can receive notifications, reset your password, etc." %}
 | 
			
		||||
        {% trans "You currently do not have any email address set up. You should really add an email address so you can receive notifications, reset your password, etc." %}
 | 
			
		||||
    </p>
 | 
			
		||||
 | 
			
		||||
    {% endif %}
 | 
			
		||||
 | 
			
		||||
    {% if can_add_email %}
 | 
			
		||||
        <br>
 | 
			
		||||
        <h4>{% trans "Add E-mail Address" %}</h4>
 | 
			
		||||
        <h4>{% trans "Add Email Address" %}</h4>
 | 
			
		||||
 | 
			
		||||
        <form method="post" action="{% url 'account_email' %}" class="add_email">
 | 
			
		||||
            {% csrf_token %}
 | 
			
		||||
            {{ add_email_form|crispy }}
 | 
			
		||||
            <button class="btn btn-primary" name="action_add" type="submit">{% trans "Add E-mail" %}</button>
 | 
			
		||||
            <button class="btn btn-primary" name="action_add" type="submit">{% trans "Add Email" %}</button>
 | 
			
		||||
        </form>
 | 
			
		||||
    {% endif %}
 | 
			
		||||
    <br>
 | 
			
		||||
@@ -220,7 +220,7 @@
 | 
			
		||||
 | 
			
		||||
{% block js_ready %}
 | 
			
		||||
(function() {
 | 
			
		||||
  var message = "{% trans 'Do you really want to remove the selected e-mail address?' %}";
 | 
			
		||||
  var message = "{% trans 'Do you really want to remove the selected email address?' %}";
 | 
			
		||||
  var actions = document.getElementsByName('action_remove');
 | 
			
		||||
  if (actions.length) {
 | 
			
		||||
    actions[0].addEventListener("click", function(e) {
 | 
			
		||||
 
 | 
			
		||||
@@ -3,17 +3,17 @@
 | 
			
		||||
{% load i18n %}
 | 
			
		||||
{% load account %}
 | 
			
		||||
 | 
			
		||||
{% block head_title %}{% trans "Confirm E-mail Address" %}{% endblock %}
 | 
			
		||||
{% block head_title %}{% trans "Confirm Email Address" %}{% endblock %}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
<h1>{% trans "Confirm E-mail Address" %}</h1>
 | 
			
		||||
<h1>{% trans "Confirm Email Address" %}</h1>
 | 
			
		||||
 | 
			
		||||
{% if confirmation %}
 | 
			
		||||
 | 
			
		||||
{% user_display confirmation.email_address.user as user_display %}
 | 
			
		||||
 | 
			
		||||
<p>{% blocktrans with confirmation.email_address.email as email %}Please confirm that <a href="mailto:{{ email }}">{{ email }}</a> is an e-mail address for user {{ user_display }}.{% endblocktrans %}</p>
 | 
			
		||||
<p>{% blocktrans with confirmation.email_address.email as email %}Please confirm that <a href="mailto:{{ email }}">{{ email }}</a> is an email address for user {{ user_display }}.{% endblocktrans %}</p>
 | 
			
		||||
 | 
			
		||||
<form method="post" action="{% url 'account_confirm_email' confirmation.key %}">
 | 
			
		||||
{% csrf_token %}
 | 
			
		||||
@@ -24,7 +24,7 @@
 | 
			
		||||
 | 
			
		||||
{% url 'account_email' as email_url %}
 | 
			
		||||
 | 
			
		||||
<p>{% blocktrans %}This e-mail confirmation link expired or is invalid. Please <a href="{{ email_url }}">issue a new e-mail confirmation request</a>.{% endblocktrans %}</p>
 | 
			
		||||
<p>{% blocktrans %}This email confirmation link expired or is invalid. Please <a href="{{ email_url }}">issue a new email confirmation request</a>.{% endblocktrans %}</p>
 | 
			
		||||
 | 
			
		||||
{% endif %}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -15,7 +15,7 @@
 | 
			
		||||
    {% endif %}
 | 
			
		||||
 | 
			
		||||
    {% if mail_conf and enable_pwd_forgot %}
 | 
			
		||||
    <p>{% trans "Forgotten your password? Enter your e-mail address below, and we'll send you an e-mail allowing you to reset it." %}</p>
 | 
			
		||||
    <p>{% trans "Forgotten your password? Enter your email address below, and we'll send you an email allowing you to reset it." %}</p>
 | 
			
		||||
 | 
			
		||||
    <form method="POST" action="{% url 'account_reset_password' %}" class="password_reset">
 | 
			
		||||
        {% csrf_token %}
 | 
			
		||||
 
 | 
			
		||||
@@ -157,6 +157,19 @@ function bomSubstitutesDialog(bom_item_id, substitutes, options={}) {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Extract a list of all existing "substitute" id values
 | 
			
		||||
    function getSubstituteIdValues(modal) {
 | 
			
		||||
 | 
			
		||||
        var id_values = [];
 | 
			
		||||
 | 
			
		||||
        $(modal).find('.substitute-row').each(function(el) {
 | 
			
		||||
            var part = $(this).attr('part');
 | 
			
		||||
            id_values.push(part);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return id_values;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function renderSubstituteRow(substitute) {
 | 
			
		||||
 | 
			
		||||
        var pk = substitute.pk;
 | 
			
		||||
@@ -171,7 +184,7 @@ function bomSubstitutesDialog(bom_item_id, substitutes, options={}) {
 | 
			
		||||
 | 
			
		||||
        // Render a single row
 | 
			
		||||
        var html = `
 | 
			
		||||
        <tr id='substitute-row-${pk}' class='substitute-row'>
 | 
			
		||||
        <tr id='substitute-row-${pk}' class='substitute-row' part='${substitute.part}'>
 | 
			
		||||
            <td id='part-${pk}'>
 | 
			
		||||
                <a href='/part/${part.pk}/'>
 | 
			
		||||
                    ${thumb} ${part.full_name}
 | 
			
		||||
@@ -246,6 +259,21 @@ function bomSubstitutesDialog(bom_item_id, substitutes, options={}) {
 | 
			
		||||
            },
 | 
			
		||||
            part: {
 | 
			
		||||
                required: false,
 | 
			
		||||
                adjustFilters: function(query, opts) {
 | 
			
		||||
 | 
			
		||||
                    var subs = getSubstituteIdValues(opts.modal);
 | 
			
		||||
 | 
			
		||||
                    // Also exclude the "master" part (if provided)
 | 
			
		||||
                    if (options.sub_part) {
 | 
			
		||||
                        subs.push(options.sub_part);
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    if (subs.length > 0) {
 | 
			
		||||
                        query.exclude_id = subs;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    return query;
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
        preFormContent: html,
 | 
			
		||||
@@ -801,6 +829,7 @@ function loadBomTable(table, options) {
 | 
			
		||||
                subs,
 | 
			
		||||
                {
 | 
			
		||||
                    table: table,
 | 
			
		||||
                    sub_part: row.sub_part,
 | 
			
		||||
                }
 | 
			
		||||
            );
 | 
			
		||||
        });
 | 
			
		||||
 
 | 
			
		||||
@@ -1349,7 +1349,7 @@ function initializeRelatedField(field, fields, options) {
 | 
			
		||||
                
 | 
			
		||||
                // Allow custom run-time filter augmentation
 | 
			
		||||
                if ('adjustFilters' in field) {
 | 
			
		||||
                    query = field.adjustFilters(query);
 | 
			
		||||
                    query = field.adjustFilters(query, options);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                return query;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,41 +1,39 @@
 | 
			
		||||
# Django framework
 | 
			
		||||
# Please keep this list sorted
 | 
			
		||||
Django==3.2.5                   # Django package
 | 
			
		||||
gunicorn>=20.1.0                # Gunicorn web server
 | 
			
		||||
 | 
			
		||||
pillow==8.3.2                   # Image manipulation
 | 
			
		||||
djangorestframework==3.12.4     # DRF framework
 | 
			
		||||
django-cors-headers==3.2.0      # CORS headers extension for DRF
 | 
			
		||||
django-filter==2.4.0            # Extended filtering options
 | 
			
		||||
django-mptt==0.11.0             # Modified Preorder Tree Traversal
 | 
			
		||||
django-sql-utils==0.5.0         # Advanced query annotation / aggregation
 | 
			
		||||
django-markdownx==3.0.1         # Markdown form fields
 | 
			
		||||
django-markdownify==0.8.0       # Markdown rendering
 | 
			
		||||
certifi                         # Certifi is (most likely) installed through one of the requirements above
 | 
			
		||||
coreapi==2.3.0                  # API documentation
 | 
			
		||||
pygments==2.7.4                 # Syntax highlighting
 | 
			
		||||
django-crispy-forms==1.11.2     # Form helpers
 | 
			
		||||
django-import-export==2.5.0     # Data import / export for admin interface
 | 
			
		||||
tablib[xls,xlsx,yaml]           # Support for XLS and XLSX formats
 | 
			
		||||
django-cleanup==5.1.0           # Manage deletion of old / unused uploaded files
 | 
			
		||||
flake8==3.8.3                   # PEP checking
 | 
			
		||||
pep8-naming==0.11.1             # PEP naming convention extension
 | 
			
		||||
coverage==5.3                   # Unit test coverage
 | 
			
		||||
coveralls==2.1.2                # Coveralls linking (for Travis)
 | 
			
		||||
rapidfuzz==0.7.6                # Fuzzy string matching
 | 
			
		||||
django-stdimage==5.1.1          # Advanced ImageField management
 | 
			
		||||
weasyprint==52.5                # PDF generation library (Note: in the future need to update to 53)
 | 
			
		||||
django-weasyprint==1.0.1        # django weasyprint integration
 | 
			
		||||
django-debug-toolbar==2.2       # Debug / profiling toolbar
 | 
			
		||||
cryptography==3.4.8             # Cryptography support
 | 
			
		||||
django-admin-shell==0.1.2       # Python shell for the admin interface
 | 
			
		||||
py-moneyed==0.8.0               # Specific version requirement for py-moneyed
 | 
			
		||||
django-money==1.1               # Django app for currency management
 | 
			
		||||
certifi                         # Certifi is (most likely) installed through one of the requirements above
 | 
			
		||||
django-allauth==0.45.0          # SSO for external providers via OpenID
 | 
			
		||||
django-cleanup==5.1.0           # Manage deletion of old / unused uploaded files
 | 
			
		||||
django-cors-headers==3.2.0      # CORS headers extension for DRF
 | 
			
		||||
django-crispy-forms==1.11.2     # Form helpers
 | 
			
		||||
django-debug-toolbar==2.2       # Debug / profiling toolbar
 | 
			
		||||
django-error-report==0.2.0      # Error report viewer for the admin interface
 | 
			
		||||
django-filter==2.4.0            # Extended filtering options
 | 
			
		||||
django-formtools==2.3           # Form wizard tools
 | 
			
		||||
django-import-export==2.5.0     # Data import / export for admin interface
 | 
			
		||||
django-markdownify==0.8.0       # Markdown rendering
 | 
			
		||||
django-markdownx==3.0.1         # Markdown form fields
 | 
			
		||||
django-money==1.1               # Django app for currency management
 | 
			
		||||
django-mptt==0.11.0             # Modified Preorder Tree Traversal
 | 
			
		||||
django-q==1.3.4                 # Background task scheduling
 | 
			
		||||
django-sql-utils==0.5.0         # Advanced query annotation / aggregation
 | 
			
		||||
django-stdimage==5.1.1          # Advanced ImageField management
 | 
			
		||||
django-test-migrations==1.1.0   # Unit testing for database migrations
 | 
			
		||||
django-weasyprint==1.0.1        # django weasyprint integration
 | 
			
		||||
djangorestframework==3.12.4     # DRF framework
 | 
			
		||||
flake8==3.8.3                   # PEP checking
 | 
			
		||||
gunicorn>=20.1.0                # Gunicorn web server
 | 
			
		||||
inventree                       # Install the latest version of the InvenTree API python library
 | 
			
		||||
pep8-naming==0.11.1             # PEP naming convention extension
 | 
			
		||||
pillow==8.3.2                   # Image manipulation
 | 
			
		||||
py-moneyed==0.8.0               # Specific version requirement for py-moneyed
 | 
			
		||||
pygments==2.7.4                 # Syntax highlighting
 | 
			
		||||
python-barcode[images]==0.13.1  # Barcode generator
 | 
			
		||||
qrcode[pil]==6.1                # QR code generator
 | 
			
		||||
django-q==1.3.4                 # Background task scheduling
 | 
			
		||||
django-formtools==2.3           # Form wizard tools
 | 
			
		||||
cryptography==3.4.8             # Cryptography support
 | 
			
		||||
django-allauth==0.45.0          # SSO for external providers via OpenID
 | 
			
		||||
 | 
			
		||||
inventree                       # Install the latest version of the InvenTree API python library
 | 
			
		||||
rapidfuzz==0.7.6                # Fuzzy string matching
 | 
			
		||||
tablib[xls,xlsx,yaml]           # Support for XLS and XLSX formats
 | 
			
		||||
weasyprint==52.5                # PDF generation library (Note: in the future need to update to 53)
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user