From 0cdc82a4b3b14551a35c6dbd450b77ec4259313f Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 14 Oct 2021 14:24:17 +1100 Subject: [PATCH 01/24] Annotate BuildList queryset with integer cast of the reference --- InvenTree/build/serializers.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/InvenTree/build/serializers.py b/InvenTree/build/serializers.py index 547f565905..f2bf97cf7f 100644 --- a/InvenTree/build/serializers.py +++ b/InvenTree/build/serializers.py @@ -10,7 +10,8 @@ from django.core.exceptions import ValidationError as DjangoValidationError from django.utils.translation import ugettext_lazy as _ from django.db.models import Case, When, Value -from django.db.models import BooleanField +from django.db.models import BooleanField, IntegerField +from django.db.models.functions import Cast from rest_framework import serializers from rest_framework.serializers import ValidationError @@ -71,6 +72,11 @@ class BuildSerializer(InvenTreeModelSerializer): ) ) + # Annotate with a "integer" version of the reference field, to be used for natural sorting + queryset = queryset.annotate( + integer_ref=Cast('reference', output_field=IntegerField()) + ) + return queryset def __init__(self, *args, **kwargs): From 233672d822eb170a064dcddfd203a1fd4178f7e4 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 14 Oct 2021 14:25:39 +1100 Subject: [PATCH 02/24] Add new functionality to InvenTreeOrderingFilter - Allow ordering by multiple field aliases - Simply way to implement "integer ordering" functionality --- InvenTree/InvenTree/filters.py | 43 +++++++++++++++++++++++++++++----- InvenTree/build/api.py | 7 +++++- 2 files changed, 43 insertions(+), 7 deletions(-) diff --git a/InvenTree/InvenTree/filters.py b/InvenTree/InvenTree/filters.py index cd1b769646..fbd1e53fe0 100644 --- a/InvenTree/InvenTree/filters.py +++ b/InvenTree/InvenTree/filters.py @@ -34,18 +34,49 @@ class InvenTreeOrderingFilter(OrderingFilter): Ordering fields should be mapped to separate fields """ - for idx, field in enumerate(ordering): + idx = 0 - reverse = False + ordering_initial = ordering + ordering = [] - if field.startswith('-'): + for field in ordering_initial: + + reverse = 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/build/api.py b/InvenTree/build/api.py index 7920003d8b..ea177c1570 100644 --- a/InvenTree/build/api.py +++ b/InvenTree/build/api.py @@ -17,6 +17,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 @@ -68,7 +69,7 @@ class BuildList(generics.ListCreateAPIView): filter_backends = [ DjangoFilterBackend, filters.SearchFilter, - filters.OrderingFilter, + InvenTreeOrderingFilter, ] ordering_fields = [ @@ -83,6 +84,10 @@ class BuildList(generics.ListCreateAPIView): 'responsible', ] + ordering_field_aliases = { + 'reference': ['integer_ref', 'reference'], + } + search_fields = [ 'reference', 'part__name', From e46875b0a32fea6efebad1acf4ce60b948695096 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 14 Oct 2021 14:31:25 +1100 Subject: [PATCH 03/24] Apply same fix to PurchaseOrder and SalesOrder lists --- InvenTree/InvenTree/filters.py | 2 -- InvenTree/build/serializers.py | 2 +- InvenTree/order/api.py | 12 ++++++++++-- InvenTree/order/serializers.py | 12 ++++++++++++ 4 files changed, 23 insertions(+), 5 deletions(-) diff --git a/InvenTree/InvenTree/filters.py b/InvenTree/InvenTree/filters.py index fbd1e53fe0..94e6e1765b 100644 --- a/InvenTree/InvenTree/filters.py +++ b/InvenTree/InvenTree/filters.py @@ -34,8 +34,6 @@ class InvenTreeOrderingFilter(OrderingFilter): Ordering fields should be mapped to separate fields """ - idx = 0 - ordering_initial = ordering ordering = [] diff --git a/InvenTree/build/serializers.py b/InvenTree/build/serializers.py index f2bf97cf7f..5a64bda0a0 100644 --- a/InvenTree/build/serializers.py +++ b/InvenTree/build/serializers.py @@ -72,7 +72,7 @@ class BuildSerializer(InvenTreeModelSerializer): ) ) - # Annotate with a "integer" version of the reference field, to be used for natural sorting + # Annotate with an "integer" version of the reference field, to be used for natural sorting queryset = queryset.annotate( integer_ref=Cast('reference', output_field=IntegerField()) ) diff --git a/InvenTree/order/api.py b/InvenTree/order/api.py index af30a3a5c5..a451f05ac8 100644 --- a/InvenTree/order/api.py +++ b/InvenTree/order/api.py @@ -152,9 +152,13 @@ class POList(generics.ListCreateAPIView): filter_backends = [ rest_filters.DjangoFilterBackend, filters.SearchFilter, - filters.OrderingFilter, + InvenTreeOrderingFilter, ] + ordering_field_aliases = { + 'reference': ['integer_ref', 'reference'], + } + filter_fields = [ 'supplier', ] @@ -504,9 +508,13 @@ class SOList(generics.ListCreateAPIView): filter_backends = [ rest_filters.DjangoFilterBackend, filters.SearchFilter, - filters.OrderingFilter, + InvenTreeOrderingFilter, ] + ordering_field_aliases = { + 'reference': ['integer_ref', 'reference'], + } + filter_fields = [ 'customer', ] diff --git a/InvenTree/order/serializers.py b/InvenTree/order/serializers.py index 40cd2def58..bcc1791db4 100644 --- a/InvenTree/order/serializers.py +++ b/InvenTree/order/serializers.py @@ -4,6 +4,7 @@ JSON serializers for the Order API # -*- coding: utf-8 -*- from __future__ import unicode_literals +from django.db.models.fields import IntegerField from django.utils.translation import ugettext_lazy as _ @@ -11,6 +12,7 @@ from django.core.exceptions import ValidationError as DjangoValidationError from django.db import models, transaction from django.db.models import Case, When, Value from django.db.models import BooleanField, ExpressionWrapper, F +from django.db.models.functions import Cast from rest_framework import serializers from rest_framework.serializers import ValidationError @@ -73,6 +75,11 @@ class POSerializer(InvenTreeModelSerializer): ) ) + # Annotate with an "integer" version of the reference field, to be used for natural sorting + queryset = queryset.annotate( + integer_ref=Cast('reference', output_field=IntegerField()) + ) + return queryset supplier_detail = CompanyBriefSerializer(source='supplier', many=False, read_only=True) @@ -428,6 +435,11 @@ class SalesOrderSerializer(InvenTreeModelSerializer): ) ) + # Annotate with an "integer" version of the reference field, to be used for natural sorting + queryset = queryset.annotate( + integer_ref=Cast('reference', output_field=IntegerField()) + ) + return queryset customer_detail = CompanyBriefSerializer(source='customer', many=False, read_only=True) From 0c60387626f8394d5ca88cd1e3082f33a9c2ae61 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 14 Oct 2021 16:50:56 +1100 Subject: [PATCH 04/24] Extract a list of existing substitute parts from the form --- InvenTree/templates/js/translated/bom.js | 25 +++++++++++++++++++++- InvenTree/templates/js/translated/forms.js | 2 +- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/InvenTree/templates/js/translated/bom.js b/InvenTree/templates/js/translated/bom.js index 7cab63153b..2cfae3a361 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,16 @@ function bomSubstitutesDialog(bom_item_id, substitutes, options={}) { }, part: { required: false, + adjustFilters: function(query, opts) { + + var subs = getSubstituteIdValues(opts.modal); + + if (subs.length > 0) { + query.exclude_id = subs; + } + + return query; + } }, }, preFormContent: html, 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; From 9b00ede61a11913b5e2e197a4f825216cd5778f8 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 14 Oct 2021 17:12:08 +1100 Subject: [PATCH 05/24] Add part queryset filtering to exclude particular ID values --- InvenTree/part/api.py | 21 +++++++++++++++++++++ InvenTree/templates/js/translated/bom.js | 7 +++++++ 2 files changed, 28 insertions(+) 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/js/translated/bom.js b/InvenTree/templates/js/translated/bom.js index 2cfae3a361..71cf380952 100644 --- a/InvenTree/templates/js/translated/bom.js +++ b/InvenTree/templates/js/translated/bom.js @@ -263,6 +263,12 @@ function bomSubstitutesDialog(bom_item_id, substitutes, options={}) { var subs = getSubstituteIdValues(opts.modal); + // Also exclude the "master" part (if provided) + if (options.sub_part) { + subs.push(options.sub_part); + console.log("sub_part:", options.sub_part); + } + if (subs.length > 0) { query.exclude_id = subs; } @@ -824,6 +830,7 @@ function loadBomTable(table, options) { subs, { table: table, + sub_part: row.sub_part, } ); }); From 7ce0f817aabfbddbb65c20925e9f3348542e3880 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 14 Oct 2021 17:45:43 +1100 Subject: [PATCH 06/24] Add a 'reference_int' field to the models, to be used as a secondary index --- InvenTree/InvenTree/models.py | 43 +++++++++++++++++++ InvenTree/build/api.py | 2 +- .../migrations/0031_build_reference_int.py | 18 ++++++++ InvenTree/build/models.py | 6 ++- InvenTree/build/serializers.py | 8 +--- InvenTree/order/api.py | 4 +- .../migrations/0051_auto_20211014_0623.py | 23 ++++++++++ InvenTree/order/models.py | 7 ++- InvenTree/order/serializers.py | 12 ------ 9 files changed, 97 insertions(+), 26 deletions(-) create mode 100644 InvenTree/build/migrations/0031_build_reference_int.py create mode 100644 InvenTree/order/migrations/0051_auto_20211014_0623.py 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/build/api.py b/InvenTree/build/api.py index ea177c1570..cf4d44a03e 100644 --- a/InvenTree/build/api.py +++ b/InvenTree/build/api.py @@ -85,7 +85,7 @@ class BuildList(generics.ListCreateAPIView): ] ordering_field_aliases = { - 'reference': ['integer_ref', 'reference'], + 'reference': ['reference_int', 'reference'], } search_fields = [ 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/models.py b/InvenTree/build/models.py index 449776579e..c477794e8c 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/serializers.py b/InvenTree/build/serializers.py index 5a64bda0a0..547f565905 100644 --- a/InvenTree/build/serializers.py +++ b/InvenTree/build/serializers.py @@ -10,8 +10,7 @@ from django.core.exceptions import ValidationError as DjangoValidationError from django.utils.translation import ugettext_lazy as _ from django.db.models import Case, When, Value -from django.db.models import BooleanField, IntegerField -from django.db.models.functions import Cast +from django.db.models import BooleanField from rest_framework import serializers from rest_framework.serializers import ValidationError @@ -72,11 +71,6 @@ class BuildSerializer(InvenTreeModelSerializer): ) ) - # Annotate with an "integer" version of the reference field, to be used for natural sorting - queryset = queryset.annotate( - integer_ref=Cast('reference', output_field=IntegerField()) - ) - return queryset def __init__(self, *args, **kwargs): diff --git a/InvenTree/order/api.py b/InvenTree/order/api.py index a451f05ac8..df0ec1a5de 100644 --- a/InvenTree/order/api.py +++ b/InvenTree/order/api.py @@ -156,7 +156,7 @@ class POList(generics.ListCreateAPIView): ] ordering_field_aliases = { - 'reference': ['integer_ref', 'reference'], + 'reference': ['reference_int', 'reference'], } filter_fields = [ @@ -512,7 +512,7 @@ class SOList(generics.ListCreateAPIView): ] ordering_field_aliases = { - 'reference': ['integer_ref', 'reference'], + 'reference': ['reference_int', 'reference'], } filter_fields = [ 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/models.py b/InvenTree/order/models.py index 4ac8925259..1b15e74663 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() diff --git a/InvenTree/order/serializers.py b/InvenTree/order/serializers.py index bcc1791db4..40cd2def58 100644 --- a/InvenTree/order/serializers.py +++ b/InvenTree/order/serializers.py @@ -4,7 +4,6 @@ JSON serializers for the Order API # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db.models.fields import IntegerField from django.utils.translation import ugettext_lazy as _ @@ -12,7 +11,6 @@ from django.core.exceptions import ValidationError as DjangoValidationError from django.db import models, transaction from django.db.models import Case, When, Value from django.db.models import BooleanField, ExpressionWrapper, F -from django.db.models.functions import Cast from rest_framework import serializers from rest_framework.serializers import ValidationError @@ -75,11 +73,6 @@ class POSerializer(InvenTreeModelSerializer): ) ) - # Annotate with an "integer" version of the reference field, to be used for natural sorting - queryset = queryset.annotate( - integer_ref=Cast('reference', output_field=IntegerField()) - ) - return queryset supplier_detail = CompanyBriefSerializer(source='supplier', many=False, read_only=True) @@ -435,11 +428,6 @@ class SalesOrderSerializer(InvenTreeModelSerializer): ) ) - # Annotate with an "integer" version of the reference field, to be used for natural sorting - queryset = queryset.annotate( - integer_ref=Cast('reference', output_field=IntegerField()) - ) - return queryset customer_detail = CompanyBriefSerializer(source='customer', many=False, read_only=True) From 5c6a7b489cdcb2fa2ea7e759e24db40b3e640c17 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 14 Oct 2021 17:54:46 +1100 Subject: [PATCH 07/24] Data migration for the Build model --- .../migrations/0032_auto_20211014_0632.py | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 InvenTree/build/migrations/0032_auto_20211014_0632.py 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..6c84c24526 --- /dev/null +++ b/InvenTree/build/migrations/0032_auto_20211014_0632.py @@ -0,0 +1,57 @@ +# 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 + """ + + print("\n - Rebuilding reference field for BuildOrder model...") + + BuildOrder = apps.get_model('build', 'build') + + n = BuildOrder.objects.count() + + 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() + + print(f" - Updated {n} BuildOrder objects") + print(f" - COMPLETE! -") + +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 + ) + ] From 068b54f666dd71a7a68fce7500186b06ea5cd3db Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 14 Oct 2021 17:58:09 +1100 Subject: [PATCH 08/24] Data migration for PurchaseOrder and SalesOrder models --- .../migrations/0052_auto_20211014_0631.py | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 InvenTree/order/migrations/0052_auto_20211014_0631.py 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..93d3f86038 --- /dev/null +++ b/InvenTree/order/migrations/0052_auto_20211014_0631.py @@ -0,0 +1,80 @@ +# 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 + """ + + print("\n - Rebuilding reference field for PurchaseOrder model...") + + PurchaseOrder = apps.get_model('order', 'purchaseorder') + + n = PurchaseOrder.objects.count() + + 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() + + print(f" - Updated {n} PurchaseOrder objects") + + print("\n - Rebuilding reference field for SalesOrder model...") + + SalesOrder = apps.get_model('order', 'salesorder') + + n = SalesOrder.objects.count() + + 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() + + print(f" - Updated {n} SalesOrder objects") + + print(f" - COMPLETE! -") + + +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 + ) + ] From d3d1d2f577be5ce5a6382d034d841cce863f7880 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 14 Oct 2021 18:00:17 +1100 Subject: [PATCH 09/24] Auto-rebuild the reference field for the SalesOrder on save --- InvenTree/order/migrations/0052_auto_20211014_0631.py | 2 -- InvenTree/order/models.py | 6 ++++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/InvenTree/order/migrations/0052_auto_20211014_0631.py b/InvenTree/order/migrations/0052_auto_20211014_0631.py index 93d3f86038..91ae28aadc 100644 --- a/InvenTree/order/migrations/0052_auto_20211014_0631.py +++ b/InvenTree/order/migrations/0052_auto_20211014_0631.py @@ -31,7 +31,6 @@ def build_refs(apps, schema_editor): order.save() print(f" - Updated {n} PurchaseOrder objects") - print("\n - Rebuilding reference field for SalesOrder model...") SalesOrder = apps.get_model('order', 'salesorder') @@ -54,7 +53,6 @@ def build_refs(apps, schema_editor): order.save() print(f" - Updated {n} SalesOrder objects") - print(f" - COMPLETE! -") diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index 1b15e74663..0c45e3746a 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -534,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') From 4327cbedceeb80689d293040ae41dfcbbb9ecfe1 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 14 Oct 2021 18:01:16 +1100 Subject: [PATCH 10/24] Remove debug message --- InvenTree/templates/js/translated/bom.js | 1 - 1 file changed, 1 deletion(-) diff --git a/InvenTree/templates/js/translated/bom.js b/InvenTree/templates/js/translated/bom.js index 71cf380952..89ae428bcd 100644 --- a/InvenTree/templates/js/translated/bom.js +++ b/InvenTree/templates/js/translated/bom.js @@ -266,7 +266,6 @@ function bomSubstitutesDialog(bom_item_id, substitutes, options={}) { // Also exclude the "master" part (if provided) if (options.sub_part) { subs.push(options.sub_part); - console.log("sub_part:", options.sub_part); } if (subs.length > 0) { From d0f60766e0ad6a1b1804605f1cdf954497a54aa8 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 14 Oct 2021 18:57:02 +1100 Subject: [PATCH 11/24] exclude new field from admin view --- InvenTree/build/admin.py | 4 ++++ InvenTree/order/admin.py | 8 ++++++++ 2 files changed, 12 insertions(+) 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/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', From 2c9bbb051ad9db880d0485a4653f4a9e74d6c95e Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 14 Oct 2021 19:12:23 +1100 Subject: [PATCH 12/24] Add some unit tests - Saving a model automatically updates the reference_int field - Data migrations are correctly applied --- .../migrations/0032_auto_20211014_0632.py | 7 --- InvenTree/build/test_build.py | 20 +++++++ .../migrations/0052_auto_20211014_0631.py | 12 ---- InvenTree/order/test_migrations.py | 59 +++++++++++++++++++ 4 files changed, 79 insertions(+), 19 deletions(-) create mode 100644 InvenTree/order/test_migrations.py diff --git a/InvenTree/build/migrations/0032_auto_20211014_0632.py b/InvenTree/build/migrations/0032_auto_20211014_0632.py index 6c84c24526..3dac2b30c6 100644 --- a/InvenTree/build/migrations/0032_auto_20211014_0632.py +++ b/InvenTree/build/migrations/0032_auto_20211014_0632.py @@ -10,12 +10,8 @@ def build_refs(apps, schema_editor): Rebuild the integer "reference fields" for existing Build objects """ - print("\n - Rebuilding reference field for BuildOrder model...") - BuildOrder = apps.get_model('build', 'build') - n = BuildOrder.objects.count() - for build in BuildOrder.objects.all(): ref = 0 @@ -31,9 +27,6 @@ def build_refs(apps, schema_editor): build.reference_int = ref build.save() - print(f" - Updated {n} BuildOrder objects") - print(f" - COMPLETE! -") - def unbuild_refs(apps, schema_editor): """ Provided only for reverse migration compatibility diff --git a/InvenTree/build/test_build.py b/InvenTree/build/test_build.py index a0874d0979..df6253362e 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/order/migrations/0052_auto_20211014_0631.py b/InvenTree/order/migrations/0052_auto_20211014_0631.py index 91ae28aadc..b400437d20 100644 --- a/InvenTree/order/migrations/0052_auto_20211014_0631.py +++ b/InvenTree/order/migrations/0052_auto_20211014_0631.py @@ -9,12 +9,8 @@ def build_refs(apps, schema_editor): Rebuild the integer "reference fields" for existing Build objects """ - print("\n - Rebuilding reference field for PurchaseOrder model...") - PurchaseOrder = apps.get_model('order', 'purchaseorder') - n = PurchaseOrder.objects.count() - for order in PurchaseOrder.objects.all(): ref = 0 @@ -30,13 +26,8 @@ def build_refs(apps, schema_editor): order.reference_int = ref order.save() - print(f" - Updated {n} PurchaseOrder objects") - print("\n - Rebuilding reference field for SalesOrder model...") - SalesOrder = apps.get_model('order', 'salesorder') - n = SalesOrder.objects.count() - for order in SalesOrder.objects.all(): ref = 0 @@ -52,9 +43,6 @@ def build_refs(apps, schema_editor): order.reference_int = ref order.save() - print(f" - Updated {n} SalesOrder objects") - print(f" - COMPLETE! -") - def unbuild_refs(apps, schema_editor): """ 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) From 3435254d2aa152a19b6078803fb3ec442daf4112 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 14 Oct 2021 16:34:59 +0200 Subject: [PATCH 13/24] fix email config check --- InvenTree/InvenTree/forms.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/InvenTree/InvenTree/forms.py b/InvenTree/InvenTree/forms.py index 8b4b87637c..c5cd7fad58 100644 --- a/InvenTree/InvenTree/forms.py +++ b/InvenTree/InvenTree/forms.py @@ -8,6 +8,7 @@ from __future__ import unicode_literals from django.utils.translation import ugettext_lazy as _ from django import forms from django.contrib.auth.models import User +from django.conf import settings from crispy_forms.helper import FormHelper from crispy_forms.layout import Layout, Field @@ -257,7 +258,7 @@ 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): + if settings.EMAIL_HOST and InvenTreeSetting.get_setting('LOGIN_ENABLE_REG', True): return super().is_open_for_signup(request) return False @@ -268,7 +269,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 From b26bf780c32baf2f9e08dc6cb60a1fa4036ea424 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 14 Oct 2021 21:27:09 +0200 Subject: [PATCH 14/24] setting to register group on signup --- InvenTree/InvenTree/forms.py | 18 ++++++++++++++++-- InvenTree/common/models.py | 11 ++++++++++- .../templates/InvenTree/settings/login.html | 1 + 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/InvenTree/InvenTree/forms.py b/InvenTree/InvenTree/forms.py index 8b4b87637c..eede6b8d32 100644 --- a/InvenTree/InvenTree/forms.py +++ b/InvenTree/InvenTree/forms.py @@ -4,10 +4,11 @@ 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 crispy_forms.helper import FormHelper from crispy_forms.layout import Layout, Field @@ -20,6 +21,7 @@ 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. """ @@ -261,6 +263,18 @@ class RegistratonMixin: return super().is_open_for_signup(request) 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 +282,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/common/models.py b/InvenTree/common/models.py index d4f26af739..a1b48d74d3 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 @@ -845,6 +845,15 @@ class InvenTreeSetting(BaseInvenTreeSetting): 'default': True, 'validator': bool, }, + 'SIGNUP_GROUP': { + 'name': _('Group on signup'), + 'description': _('Group new user are asigned on registration'), + 'default': '', + 'choices': [ + ('', _('No group')), + *[(str(a.id), str(a)) for a in Group.objects.all()] + ], + }, } class Meta: diff --git a/InvenTree/templates/InvenTree/settings/login.html b/InvenTree/templates/InvenTree/settings/login.html index 289f87a3c9..57e0adb45e 100644 --- a/InvenTree/templates/InvenTree/settings/login.html +++ b/InvenTree/templates/InvenTree/settings/login.html @@ -25,6 +25,7 @@ {% 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" %} From 3a586af55645e1639b15d2a1cf8d79dd56040b50 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 14 Oct 2021 21:27:41 +0200 Subject: [PATCH 15/24] move setting to better fit grouping --- InvenTree/templates/InvenTree/settings/login.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/templates/InvenTree/settings/login.html b/InvenTree/templates/InvenTree/settings/login.html index 57e0adb45e..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,6 +21,7 @@ + {% 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" %} From f95896e8ea4086d285a6d4401bab100415432d1d Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 14 Oct 2021 21:33:35 +0200 Subject: [PATCH 16/24] this was not meant to be submitted --- InvenTree/InvenTree/forms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/InvenTree/forms.py b/InvenTree/InvenTree/forms.py index eede6b8d32..dd0af9b6c3 100644 --- a/InvenTree/InvenTree/forms.py +++ b/InvenTree/InvenTree/forms.py @@ -282,7 +282,7 @@ class CustomAccountAdapter(RegistratonMixin, DefaultAccountAdapter): """ def send_mail(self, template_prefix, email, context): """only send mail if backend configured""" - if settings.EMAIL_HOST: + if InvenTreeSetting.get_setting('EMAIL_HOST', None): return super().send_mail(template_prefix, email, context) return False From 27aec4246e80115061cab9e1481c24ec9e41d99f Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 14 Oct 2021 21:33:54 +0200 Subject: [PATCH 17/24] PEP fix --- InvenTree/InvenTree/forms.py | 1 + 1 file changed, 1 insertion(+) diff --git a/InvenTree/InvenTree/forms.py b/InvenTree/InvenTree/forms.py index dd0af9b6c3..5903fea6e0 100644 --- a/InvenTree/InvenTree/forms.py +++ b/InvenTree/InvenTree/forms.py @@ -23,6 +23,7 @@ from common.models import InvenTreeSetting logger = logging.getLogger('inventree') + class HelperForm(forms.ModelForm): """ Provides simple integration of crispy_forms extension. """ From e0887cf55f3a902a5126420c5b33bcfa60fdf635 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 14 Oct 2021 22:16:07 +0200 Subject: [PATCH 18/24] move goup forming into own function --- InvenTree/common/models.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index a1b48d74d3..0a5e87e3fa 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -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 @@ -478,6 +475,8 @@ class BaseInvenTreeSetting(models.Model): return value +def group_options(): + return [('', _('No group')), *[(str(a.id), str(a)) for a in Group.objects.all()]] class InvenTreeSetting(BaseInvenTreeSetting): """ @@ -849,10 +848,7 @@ class InvenTreeSetting(BaseInvenTreeSetting): 'name': _('Group on signup'), 'description': _('Group new user are asigned on registration'), 'default': '', - 'choices': [ - ('', _('No group')), - *[(str(a.id), str(a)) for a in Group.objects.all()] - ], + 'choices': group_options }, } From 0657b71fe8fdeca1992a4f950d310219e1844d2b Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 14 Oct 2021 22:19:52 +0200 Subject: [PATCH 19/24] clearer name and PEP foxes --- InvenTree/common/models.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index 0a5e87e3fa..3302d06eb1 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -475,9 +475,12 @@ class BaseInvenTreeSetting(models.Model): return value -def group_options(): + +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 @@ -848,7 +851,7 @@ class InvenTreeSetting(BaseInvenTreeSetting): 'name': _('Group on signup'), 'description': _('Group new user are asigned on registration'), 'default': '', - 'choices': group_options + 'choices': settings_group_options }, } From 0997ba2eb4fdf3a3ee71148a4ac55525d0272718 Mon Sep 17 00:00:00 2001 From: Nigel Date: Thu, 14 Oct 2021 14:35:41 -0600 Subject: [PATCH 20/24] Sort requirements.txt --- requirements.txt | 64 +++++++++++++++++++++++------------------------- 1 file changed, 31 insertions(+), 33 deletions(-) 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) From 8e4deaa8c83fdca7a9676e5a7ef178a17818c611 Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 15 Oct 2021 11:52:13 +1100 Subject: [PATCH 21/24] Standardize spelling of email / e-mail (Spoiler: I chose "email") --- InvenTree/InvenTree/forms.py | 4 ++-- InvenTree/common/models.py | 2 +- InvenTree/templates/InvenTree/settings/user.html | 12 ++++++------ InvenTree/templates/account/email_confirm.html | 8 ++++---- InvenTree/templates/account/password_reset.html | 2 +- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/InvenTree/InvenTree/forms.py b/InvenTree/InvenTree/forms.py index c5cd7fad58..084de06472 100644 --- a/InvenTree/InvenTree/forms.py +++ b/InvenTree/InvenTree/forms.py @@ -224,11 +224,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"), } ), ) diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index d4f26af739..fbe143de5a 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -822,7 +822,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, 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 'Signup' %}
-

{% 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:' %}

{% 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 %} From 63434454339988e23dbdd3557c8ae07cf6e31180 Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 15 Oct 2021 15:05:55 +1100 Subject: [PATCH 22/24] Add support for backend-specific database functionality --- InvenTree/InvenTree/settings.py | 100 +++++++++++++++++++++----------- 1 file changed, 66 insertions(+), 34 deletions(-) diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index a07324ec84..9b38c27159 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,41 @@ 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 + +Various database options can be specified in config.yaml if required: + +""" + +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) + +# 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 +684,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 From 603f7d5f45ce694f5fa2849643a2138a1c5068f6 Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 15 Oct 2021 15:13:06 +1100 Subject: [PATCH 23/24] Fixes --- InvenTree/InvenTree/settings.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index 9b38c27159..2095cab533 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -454,11 +454,9 @@ logger.info(f"DB_HOST: {db_host}") """ 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 +""" -Various database options can be specified in config.yaml if required: - -""" - +# 'OPTIONS' or 'options' can be specified in config.yaml db_options = db_config.get('OPTIONS', db_config.get('options', {})) # Specific options for postgres backend @@ -467,7 +465,15 @@ if 'postgres' in db_engine: # Connection timeout if 'connect_timeout' not in db_options: - db_options['connect_timeout'] = int(os.getenv('INVENTREE_DB_TIMEOUT'), 2) + 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: From 103a4af9d48346239af786ff515a968ffb72dc6e Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 15 Oct 2021 23:18:03 +0200 Subject: [PATCH 24/24] fix signup with providers with extra args --- InvenTree/InvenTree/forms.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/InvenTree/InvenTree/forms.py b/InvenTree/InvenTree/forms.py index 279635756a..cd431a5f93 100644 --- a/InvenTree/InvenTree/forms.py +++ b/InvenTree/InvenTree/forms.py @@ -260,9 +260,9 @@ class RegistratonMixin: """ Mixin to check if registration should be enabled """ - def is_open_for_signup(self, 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) + return super().is_open_for_signup(request, *args, **kwargs) return False def save_user(self, request, user, form, commit=True):