Merge branch 'master' into build-output-complete

This commit is contained in:
Oliver 2021-10-16 13:18:35 +11:00
commit ccaa7d2683
25 changed files with 562 additions and 107 deletions

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -9,6 +9,10 @@ from .models import Build, BuildItem
class BuildAdmin(ImportExportModelAdmin):
exclude = [
'reference_int',
]
list_display = (
'reference',
'title',

View File

@ -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',

View 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),
),
]

View 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
)
]

View File

@ -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:

View File

@ -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

View File

@ -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:

View File

@ -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',

View File

@ -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',
]

View 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),
),
]

View 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
)
]

View File

@ -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')

View 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)

View File

@ -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)

View File

@ -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>

View File

@ -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) {

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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,
}
);
});

View File

@ -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;

View File

@ -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)