From 0cdc82a4b3b14551a35c6dbd450b77ec4259313f Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 14 Oct 2021 14:24:17 +1100 Subject: [PATCH 1/9] 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 2/9] 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 3/9] 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 7ce0f817aabfbddbb65c20925e9f3348542e3880 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 14 Oct 2021 17:45:43 +1100 Subject: [PATCH 4/9] 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 5/9] 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 6/9] 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 7/9] 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 d0f60766e0ad6a1b1804605f1cdf954497a54aa8 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 14 Oct 2021 18:57:02 +1100 Subject: [PATCH 8/9] 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 9/9] 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)