diff --git a/InvenTree/InvenTree/filters.py b/InvenTree/InvenTree/filters.py index cd1b769646..94e6e1765b 100644 --- a/InvenTree/InvenTree/filters.py +++ b/InvenTree/InvenTree/filters.py @@ -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: + + reverse = field.startswith('-') - if field.startswith('-'): + if reverse: field = field[1:] - reverse = True + # Are aliases defined for this field? if field in aliases: - ordering[idx] = aliases[field] + 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: - ordering[idx] = '-' + ordering[idx] + a = '-' + a + + ordering.append(a) return ordering diff --git a/InvenTree/InvenTree/forms.py b/InvenTree/InvenTree/forms.py index 8b4b87637c..cd431a5f93 100644 --- a/InvenTree/InvenTree/forms.py +++ b/InvenTree/InvenTree/forms.py @@ -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 diff --git a/InvenTree/InvenTree/models.py b/InvenTree/InvenTree/models.py index 2ca179bb40..0f8350f84f 100644 --- a/InvenTree/InvenTree/models.py +++ b/InvenTree/InvenTree/models.py @@ -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. diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index a07324ec84..2095cab533 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -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 diff --git a/InvenTree/build/admin.py b/InvenTree/build/admin.py index 86592c7a81..a5ad838660 100644 --- a/InvenTree/build/admin.py +++ b/InvenTree/build/admin.py @@ -9,6 +9,10 @@ from .models import Build, BuildItem class BuildAdmin(ImportExportModelAdmin): + exclude = [ + 'reference_int', + ] + list_display = ( 'reference', 'title', diff --git a/InvenTree/build/api.py b/InvenTree/build/api.py index dc2e81b978..cfc2cab16f 100644 --- a/InvenTree/build/api.py +++ b/InvenTree/build/api.py @@ -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', diff --git a/InvenTree/build/migrations/0031_build_reference_int.py b/InvenTree/build/migrations/0031_build_reference_int.py new file mode 100644 index 0000000000..c7fc2c16cc --- /dev/null +++ b/InvenTree/build/migrations/0031_build_reference_int.py @@ -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), + ), + ] diff --git a/InvenTree/build/migrations/0032_auto_20211014_0632.py b/InvenTree/build/migrations/0032_auto_20211014_0632.py new file mode 100644 index 0000000000..3dac2b30c6 --- /dev/null +++ b/InvenTree/build/migrations/0032_auto_20211014_0632.py @@ -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 + ) + ] diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index 994d060585..403b3a9430 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -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: diff --git a/InvenTree/build/test_build.py b/InvenTree/build/test_build.py index 31e4cf1822..f8c381f224 100644 --- a/InvenTree/build/test_build.py +++ b/InvenTree/build/test_build.py @@ -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 diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index d4f26af739..81fee4ff65 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -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: diff --git a/InvenTree/order/admin.py b/InvenTree/order/admin.py index 54e91ed844..25b0922291 100644 --- a/InvenTree/order/admin.py +++ b/InvenTree/order/admin.py @@ -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', diff --git a/InvenTree/order/api.py b/InvenTree/order/api.py index 42ed28dff0..1b294d3e3e 100644 --- a/InvenTree/order/api.py +++ b/InvenTree/order/api.py @@ -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', ] diff --git a/InvenTree/order/migrations/0051_auto_20211014_0623.py b/InvenTree/order/migrations/0051_auto_20211014_0623.py new file mode 100644 index 0000000000..20cc893dd2 --- /dev/null +++ b/InvenTree/order/migrations/0051_auto_20211014_0623.py @@ -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), + ), + ] diff --git a/InvenTree/order/migrations/0052_auto_20211014_0631.py b/InvenTree/order/migrations/0052_auto_20211014_0631.py new file mode 100644 index 0000000000..b400437d20 --- /dev/null +++ b/InvenTree/order/migrations/0052_auto_20211014_0631.py @@ -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 + ) + ] diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index 4ac8925259..0c45e3746a 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -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') diff --git a/InvenTree/order/test_migrations.py b/InvenTree/order/test_migrations.py new file mode 100644 index 0000000000..b7db1f1b70 --- /dev/null +++ b/InvenTree/order/test_migrations.py @@ -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) diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index dccc2f9ac1..4dd9841332 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -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) diff --git a/InvenTree/templates/InvenTree/settings/login.html b/InvenTree/templates/InvenTree/settings/login.html index 289f87a3c9..7ffebee91e 100644 --- a/InvenTree/templates/InvenTree/settings/login.html +++ b/InvenTree/templates/InvenTree/settings/login.html @@ -14,7 +14,6 @@ {% include "InvenTree/settings/header.html" %} - {% 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 @@ + {% 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" %}
{% trans 'Signup' %}
diff --git a/InvenTree/templates/InvenTree/settings/user.html b/InvenTree/templates/InvenTree/settings/user.html index 569b218b43..d6cbf998a7 100644 --- a/InvenTree/templates/InvenTree/settings/user.html +++ b/InvenTree/templates/InvenTree/settings/user.html @@ -39,12 +39,12 @@
-

{% trans "E-Mail" %}

+

{% trans "Email" %}

{% if user.emailaddress_set.all %} -

{% trans 'The following e-mail addresses are associated with your account:' %}

+

{% trans 'The following email addresses are associated with your account:' %}

{% csrf_token %} @@ -78,19 +78,19 @@ {% else %}

{% trans 'Warning:'%} - {% 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." %}

{% endif %} {% if can_add_email %}
-

{% trans "Add E-mail Address" %}

+

{% trans "Add Email Address" %}

{% csrf_token %} {{ add_email_form|crispy }} - +
{% endif %}
@@ -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) { diff --git a/InvenTree/templates/account/email_confirm.html b/InvenTree/templates/account/email_confirm.html index 12b041f710..1bdd051fdc 100644 --- a/InvenTree/templates/account/email_confirm.html +++ b/InvenTree/templates/account/email_confirm.html @@ -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 %} -

{% trans "Confirm E-mail Address" %}

+

{% trans "Confirm Email Address" %}

{% if confirmation %} {% user_display confirmation.email_address.user as user_display %} -

{% blocktrans with confirmation.email_address.email as email %}Please confirm that {{ email }} is an e-mail address for user {{ user_display }}.{% endblocktrans %}

+

{% blocktrans with confirmation.email_address.email as email %}Please confirm that {{ email }} is an email address for user {{ user_display }}.{% endblocktrans %}

{% csrf_token %} @@ -24,7 +24,7 @@ {% url 'account_email' as email_url %} -

{% blocktrans %}This e-mail confirmation link expired or is invalid. Please issue a new e-mail confirmation request.{% endblocktrans %}

+

{% blocktrans %}This email confirmation link expired or is invalid. Please issue a new email confirmation request.{% endblocktrans %}

{% endif %} diff --git a/InvenTree/templates/account/password_reset.html b/InvenTree/templates/account/password_reset.html index 2cfb45a716..1eeb5c6179 100644 --- a/InvenTree/templates/account/password_reset.html +++ b/InvenTree/templates/account/password_reset.html @@ -15,7 +15,7 @@ {% endif %} {% if mail_conf and enable_pwd_forgot %} -

{% trans "Forgotten your password? Enter your e-mail address below, and we'll send you an e-mail allowing you to reset it." %}

+

{% trans "Forgotten your password? Enter your email address below, and we'll send you an email allowing you to reset it." %}

{% csrf_token %} diff --git a/InvenTree/templates/js/translated/bom.js b/InvenTree/templates/js/translated/bom.js index 7cab63153b..89ae428bcd 100644 --- a/InvenTree/templates/js/translated/bom.js +++ b/InvenTree/templates/js/translated/bom.js @@ -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 = ` - + ${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, } ); }); diff --git a/InvenTree/templates/js/translated/forms.js b/InvenTree/templates/js/translated/forms.js index fecf5f1bd6..2483263219 100644 --- a/InvenTree/templates/js/translated/forms.js +++ b/InvenTree/templates/js/translated/forms.js @@ -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; diff --git a/requirements.txt b/requirements.txt index 9fa4149aba..b9f1dfd692 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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-test-migrations==1.1.0 # Unit testing for database migrations +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)