mirror of
https://github.com/inventree/InvenTree.git
synced 2025-04-29 12:06:44 +00:00
Merge branch 'master' into build-output-complete
This commit is contained in:
commit
ccaa7d2683
@ -34,18 +34,47 @@ class InvenTreeOrderingFilter(OrderingFilter):
|
|||||||
Ordering fields should be mapped to separate fields
|
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:]
|
field = field[1:]
|
||||||
reverse = True
|
|
||||||
|
|
||||||
|
# Are aliases defined for this field?
|
||||||
if field in aliases:
|
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:
|
if reverse:
|
||||||
ordering[idx] = '-' + ordering[idx]
|
a = '-' + a
|
||||||
|
|
||||||
|
ordering.append(a)
|
||||||
|
|
||||||
return ordering
|
return ordering
|
||||||
|
@ -4,10 +4,12 @@ Helper forms which subclass Django forms to provide additional functionality
|
|||||||
|
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
import logging
|
||||||
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django import forms
|
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.helper import FormHelper
|
||||||
from crispy_forms.layout import Layout, Field
|
from crispy_forms.layout import Layout, Field
|
||||||
@ -20,6 +22,8 @@ from allauth.socialaccount.adapter import DefaultSocialAccountAdapter
|
|||||||
from part.models import PartCategory
|
from part.models import PartCategory
|
||||||
from common.models import InvenTreeSetting
|
from common.models import InvenTreeSetting
|
||||||
|
|
||||||
|
logger = logging.getLogger('inventree')
|
||||||
|
|
||||||
|
|
||||||
class HelperForm(forms.ModelForm):
|
class HelperForm(forms.ModelForm):
|
||||||
""" Provides simple integration of crispy_forms extension. """
|
""" Provides simple integration of crispy_forms extension. """
|
||||||
@ -223,11 +227,11 @@ class CustomSignupForm(SignupForm):
|
|||||||
# check for two mail fields
|
# check for two mail fields
|
||||||
if InvenTreeSetting.get_setting('LOGIN_SIGNUP_MAIL_TWICE'):
|
if InvenTreeSetting.get_setting('LOGIN_SIGNUP_MAIL_TWICE'):
|
||||||
self.fields["email2"] = forms.EmailField(
|
self.fields["email2"] = forms.EmailField(
|
||||||
label=_("E-mail (again)"),
|
label=_("Email (again)"),
|
||||||
widget=forms.TextInput(
|
widget=forms.TextInput(
|
||||||
attrs={
|
attrs={
|
||||||
"type": "email",
|
"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
|
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 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 super().is_open_for_signup(request, *args, **kwargs)
|
||||||
return False
|
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):
|
class CustomAccountAdapter(RegistratonMixin, DefaultAccountAdapter):
|
||||||
"""
|
"""
|
||||||
@ -268,7 +284,7 @@ class CustomAccountAdapter(RegistratonMixin, DefaultAccountAdapter):
|
|||||||
"""
|
"""
|
||||||
def send_mail(self, template_prefix, email, context):
|
def send_mail(self, template_prefix, email, context):
|
||||||
"""only send mail if backend configured"""
|
"""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 super().send_mail(template_prefix, email, context)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ Generic models which provide extra functionality over base Django model types.
|
|||||||
|
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import re
|
||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
@ -43,6 +44,48 @@ def rename_attachment(instance, filename):
|
|||||||
return os.path.join(instance.getSubdir(), 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):
|
class InvenTreeAttachment(models.Model):
|
||||||
""" Provides an abstracted class for managing file attachments.
|
""" Provides an abstracted class for managing file attachments.
|
||||||
|
|
||||||
|
@ -385,39 +385,6 @@ Q_CLUSTER = {
|
|||||||
'sync': False,
|
'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.
|
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_NAME: {db_name}")
|
||||||
logger.info(f"DB_HOST: {db_host}")
|
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 = {
|
CACHES = {
|
||||||
'default': {
|
'default': {
|
||||||
@ -683,3 +690,34 @@ ACCOUNT_FORMS = {
|
|||||||
|
|
||||||
SOCIALACCOUNT_ADAPTER = 'InvenTree.forms.CustomSocialAccountAdapter'
|
SOCIALACCOUNT_ADAPTER = 'InvenTree.forms.CustomSocialAccountAdapter'
|
||||||
ACCOUNT_ADAPTER = 'InvenTree.forms.CustomAccountAdapter'
|
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):
|
class BuildAdmin(ImportExportModelAdmin):
|
||||||
|
|
||||||
|
exclude = [
|
||||||
|
'reference_int',
|
||||||
|
]
|
||||||
|
|
||||||
list_display = (
|
list_display = (
|
||||||
'reference',
|
'reference',
|
||||||
'title',
|
'title',
|
||||||
|
@ -15,6 +15,7 @@ from django_filters import rest_framework as rest_filters
|
|||||||
|
|
||||||
from InvenTree.api import AttachmentMixin
|
from InvenTree.api import AttachmentMixin
|
||||||
from InvenTree.helpers import str2bool, isNull
|
from InvenTree.helpers import str2bool, isNull
|
||||||
|
from InvenTree.filters import InvenTreeOrderingFilter
|
||||||
from InvenTree.status_codes import BuildStatus
|
from InvenTree.status_codes import BuildStatus
|
||||||
|
|
||||||
from .models import Build, BuildItem, BuildOrderAttachment
|
from .models import Build, BuildItem, BuildOrderAttachment
|
||||||
@ -66,7 +67,7 @@ class BuildList(generics.ListCreateAPIView):
|
|||||||
filter_backends = [
|
filter_backends = [
|
||||||
DjangoFilterBackend,
|
DjangoFilterBackend,
|
||||||
filters.SearchFilter,
|
filters.SearchFilter,
|
||||||
filters.OrderingFilter,
|
InvenTreeOrderingFilter,
|
||||||
]
|
]
|
||||||
|
|
||||||
ordering_fields = [
|
ordering_fields = [
|
||||||
@ -81,6 +82,10 @@ class BuildList(generics.ListCreateAPIView):
|
|||||||
'responsible',
|
'responsible',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
ordering_field_aliases = {
|
||||||
|
'reference': ['reference_int', 'reference'],
|
||||||
|
}
|
||||||
|
|
||||||
search_fields = [
|
search_fields = [
|
||||||
'reference',
|
'reference',
|
||||||
'part__name',
|
'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.status_codes import BuildStatus, StockStatus, StockHistoryCode
|
||||||
from InvenTree.helpers import increment, getSetting, normalize, MakeBarcode
|
from InvenTree.helpers import increment, getSetting, normalize, MakeBarcode
|
||||||
from InvenTree.validators import validate_build_order_reference
|
from InvenTree.validators import validate_build_order_reference
|
||||||
from InvenTree.models import InvenTreeAttachment
|
from InvenTree.models import InvenTreeAttachment, ReferenceIndexingMixin
|
||||||
|
|
||||||
import common.models
|
import common.models
|
||||||
|
|
||||||
@ -69,7 +69,7 @@ def get_next_build_number():
|
|||||||
return reference
|
return reference
|
||||||
|
|
||||||
|
|
||||||
class Build(MPTTModel):
|
class Build(MPTTModel, ReferenceIndexingMixin):
|
||||||
""" A Build object organises the creation of new StockItem objects from other existing StockItem objects.
|
""" A Build object organises the creation of new StockItem objects from other existing StockItem objects.
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
@ -108,6 +108,8 @@ class Build(MPTTModel):
|
|||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
|
|
||||||
|
self.rebuild_reference_field()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
except InvalidMove:
|
except InvalidMove:
|
||||||
|
@ -118,6 +118,26 @@ class BuildTest(TestCase):
|
|||||||
|
|
||||||
self.stock_3_1 = StockItem.objects.create(part=self.sub_part_3, quantity=1000)
|
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):
|
def test_init(self):
|
||||||
# Perform some basic tests before we start the ball rolling
|
# Perform some basic tests before we start the ball rolling
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ import decimal
|
|||||||
import math
|
import math
|
||||||
|
|
||||||
from django.db import models, transaction
|
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.db.utils import IntegrityError, OperationalError
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
@ -182,12 +182,9 @@ class BaseInvenTreeSetting(models.Model):
|
|||||||
else:
|
else:
|
||||||
choices = None
|
choices = None
|
||||||
|
|
||||||
"""
|
if callable(choices):
|
||||||
TODO:
|
|
||||||
if type(choices) is function:
|
|
||||||
# Evaluate the function (we expect it will return a list of tuples...)
|
# Evaluate the function (we expect it will return a list of tuples...)
|
||||||
return choices()
|
return choices()
|
||||||
"""
|
|
||||||
|
|
||||||
return choices
|
return choices
|
||||||
|
|
||||||
@ -479,6 +476,11 @@ class BaseInvenTreeSetting(models.Model):
|
|||||||
return value
|
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):
|
class InvenTreeSetting(BaseInvenTreeSetting):
|
||||||
"""
|
"""
|
||||||
An InvenTreeSetting object is a key:value pair used for storing
|
An InvenTreeSetting object is a key:value pair used for storing
|
||||||
@ -822,7 +824,7 @@ class InvenTreeSetting(BaseInvenTreeSetting):
|
|||||||
'validator': bool,
|
'validator': bool,
|
||||||
},
|
},
|
||||||
'LOGIN_MAIL_REQUIRED': {
|
'LOGIN_MAIL_REQUIRED': {
|
||||||
'name': _('E-Mail required'),
|
'name': _('Email required'),
|
||||||
'description': _('Require user to supply mail on signup'),
|
'description': _('Require user to supply mail on signup'),
|
||||||
'default': False,
|
'default': False,
|
||||||
'validator': bool,
|
'validator': bool,
|
||||||
@ -845,6 +847,12 @@ class InvenTreeSetting(BaseInvenTreeSetting):
|
|||||||
'default': True,
|
'default': True,
|
||||||
'validator': bool,
|
'validator': bool,
|
||||||
},
|
},
|
||||||
|
'SIGNUP_GROUP': {
|
||||||
|
'name': _('Group on signup'),
|
||||||
|
'description': _('Group new user are asigned on registration'),
|
||||||
|
'default': '',
|
||||||
|
'choices': settings_group_options
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -20,6 +20,10 @@ class PurchaseOrderLineItemInlineAdmin(admin.StackedInline):
|
|||||||
|
|
||||||
class PurchaseOrderAdmin(ImportExportModelAdmin):
|
class PurchaseOrderAdmin(ImportExportModelAdmin):
|
||||||
|
|
||||||
|
exclude = [
|
||||||
|
'reference_int',
|
||||||
|
]
|
||||||
|
|
||||||
list_display = (
|
list_display = (
|
||||||
'reference',
|
'reference',
|
||||||
'supplier',
|
'supplier',
|
||||||
@ -41,6 +45,10 @@ class PurchaseOrderAdmin(ImportExportModelAdmin):
|
|||||||
|
|
||||||
class SalesOrderAdmin(ImportExportModelAdmin):
|
class SalesOrderAdmin(ImportExportModelAdmin):
|
||||||
|
|
||||||
|
exclude = [
|
||||||
|
'reference_int',
|
||||||
|
]
|
||||||
|
|
||||||
list_display = (
|
list_display = (
|
||||||
'reference',
|
'reference',
|
||||||
'customer',
|
'customer',
|
||||||
|
@ -151,9 +151,13 @@ class POList(generics.ListCreateAPIView):
|
|||||||
filter_backends = [
|
filter_backends = [
|
||||||
rest_filters.DjangoFilterBackend,
|
rest_filters.DjangoFilterBackend,
|
||||||
filters.SearchFilter,
|
filters.SearchFilter,
|
||||||
filters.OrderingFilter,
|
InvenTreeOrderingFilter,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
ordering_field_aliases = {
|
||||||
|
'reference': ['reference_int', 'reference'],
|
||||||
|
}
|
||||||
|
|
||||||
filter_fields = [
|
filter_fields = [
|
||||||
'supplier',
|
'supplier',
|
||||||
]
|
]
|
||||||
@ -489,9 +493,13 @@ class SOList(generics.ListCreateAPIView):
|
|||||||
filter_backends = [
|
filter_backends = [
|
||||||
rest_filters.DjangoFilterBackend,
|
rest_filters.DjangoFilterBackend,
|
||||||
filters.SearchFilter,
|
filters.SearchFilter,
|
||||||
filters.OrderingFilter,
|
InvenTreeOrderingFilter,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
ordering_field_aliases = {
|
||||||
|
'reference': ['reference_int', 'reference'],
|
||||||
|
}
|
||||||
|
|
||||||
filter_fields = [
|
filter_fields = [
|
||||||
'customer',
|
'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.fields import InvenTreeModelMoneyField, RoundingDecimalField
|
||||||
from InvenTree.helpers import decimal2string, increment, getSetting
|
from InvenTree.helpers import decimal2string, increment, getSetting
|
||||||
from InvenTree.status_codes import PurchaseOrderStatus, SalesOrderStatus, StockStatus, StockHistoryCode
|
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():
|
def get_next_po_number():
|
||||||
@ -89,7 +89,7 @@ def get_next_so_number():
|
|||||||
return reference
|
return reference
|
||||||
|
|
||||||
|
|
||||||
class Order(models.Model):
|
class Order(ReferenceIndexingMixin):
|
||||||
""" Abstract model for an order.
|
""" Abstract model for an order.
|
||||||
|
|
||||||
Instances of this class:
|
Instances of this class:
|
||||||
@ -147,6 +147,9 @@ class Order(models.Model):
|
|||||||
return new_ref
|
return new_ref
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
|
|
||||||
|
self.rebuild_reference_field()
|
||||||
|
|
||||||
if not self.creation_date:
|
if not self.creation_date:
|
||||||
self.creation_date = datetime.now().date()
|
self.creation_date = datetime.now().date()
|
||||||
|
|
||||||
@ -531,6 +534,12 @@ class SalesOrder(Order):
|
|||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
|
||||||
|
self.rebuild_reference_field()
|
||||||
|
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
|
||||||
prefix = getSetting('SALESORDER_REFERENCE_PREFIX')
|
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):
|
except (ValueError, Part.DoesNotExist):
|
||||||
pass
|
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 part variant tree?
|
||||||
exclude_tree = params.get('exclude_tree', None)
|
exclude_tree = params.get('exclude_tree', None)
|
||||||
|
|
||||||
|
@ -14,7 +14,6 @@
|
|||||||
<table class='table table-striped table-condensed'>
|
<table class='table table-striped table-condensed'>
|
||||||
{% include "InvenTree/settings/header.html" %}
|
{% include "InvenTree/settings/header.html" %}
|
||||||
<tbody>
|
<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_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_ENABLE_PWD_FORGOT" icon="fa-info-circle" %}
|
||||||
{% include "InvenTree/settings/setting.html" with key="LOGIN_MAIL_REQUIRED" 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>{% trans 'Signup' %}</td>
|
||||||
<td colspan='4'></td>
|
<td colspan='4'></td>
|
||||||
</tr>
|
</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_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_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="LOGIN_SIGNUP_SSO_AUTO" icon="fa-info-circle" %}
|
||||||
|
{% include "InvenTree/settings/setting.html" with key="SIGNUP_GROUP" %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
@ -39,12 +39,12 @@
|
|||||||
</table>
|
</table>
|
||||||
|
|
||||||
<div class='panel-heading'>
|
<div class='panel-heading'>
|
||||||
<h4>{% trans "E-Mail" %}</h4>
|
<h4>{% trans "Email" %}</h4>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
{% if user.emailaddress_set.all %}
|
{% 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">
|
<form action="{% url 'account_email' %}" class="email_list" method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
@ -78,19 +78,19 @@
|
|||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<p><strong>{% trans 'Warning:'%}</strong>
|
<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>
|
</p>
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if can_add_email %}
|
{% if can_add_email %}
|
||||||
<br>
|
<br>
|
||||||
<h4>{% trans "Add E-mail Address" %}</h4>
|
<h4>{% trans "Add Email Address" %}</h4>
|
||||||
|
|
||||||
<form method="post" action="{% url 'account_email' %}" class="add_email">
|
<form method="post" action="{% url 'account_email' %}" class="add_email">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{{ add_email_form|crispy }}
|
{{ 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>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<br>
|
<br>
|
||||||
@ -220,7 +220,7 @@
|
|||||||
|
|
||||||
{% block js_ready %}
|
{% block js_ready %}
|
||||||
(function() {
|
(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');
|
var actions = document.getElementsByName('action_remove');
|
||||||
if (actions.length) {
|
if (actions.length) {
|
||||||
actions[0].addEventListener("click", function(e) {
|
actions[0].addEventListener("click", function(e) {
|
||||||
|
@ -3,17 +3,17 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load account %}
|
{% load account %}
|
||||||
|
|
||||||
{% block head_title %}{% trans "Confirm E-mail Address" %}{% endblock %}
|
{% block head_title %}{% trans "Confirm Email Address" %}{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>{% trans "Confirm E-mail Address" %}</h1>
|
<h1>{% trans "Confirm Email Address" %}</h1>
|
||||||
|
|
||||||
{% if confirmation %}
|
{% if confirmation %}
|
||||||
|
|
||||||
{% user_display confirmation.email_address.user as user_display %}
|
{% 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 %}">
|
<form method="post" action="{% url 'account_confirm_email' confirmation.key %}">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
@ -24,7 +24,7 @@
|
|||||||
|
|
||||||
{% url 'account_email' as email_url %}
|
{% 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 %}
|
{% endif %}
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if mail_conf and enable_pwd_forgot %}
|
{% 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">
|
<form method="POST" action="{% url 'account_reset_password' %}" class="password_reset">
|
||||||
{% csrf_token %}
|
{% 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) {
|
function renderSubstituteRow(substitute) {
|
||||||
|
|
||||||
var pk = substitute.pk;
|
var pk = substitute.pk;
|
||||||
@ -171,7 +184,7 @@ function bomSubstitutesDialog(bom_item_id, substitutes, options={}) {
|
|||||||
|
|
||||||
// Render a single row
|
// Render a single row
|
||||||
var html = `
|
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}'>
|
<td id='part-${pk}'>
|
||||||
<a href='/part/${part.pk}/'>
|
<a href='/part/${part.pk}/'>
|
||||||
${thumb} ${part.full_name}
|
${thumb} ${part.full_name}
|
||||||
@ -246,6 +259,21 @@ function bomSubstitutesDialog(bom_item_id, substitutes, options={}) {
|
|||||||
},
|
},
|
||||||
part: {
|
part: {
|
||||||
required: false,
|
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,
|
preFormContent: html,
|
||||||
@ -801,6 +829,7 @@ function loadBomTable(table, options) {
|
|||||||
subs,
|
subs,
|
||||||
{
|
{
|
||||||
table: table,
|
table: table,
|
||||||
|
sub_part: row.sub_part,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -1349,7 +1349,7 @@ function initializeRelatedField(field, fields, options) {
|
|||||||
|
|
||||||
// Allow custom run-time filter augmentation
|
// Allow custom run-time filter augmentation
|
||||||
if ('adjustFilters' in field) {
|
if ('adjustFilters' in field) {
|
||||||
query = field.adjustFilters(query);
|
query = field.adjustFilters(query, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
return query;
|
return query;
|
||||||
|
@ -1,41 +1,39 @@
|
|||||||
# Django framework
|
# Please keep this list sorted
|
||||||
Django==3.2.5 # Django package
|
Django==3.2.5 # Django package
|
||||||
gunicorn>=20.1.0 # Gunicorn web server
|
certifi # Certifi is (most likely) installed through one of the requirements above
|
||||||
|
|
||||||
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
|
|
||||||
coreapi==2.3.0 # API documentation
|
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
|
coverage==5.3 # Unit test coverage
|
||||||
coveralls==2.1.2 # Coveralls linking (for Travis)
|
coveralls==2.1.2 # Coveralls linking (for Travis)
|
||||||
rapidfuzz==0.7.6 # Fuzzy string matching
|
cryptography==3.4.8 # Cryptography support
|
||||||
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
|
|
||||||
django-admin-shell==0.1.2 # Python shell for the admin interface
|
django-admin-shell==0.1.2 # Python shell for the admin interface
|
||||||
py-moneyed==0.8.0 # Specific version requirement for py-moneyed
|
django-allauth==0.45.0 # SSO for external providers via OpenID
|
||||||
django-money==1.1 # Django app for currency management
|
django-cleanup==5.1.0 # Manage deletion of old / unused uploaded files
|
||||||
certifi # Certifi is (most likely) installed through one of the requirements above
|
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-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
|
python-barcode[images]==0.13.1 # Barcode generator
|
||||||
qrcode[pil]==6.1 # QR code generator
|
qrcode[pil]==6.1 # QR code generator
|
||||||
django-q==1.3.4 # Background task scheduling
|
rapidfuzz==0.7.6 # Fuzzy string matching
|
||||||
django-formtools==2.3 # Form wizard tools
|
tablib[xls,xlsx,yaml] # Support for XLS and XLSX formats
|
||||||
cryptography==3.4.8 # Cryptography support
|
weasyprint==52.5 # PDF generation library (Note: in the future need to update to 53)
|
||||||
django-allauth==0.45.0 # SSO for external providers via OpenID
|
|
||||||
|
|
||||||
inventree # Install the latest version of the InvenTree API python library
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user