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