mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge pull request #812 from SchrodingersGat/more-testing
Add function to generate "keys" for test results.
This commit is contained in:
commit
915bbef3b4
@ -19,6 +19,23 @@ from .version import inventreeVersion, inventreeInstanceName
|
|||||||
from .settings import MEDIA_URL, STATIC_URL
|
from .settings import MEDIA_URL, STATIC_URL
|
||||||
|
|
||||||
|
|
||||||
|
def generateTestKey(test_name):
|
||||||
|
"""
|
||||||
|
Generate a test 'key' for a given test name.
|
||||||
|
This must not have illegal chars as it will be used for dict lookup in a template.
|
||||||
|
|
||||||
|
Tests must be named such that they will have unique keys.
|
||||||
|
"""
|
||||||
|
|
||||||
|
key = test_name.strip().lower()
|
||||||
|
key = key.replace(" ", "")
|
||||||
|
|
||||||
|
# Remove any characters that cannot be used to represent a variable
|
||||||
|
key = re.sub(r'[^a-zA-Z0-9]', '', key)
|
||||||
|
|
||||||
|
return key
|
||||||
|
|
||||||
|
|
||||||
def getMediaUrl(filename):
|
def getMediaUrl(filename):
|
||||||
"""
|
"""
|
||||||
Return the qualified access path for the given file,
|
Return the qualified access path for the given file,
|
||||||
|
@ -53,6 +53,9 @@ class InvenTreeAttachment(models.Model):
|
|||||||
|
|
||||||
return "attachments"
|
return "attachments"
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return os.path.basename(self.attachment.name)
|
||||||
|
|
||||||
attachment = models.FileField(upload_to=rename_attachment,
|
attachment = models.FileField(upload_to=rename_attachment,
|
||||||
help_text=_('Select file to attach'))
|
help_text=_('Select file to attach'))
|
||||||
|
|
||||||
|
@ -12,6 +12,7 @@ from .models import PartCategory, Part
|
|||||||
from .models import PartAttachment, PartStar
|
from .models import PartAttachment, PartStar
|
||||||
from .models import BomItem
|
from .models import BomItem
|
||||||
from .models import PartParameterTemplate, PartParameter
|
from .models import PartParameterTemplate, PartParameter
|
||||||
|
from .models import PartTestTemplate
|
||||||
|
|
||||||
from stock.models import StockLocation
|
from stock.models import StockLocation
|
||||||
from company.models import SupplierPart
|
from company.models import SupplierPart
|
||||||
@ -126,6 +127,11 @@ class PartStarAdmin(admin.ModelAdmin):
|
|||||||
list_display = ('part', 'user')
|
list_display = ('part', 'user')
|
||||||
|
|
||||||
|
|
||||||
|
class PartTestTemplateAdmin(admin.ModelAdmin):
|
||||||
|
|
||||||
|
list_display = ('part', 'test_name', 'required')
|
||||||
|
|
||||||
|
|
||||||
class BomItemResource(ModelResource):
|
class BomItemResource(ModelResource):
|
||||||
""" Class for managing BomItem data import/export """
|
""" Class for managing BomItem data import/export """
|
||||||
|
|
||||||
@ -202,3 +208,4 @@ admin.site.register(PartStar, PartStarAdmin)
|
|||||||
admin.site.register(BomItem, BomItemAdmin)
|
admin.site.register(BomItem, BomItemAdmin)
|
||||||
admin.site.register(PartParameterTemplate, ParameterTemplateAdmin)
|
admin.site.register(PartParameterTemplate, ParameterTemplateAdmin)
|
||||||
admin.site.register(PartParameter, ParameterAdmin)
|
admin.site.register(PartParameter, ParameterAdmin)
|
||||||
|
admin.site.register(PartTestTemplate, PartTestTemplateAdmin)
|
||||||
|
@ -19,7 +19,7 @@ from django.urls import reverse
|
|||||||
|
|
||||||
from .models import Part, PartCategory, BomItem, PartStar
|
from .models import Part, PartCategory, BomItem, PartStar
|
||||||
from .models import PartParameter, PartParameterTemplate
|
from .models import PartParameter, PartParameterTemplate
|
||||||
from .models import PartAttachment
|
from .models import PartAttachment, PartTestTemplate
|
||||||
|
|
||||||
from . import serializers as part_serializers
|
from . import serializers as part_serializers
|
||||||
|
|
||||||
@ -120,6 +120,52 @@ class PartAttachmentList(generics.ListCreateAPIView, AttachmentMixin):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class PartTestTemplateList(generics.ListCreateAPIView):
|
||||||
|
"""
|
||||||
|
API endpoint for listing (and creating) a PartTestTemplate.
|
||||||
|
"""
|
||||||
|
|
||||||
|
queryset = PartTestTemplate.objects.all()
|
||||||
|
serializer_class = part_serializers.PartTestTemplateSerializer
|
||||||
|
|
||||||
|
def filter_queryset(self, queryset):
|
||||||
|
"""
|
||||||
|
Filter the test list queryset.
|
||||||
|
|
||||||
|
If filtering by 'part', we include results for any parts "above" the specified part.
|
||||||
|
"""
|
||||||
|
|
||||||
|
queryset = super().filter_queryset(queryset)
|
||||||
|
|
||||||
|
params = self.request.query_params
|
||||||
|
|
||||||
|
part = params.get('part', None)
|
||||||
|
|
||||||
|
# Filter by part
|
||||||
|
if part:
|
||||||
|
try:
|
||||||
|
part = Part.objects.get(pk=part)
|
||||||
|
queryset = queryset.filter(part__in=part.get_ancestors(include_self=True))
|
||||||
|
except (ValueError, Part.DoesNotExist):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Filter by 'required' status
|
||||||
|
required = params.get('required', None)
|
||||||
|
|
||||||
|
if required is not None:
|
||||||
|
queryset = queryset.filter(required=required)
|
||||||
|
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
permission_classes = [permissions.IsAuthenticated]
|
||||||
|
|
||||||
|
filter_backends = [
|
||||||
|
DjangoFilterBackend,
|
||||||
|
filters.OrderingFilter,
|
||||||
|
filters.SearchFilter,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class PartThumbs(generics.ListAPIView):
|
class PartThumbs(generics.ListAPIView):
|
||||||
""" API endpoint for retrieving information on available Part thumbnails """
|
""" API endpoint for retrieving information on available Part thumbnails """
|
||||||
|
|
||||||
@ -635,8 +681,13 @@ part_api_urls = [
|
|||||||
url(r'^$', CategoryList.as_view(), name='api-part-category-list'),
|
url(r'^$', CategoryList.as_view(), name='api-part-category-list'),
|
||||||
])),
|
])),
|
||||||
|
|
||||||
|
# Base URL for PartTestTemplate API endpoints
|
||||||
|
url(r'^test-template/', include([
|
||||||
|
url(r'^$', PartTestTemplateList.as_view(), name='api-part-test-template-list'),
|
||||||
|
])),
|
||||||
|
|
||||||
# Base URL for PartAttachment API endpoints
|
# Base URL for PartAttachment API endpoints
|
||||||
url(r'attachment/', include([
|
url(r'^attachment/', include([
|
||||||
url(r'^$', PartAttachmentList.as_view(), name='api-part-attachment-list'),
|
url(r'^$', PartAttachmentList.as_view(), name='api-part-attachment-list'),
|
||||||
])),
|
])),
|
||||||
|
|
||||||
|
@ -106,6 +106,7 @@
|
|||||||
name: 'Chair Template'
|
name: 'Chair Template'
|
||||||
description: 'A chair'
|
description: 'A chair'
|
||||||
is_template: True
|
is_template: True
|
||||||
|
trackable: true
|
||||||
category: 7
|
category: 7
|
||||||
tree_id: 1
|
tree_id: 1
|
||||||
level: 0
|
level: 0
|
||||||
@ -117,6 +118,7 @@
|
|||||||
fields:
|
fields:
|
||||||
name: 'Blue Chair'
|
name: 'Blue Chair'
|
||||||
variant_of: 10000
|
variant_of: 10000
|
||||||
|
trackable: true
|
||||||
category: 7
|
category: 7
|
||||||
tree_id: 1
|
tree_id: 1
|
||||||
level: 0
|
level: 0
|
||||||
@ -128,6 +130,7 @@
|
|||||||
fields:
|
fields:
|
||||||
name: 'Red chair'
|
name: 'Red chair'
|
||||||
variant_of: 10000
|
variant_of: 10000
|
||||||
|
trackable: true
|
||||||
category: 7
|
category: 7
|
||||||
tree_id: 1
|
tree_id: 1
|
||||||
level: 0
|
level: 0
|
||||||
@ -140,6 +143,7 @@
|
|||||||
name: 'Green chair'
|
name: 'Green chair'
|
||||||
variant_of: 10000
|
variant_of: 10000
|
||||||
category: 7
|
category: 7
|
||||||
|
trackable: true
|
||||||
tree_id: 1
|
tree_id: 1
|
||||||
level: 0
|
level: 0
|
||||||
lft: 0
|
lft: 0
|
||||||
@ -150,7 +154,8 @@
|
|||||||
fields:
|
fields:
|
||||||
name: 'Green chair variant'
|
name: 'Green chair variant'
|
||||||
variant_of: 10003
|
variant_of: 10003
|
||||||
category:
|
category: 7
|
||||||
|
trackable: true
|
||||||
tree_id: 1
|
tree_id: 1
|
||||||
level: 0
|
level: 0
|
||||||
lft: 0
|
lft: 0
|
||||||
|
39
InvenTree/part/fixtures/test_templates.yaml
Normal file
39
InvenTree/part/fixtures/test_templates.yaml
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
# Tests for the top-level "chair" part
|
||||||
|
- model: part.parttesttemplate
|
||||||
|
fields:
|
||||||
|
part: 10000
|
||||||
|
test_name: Test strength of chair
|
||||||
|
|
||||||
|
- model: part.parttesttemplate
|
||||||
|
fields:
|
||||||
|
part: 10000
|
||||||
|
test_name: Apply paint
|
||||||
|
|
||||||
|
- model: part.parttesttemplate
|
||||||
|
fields:
|
||||||
|
part: 10000
|
||||||
|
test_name: Sew cushion
|
||||||
|
|
||||||
|
- model: part.parttesttemplate
|
||||||
|
fields:
|
||||||
|
part: 10000
|
||||||
|
test_name: Attach legs
|
||||||
|
|
||||||
|
- model: part.parttesttemplate
|
||||||
|
fields:
|
||||||
|
part: 10000
|
||||||
|
test_name: Record weight
|
||||||
|
required: false
|
||||||
|
|
||||||
|
# Add some tests for one of the variants
|
||||||
|
- model: part.parttesttemplate
|
||||||
|
fields:
|
||||||
|
part: 10003
|
||||||
|
test_name: Check that chair is green
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- model: part.parttesttemplate
|
||||||
|
fields:
|
||||||
|
part: 10004
|
||||||
|
test_name: Check that chair is especially green
|
||||||
|
required: False
|
@ -15,6 +15,7 @@ from django.utils.translation import ugettext as _
|
|||||||
from .models import Part, PartCategory, PartAttachment
|
from .models import Part, PartCategory, PartAttachment
|
||||||
from .models import BomItem
|
from .models import BomItem
|
||||||
from .models import PartParameterTemplate, PartParameter
|
from .models import PartParameterTemplate, PartParameter
|
||||||
|
from .models import PartTestTemplate
|
||||||
|
|
||||||
from common.models import Currency
|
from common.models import Currency
|
||||||
|
|
||||||
@ -29,6 +30,19 @@ class PartImageForm(HelperForm):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class EditPartTestTemplateForm(HelperForm):
|
||||||
|
""" Class for creating / editing a PartTestTemplate object """
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = PartTestTemplate
|
||||||
|
|
||||||
|
fields = [
|
||||||
|
'part',
|
||||||
|
'test_name',
|
||||||
|
'required'
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class BomExportForm(forms.Form):
|
class BomExportForm(forms.Form):
|
||||||
""" Simple form to let user set BOM export options,
|
""" Simple form to let user set BOM export options,
|
||||||
before exporting a BOM (bill of materials) file.
|
before exporting a BOM (bill of materials) file.
|
||||||
@ -125,13 +139,11 @@ class EditPartForm(HelperForm):
|
|||||||
'revision',
|
'revision',
|
||||||
'keywords',
|
'keywords',
|
||||||
'variant_of',
|
'variant_of',
|
||||||
'is_template',
|
|
||||||
'link',
|
'link',
|
||||||
'default_location',
|
'default_location',
|
||||||
'default_supplier',
|
'default_supplier',
|
||||||
'units',
|
'units',
|
||||||
'minimum_stock',
|
'minimum_stock',
|
||||||
'active',
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
23
InvenTree/part/migrations/0040_parttesttemplate.py
Normal file
23
InvenTree/part/migrations/0040_parttesttemplate.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 3.0.5 on 2020-05-17 03:26
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('part', '0039_auto_20200515_1127'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='PartTestTemplate',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('test_name', models.CharField(help_text='Enter a name for the test', max_length=100, verbose_name='Test name')),
|
||||||
|
('required', models.BooleanField(default=True, help_text='Is this test required to pass?', verbose_name='Required')),
|
||||||
|
('part', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='test_templates', to='part.Part')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
19
InvenTree/part/migrations/0041_auto_20200517_0348.py
Normal file
19
InvenTree/part/migrations/0041_auto_20200517_0348.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
# Generated by Django 3.0.5 on 2020-05-17 03:48
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('part', '0040_parttesttemplate'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='parttesttemplate',
|
||||||
|
name='part',
|
||||||
|
field=models.ForeignKey(limit_choices_to={'trackable': True}, on_delete=django.db.models.deletion.CASCADE, related_name='test_templates', to='part.Part'),
|
||||||
|
),
|
||||||
|
]
|
@ -990,6 +990,30 @@ class Part(MPTTModel):
|
|||||||
|
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
|
def getTestTemplates(self, required=None, include_parent=True):
|
||||||
|
"""
|
||||||
|
Return a list of all test templates associated with this Part.
|
||||||
|
These are used for validation of a StockItem.
|
||||||
|
|
||||||
|
args:
|
||||||
|
required: Set to True or False to filter by "required" status
|
||||||
|
include_parent: Set to True to traverse upwards
|
||||||
|
"""
|
||||||
|
|
||||||
|
if include_parent:
|
||||||
|
tests = PartTestTemplate.objects.filter(part__in=self.get_ancestors(include_self=True))
|
||||||
|
else:
|
||||||
|
tests = self.test_templates
|
||||||
|
|
||||||
|
if required is not None:
|
||||||
|
tests = tests.filter(required=required)
|
||||||
|
|
||||||
|
return tests
|
||||||
|
|
||||||
|
def getRequiredTests(self):
|
||||||
|
# Return the tests which are required by this part
|
||||||
|
return self.getTestTemplates(required=True)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def attachment_count(self):
|
def attachment_count(self):
|
||||||
""" Count the number of attachments for this part.
|
""" Count the number of attachments for this part.
|
||||||
@ -1109,6 +1133,88 @@ class PartStar(models.Model):
|
|||||||
unique_together = ['part', 'user']
|
unique_together = ['part', 'user']
|
||||||
|
|
||||||
|
|
||||||
|
class PartTestTemplate(models.Model):
|
||||||
|
"""
|
||||||
|
A PartTestTemplate defines a 'template' for a test which is required to be run
|
||||||
|
against a StockItem (an instance of the Part).
|
||||||
|
|
||||||
|
The test template applies "recursively" to part variants, allowing tests to be
|
||||||
|
defined in a heirarchy.
|
||||||
|
|
||||||
|
Test names are simply strings, rather than enforcing any sort of structure or pattern.
|
||||||
|
It is up to the user to determine what tests are defined (and how they are run).
|
||||||
|
|
||||||
|
To enable generation of unique lookup-keys for each test, there are some validation tests
|
||||||
|
run on the model (refer to the validate_unique function).
|
||||||
|
"""
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
|
||||||
|
self.clean()
|
||||||
|
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
|
||||||
|
self.test_name = self.test_name.strip()
|
||||||
|
|
||||||
|
self.validate_unique()
|
||||||
|
super().clean()
|
||||||
|
|
||||||
|
def validate_unique(self, exclude=None):
|
||||||
|
"""
|
||||||
|
Test that this test template is 'unique' within this part tree.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not self.part.trackable:
|
||||||
|
raise ValidationError({
|
||||||
|
'part': _('Test templates can only be created for trackable parts')
|
||||||
|
})
|
||||||
|
|
||||||
|
# Get a list of all tests "above" this one
|
||||||
|
tests = PartTestTemplate.objects.filter(
|
||||||
|
part__in=self.part.get_ancestors(include_self=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
# If this item is already in the database, exclude it from comparison!
|
||||||
|
if self.pk is not None:
|
||||||
|
tests = tests.exclude(pk=self.pk)
|
||||||
|
|
||||||
|
key = self.key
|
||||||
|
|
||||||
|
for test in tests:
|
||||||
|
if test.key == key:
|
||||||
|
raise ValidationError({
|
||||||
|
'test_name': _("Test with this name already exists for this part")
|
||||||
|
})
|
||||||
|
|
||||||
|
super().validate_unique(exclude)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def key(self):
|
||||||
|
""" Generate a key for this test """
|
||||||
|
return helpers.generateTestKey(self.test_name)
|
||||||
|
|
||||||
|
part = models.ForeignKey(
|
||||||
|
Part,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='test_templates',
|
||||||
|
limit_choices_to={'trackable': True},
|
||||||
|
)
|
||||||
|
|
||||||
|
test_name = models.CharField(
|
||||||
|
blank=False, max_length=100,
|
||||||
|
verbose_name=_("Test name"),
|
||||||
|
help_text=_("Enter a name for the test")
|
||||||
|
)
|
||||||
|
|
||||||
|
required = models.BooleanField(
|
||||||
|
default=True,
|
||||||
|
verbose_name=_("Required"),
|
||||||
|
help_text=_("Is this test required to pass?")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class PartParameterTemplate(models.Model):
|
class PartParameterTemplate(models.Model):
|
||||||
"""
|
"""
|
||||||
A PartParameterTemplate provides a template for key:value pairs for extra
|
A PartParameterTemplate provides a template for key:value pairs for extra
|
||||||
|
@ -10,6 +10,7 @@ from .models import PartCategory
|
|||||||
from .models import BomItem
|
from .models import BomItem
|
||||||
from .models import PartParameter, PartParameterTemplate
|
from .models import PartParameter, PartParameterTemplate
|
||||||
from .models import PartAttachment
|
from .models import PartAttachment
|
||||||
|
from .models import PartTestTemplate
|
||||||
|
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
@ -56,6 +57,22 @@ class PartAttachmentSerializer(InvenTreeModelSerializer):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class PartTestTemplateSerializer(InvenTreeModelSerializer):
|
||||||
|
"""
|
||||||
|
Serializer for the PartTestTemplate class
|
||||||
|
"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = PartTestTemplate
|
||||||
|
|
||||||
|
fields = [
|
||||||
|
'pk',
|
||||||
|
'part',
|
||||||
|
'test_name',
|
||||||
|
'required'
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class PartThumbSerializer(serializers.Serializer):
|
class PartThumbSerializer(serializers.Serializer):
|
||||||
"""
|
"""
|
||||||
Serializer for the 'image' field of the Part model.
|
Serializer for the 'image' field of the Part model.
|
||||||
|
@ -128,6 +128,15 @@
|
|||||||
<td><i>{% trans "Part is not a virtual part" %}</i></td>
|
<td><i>{% trans "Part is not a virtual part" %}</i></td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><b>{% trans "Template" %}</b></td>
|
||||||
|
<td>{% include "slide.html" with state=part.is_template field='is_template' %}</td>
|
||||||
|
{% if part.is_template %}
|
||||||
|
<td>{% trans "Part is a template part (variants can be made from this part)" %}</td>
|
||||||
|
{% else %}
|
||||||
|
<td><i>{% trans "Part is not a template part" %}</i></td>
|
||||||
|
{% endif %}
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><b>{% trans "Assembly" %}</b></td>
|
<td><b>{% trans "Assembly" %}</b></td>
|
||||||
<td>{% include "slide.html" with state=part.assembly field='assembly' %}</td>
|
<td>{% include "slide.html" with state=part.assembly field='assembly' %}</td>
|
||||||
@ -173,6 +182,15 @@
|
|||||||
<td><i>{% trans "Part cannot be sold to customers" %}</i></td>
|
<td><i>{% trans "Part cannot be sold to customers" %}</i></td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><b>{% trans "Active" %}</b></td>
|
||||||
|
<td>{% include "slide.html" with state=part.active field='active' %}</td>
|
||||||
|
{% if part.active %}
|
||||||
|
<td>{% trans "Part is active" %}</td>
|
||||||
|
{% else %}
|
||||||
|
<td><i>{% trans "Part is not active" %}</i></td>
|
||||||
|
{% endif %}
|
||||||
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -196,7 +214,7 @@
|
|||||||
data[field] = checked;
|
data[field] = checked;
|
||||||
|
|
||||||
// Update the particular field
|
// Update the particular field
|
||||||
inventreePut("/api/part/{{ part.id }}/",
|
inventreePut("{% url 'api-part-detail' part.id %}",
|
||||||
data,
|
data,
|
||||||
{
|
{
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
|
75
InvenTree/part/templates/part/part_tests.html
Normal file
75
InvenTree/part/templates/part/part_tests.html
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
{% extends "part/part_base.html" %}
|
||||||
|
{% load static %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% block details %}
|
||||||
|
|
||||||
|
{% include 'part/tabs.html' with tab='tests' %}
|
||||||
|
|
||||||
|
<h4>{% trans "Part Test Templates" %}</h4>
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<div id='button-toolbar'>
|
||||||
|
<div class='button-toolbar container-fluid' style="float: right;">
|
||||||
|
<div class='btn-group' role='group'>
|
||||||
|
<button type='button' class='btn btn-success' id='add-test-template'>{% trans "Add Test Template" %}</button>
|
||||||
|
</div>
|
||||||
|
<div class='filter-list' id='filter-list-parttests'>
|
||||||
|
<!-- Empty div -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table class='table table-striped table-condensed' data-toolbar='#button-toolbar' id='test-template-table'></table>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block js_ready %}
|
||||||
|
{{ block.super }}
|
||||||
|
|
||||||
|
loadPartTestTemplateTable(
|
||||||
|
$("#test-template-table"),
|
||||||
|
{
|
||||||
|
part: {{ part.pk }},
|
||||||
|
params: {
|
||||||
|
part: {{ part.pk }},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
function reloadTable() {
|
||||||
|
$("#test-template-table").bootstrapTable("refresh");
|
||||||
|
}
|
||||||
|
|
||||||
|
$("#add-test-template").click(function() {
|
||||||
|
launchModalForm(
|
||||||
|
"{% url 'part-test-template-create' %}",
|
||||||
|
{
|
||||||
|
data: {
|
||||||
|
part: {{ part.id }},
|
||||||
|
},
|
||||||
|
success: reloadTable,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#test-template-table").on('click', '.button-test-edit', function() {
|
||||||
|
var button = $(this);
|
||||||
|
|
||||||
|
var url = `/part/test-template/${button.attr('pk')}/edit/`;
|
||||||
|
|
||||||
|
launchModalForm(url, {
|
||||||
|
success: reloadTable,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#test-template-table").on('click', '.button-test-delete', function() {
|
||||||
|
var button = $(this);
|
||||||
|
|
||||||
|
var url = `/part/test-template/${button.attr('pk')}/delete/`;
|
||||||
|
|
||||||
|
launchModalForm(url, {
|
||||||
|
success: reloadTable,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
{% endblock %}
|
@ -48,7 +48,9 @@
|
|||||||
<a href="{% url 'part-sales-orders' part.id %}">{% trans "Sales Orders" %} <span class='badge'>{{ part.sales_orders|length }}</span></a>
|
<a href="{% url 'part-sales-orders' part.id %}">{% trans "Sales Orders" %} <span class='badge'>{{ part.sales_orders|length }}</span></a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if 0 and part.trackable %}
|
{% if part.trackable %}
|
||||||
|
{% if 0 %}
|
||||||
|
<!-- TODO - Add the 'tracking' tab back in -->
|
||||||
<li{% ifequal tab 'track' %} class="active"{% endifequal %}>
|
<li{% ifequal tab 'track' %} class="active"{% endifequal %}>
|
||||||
<a href="{% url 'part-track' part.id %}">{% trans "Tracking" %}
|
<a href="{% url 'part-track' part.id %}">{% trans "Tracking" %}
|
||||||
{% if parts.serials.all|length > 0 %}
|
{% if parts.serials.all|length > 0 %}
|
||||||
@ -56,6 +58,12 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</a></li>
|
</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<li{% ifequal tab 'tests' %} class='active'{% endifequal %}>
|
||||||
|
<a href='{% url "part-test-templates" part.id %}'>{% trans "Tests" %}
|
||||||
|
{% if part.getTestTemplates.count > 0 %}<span class='badge'>{{ part.getTestTemplates.count }}</span>{% endif %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
<li{% ifequal tab 'attachments' %} class="active"{% endifequal %}>
|
<li{% ifequal tab 'attachments' %} class="active"{% endifequal %}>
|
||||||
<a href="{% url 'part-attachments' part.id %}">{% trans "Attachments" %} {% if part.attachment_count > 0 %}<span class="badge">{{ part.attachment_count }}</span>{% endif %}</a>
|
<a href="{% url 'part-attachments' part.id %}">{% trans "Attachments" %} {% if part.attachment_count > 0 %}<span class="badge">{{ part.attachment_count }}</span>{% endif %}</a>
|
||||||
</li>
|
</li>
|
||||||
|
@ -16,6 +16,7 @@ class PartAPITest(APITestCase):
|
|||||||
'part',
|
'part',
|
||||||
'location',
|
'location',
|
||||||
'bom',
|
'bom',
|
||||||
|
'test_templates',
|
||||||
]
|
]
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
@ -159,3 +160,56 @@ class PartAPITest(APITestCase):
|
|||||||
data['part'] = 2
|
data['part'] = 2
|
||||||
data['sub_part'] = 2
|
data['sub_part'] = 2
|
||||||
response = self.client.post(url, data, format='json')
|
response = self.client.post(url, data, format='json')
|
||||||
|
|
||||||
|
def test_test_templates(self):
|
||||||
|
|
||||||
|
url = reverse('api-part-test-template-list')
|
||||||
|
|
||||||
|
# List ALL items
|
||||||
|
response = self.client.get(url)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(len(response.data), 7)
|
||||||
|
|
||||||
|
# Request for a particular part
|
||||||
|
response = self.client.get(url, data={'part': 10000})
|
||||||
|
self.assertEqual(len(response.data), 5)
|
||||||
|
|
||||||
|
response = self.client.get(url, data={'part': 10004})
|
||||||
|
self.assertEqual(len(response.data), 7)
|
||||||
|
|
||||||
|
# Try to post a new object (should succeed)
|
||||||
|
response = self.client.post(
|
||||||
|
url,
|
||||||
|
data={
|
||||||
|
'part': 10000,
|
||||||
|
'test_name': 'New Test',
|
||||||
|
'required': True,
|
||||||
|
},
|
||||||
|
format='json',
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||||
|
|
||||||
|
# Try to post a new test with the same name (should fail)
|
||||||
|
response = self.client.post(
|
||||||
|
url,
|
||||||
|
data={
|
||||||
|
'part': 10004,
|
||||||
|
'test_name': " newtest"
|
||||||
|
},
|
||||||
|
format='json',
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
# Try to post a new test against a non-trackable part (should fail)
|
||||||
|
response = self.client.post(
|
||||||
|
url,
|
||||||
|
data={
|
||||||
|
'part': 1,
|
||||||
|
'test_name': 'A simple test',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
@ -88,9 +88,9 @@ class CategoryTest(TestCase):
|
|||||||
|
|
||||||
self.assertEqual(self.electronics.partcount(), 3)
|
self.assertEqual(self.electronics.partcount(), 3)
|
||||||
|
|
||||||
self.assertEqual(self.mechanical.partcount(), 8)
|
self.assertEqual(self.mechanical.partcount(), 9)
|
||||||
self.assertEqual(self.mechanical.partcount(active=True), 7)
|
self.assertEqual(self.mechanical.partcount(active=True), 8)
|
||||||
self.assertEqual(self.mechanical.partcount(False), 6)
|
self.assertEqual(self.mechanical.partcount(False), 7)
|
||||||
|
|
||||||
self.assertEqual(self.electronics.item_count, self.electronics.partcount())
|
self.assertEqual(self.electronics.item_count, self.electronics.partcount())
|
||||||
|
|
||||||
|
@ -5,10 +5,11 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from .models import Part
|
from .models import Part, PartTestTemplate
|
||||||
from .models import rename_part_image, match_part_names
|
from .models import rename_part_image, match_part_names
|
||||||
from .templatetags import inventree_extras
|
from .templatetags import inventree_extras
|
||||||
|
|
||||||
@ -105,3 +106,61 @@ class PartTest(TestCase):
|
|||||||
matches = match_part_names('M2x5 LPHS')
|
matches = match_part_names('M2x5 LPHS')
|
||||||
|
|
||||||
self.assertTrue(len(matches) > 0)
|
self.assertTrue(len(matches) > 0)
|
||||||
|
|
||||||
|
|
||||||
|
class TestTemplateTest(TestCase):
|
||||||
|
|
||||||
|
fixtures = [
|
||||||
|
'category',
|
||||||
|
'part',
|
||||||
|
'location',
|
||||||
|
'test_templates',
|
||||||
|
]
|
||||||
|
|
||||||
|
def test_template_count(self):
|
||||||
|
|
||||||
|
chair = Part.objects.get(pk=10000)
|
||||||
|
|
||||||
|
# Tests for the top-level chair object (nothing above it!)
|
||||||
|
self.assertEqual(chair.test_templates.count(), 5)
|
||||||
|
self.assertEqual(chair.getTestTemplates().count(), 5)
|
||||||
|
self.assertEqual(chair.getTestTemplates(required=True).count(), 4)
|
||||||
|
self.assertEqual(chair.getTestTemplates(required=False).count(), 1)
|
||||||
|
|
||||||
|
# Test the lowest-level part which has more associated tests
|
||||||
|
variant = Part.objects.get(pk=10004)
|
||||||
|
|
||||||
|
self.assertEqual(variant.getTestTemplates().count(), 7)
|
||||||
|
self.assertEqual(variant.getTestTemplates(include_parent=False).count(), 1)
|
||||||
|
self.assertEqual(variant.getTestTemplates(required=True).count(), 5)
|
||||||
|
|
||||||
|
def test_uniqueness(self):
|
||||||
|
# Test names must be unique for this part and also parts above
|
||||||
|
|
||||||
|
variant = Part.objects.get(pk=10004)
|
||||||
|
|
||||||
|
with self.assertRaises(ValidationError):
|
||||||
|
PartTestTemplate.objects.create(
|
||||||
|
part=variant,
|
||||||
|
test_name='Record weight'
|
||||||
|
)
|
||||||
|
|
||||||
|
with self.assertRaises(ValidationError):
|
||||||
|
PartTestTemplate.objects.create(
|
||||||
|
part=variant,
|
||||||
|
test_name='Check that chair is especially green'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Also should fail if we attempt to create a test that would generate the same key
|
||||||
|
with self.assertRaises(ValidationError):
|
||||||
|
PartTestTemplate.objects.create(
|
||||||
|
part=variant,
|
||||||
|
test_name='ReCoRD weiGHT '
|
||||||
|
)
|
||||||
|
|
||||||
|
# But we should be able to create a new one!
|
||||||
|
n = variant.getTestTemplates().count()
|
||||||
|
|
||||||
|
PartTestTemplate.objects.create(part=variant, test_name='A Sample Test')
|
||||||
|
|
||||||
|
self.assertEqual(variant.getTestTemplates().count(), n + 1)
|
||||||
|
@ -52,6 +52,7 @@ part_detail_urls = [
|
|||||||
url(r'^suppliers/?', views.PartDetail.as_view(template_name='part/supplier.html'), name='part-suppliers'),
|
url(r'^suppliers/?', views.PartDetail.as_view(template_name='part/supplier.html'), name='part-suppliers'),
|
||||||
url(r'^orders/?', views.PartDetail.as_view(template_name='part/orders.html'), name='part-orders'),
|
url(r'^orders/?', views.PartDetail.as_view(template_name='part/orders.html'), name='part-orders'),
|
||||||
url(r'^sales-orders/', views.PartDetail.as_view(template_name='part/sales_orders.html'), name='part-sales-orders'),
|
url(r'^sales-orders/', views.PartDetail.as_view(template_name='part/sales_orders.html'), name='part-sales-orders'),
|
||||||
|
url(r'^tests/', views.PartDetail.as_view(template_name='part/part_tests.html'), name='part-test-templates'),
|
||||||
url(r'^track/?', views.PartDetail.as_view(template_name='part/track.html'), name='part-track'),
|
url(r'^track/?', views.PartDetail.as_view(template_name='part/track.html'), name='part-track'),
|
||||||
url(r'^attachments/?', views.PartDetail.as_view(template_name='part/attachments.html'), name='part-attachments'),
|
url(r'^attachments/?', views.PartDetail.as_view(template_name='part/attachments.html'), name='part-attachments'),
|
||||||
url(r'^notes/?', views.PartNotes.as_view(), name='part-notes'),
|
url(r'^notes/?', views.PartNotes.as_view(), name='part-notes'),
|
||||||
@ -107,6 +108,13 @@ part_urls = [
|
|||||||
# Part attachments
|
# Part attachments
|
||||||
url(r'^attachment/', include(part_attachment_urls)),
|
url(r'^attachment/', include(part_attachment_urls)),
|
||||||
|
|
||||||
|
# Part test templates
|
||||||
|
url(r'^test-template/', include([
|
||||||
|
url(r'^new/', views.PartTestTemplateCreate.as_view(), name='part-test-template-create'),
|
||||||
|
url(r'^(?P<pk>\d+)/edit/', views.PartTestTemplateEdit.as_view(), name='part-test-template-edit'),
|
||||||
|
url(r'^(?P<pk>\d+)/delete/', views.PartTestTemplateDelete.as_view(), name='part-test-template-delete'),
|
||||||
|
])),
|
||||||
|
|
||||||
# Part parameters
|
# Part parameters
|
||||||
url(r'^parameter/', include(part_parameter_urls)),
|
url(r'^parameter/', include(part_parameter_urls)),
|
||||||
|
|
||||||
|
@ -25,6 +25,7 @@ from .models import PartCategory, Part, PartAttachment
|
|||||||
from .models import PartParameterTemplate, PartParameter
|
from .models import PartParameterTemplate, PartParameter
|
||||||
from .models import BomItem
|
from .models import BomItem
|
||||||
from .models import match_part_names
|
from .models import match_part_names
|
||||||
|
from .models import PartTestTemplate
|
||||||
|
|
||||||
from common.models import Currency, InvenTreeSetting
|
from common.models import Currency, InvenTreeSetting
|
||||||
from company.models import SupplierPart
|
from company.models import SupplierPart
|
||||||
@ -149,6 +150,55 @@ class PartAttachmentDelete(AjaxDeleteView):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class PartTestTemplateCreate(AjaxCreateView):
|
||||||
|
""" View for creating a PartTestTemplate """
|
||||||
|
|
||||||
|
model = PartTestTemplate
|
||||||
|
form_class = part_forms.EditPartTestTemplateForm
|
||||||
|
ajax_form_title = _("Create Test Template")
|
||||||
|
|
||||||
|
def get_initial(self):
|
||||||
|
|
||||||
|
initials = super().get_initial()
|
||||||
|
|
||||||
|
try:
|
||||||
|
part_id = self.request.GET.get('part', None)
|
||||||
|
initials['part'] = Part.objects.get(pk=part_id)
|
||||||
|
except (ValueError, Part.DoesNotExist):
|
||||||
|
pass
|
||||||
|
|
||||||
|
return initials
|
||||||
|
|
||||||
|
def get_form(self):
|
||||||
|
|
||||||
|
form = super().get_form()
|
||||||
|
form.fields['part'].widget = HiddenInput()
|
||||||
|
|
||||||
|
return form
|
||||||
|
|
||||||
|
|
||||||
|
class PartTestTemplateEdit(AjaxUpdateView):
|
||||||
|
""" View for editing a PartTestTemplate """
|
||||||
|
|
||||||
|
model = PartTestTemplate
|
||||||
|
form_class = part_forms.EditPartTestTemplateForm
|
||||||
|
ajax_form_title = _("Edit Test Template")
|
||||||
|
|
||||||
|
def get_form(self):
|
||||||
|
|
||||||
|
form = super().get_form()
|
||||||
|
form.fields['part'].widget = HiddenInput()
|
||||||
|
|
||||||
|
return form
|
||||||
|
|
||||||
|
|
||||||
|
class PartTestTemplateDelete(AjaxDeleteView):
|
||||||
|
""" View for deleting a PartTestTemplate """
|
||||||
|
|
||||||
|
model = PartTestTemplate
|
||||||
|
ajax_form_title = _("Delete Test Template")
|
||||||
|
|
||||||
|
|
||||||
class PartSetCategory(AjaxUpdateView):
|
class PartSetCategory(AjaxUpdateView):
|
||||||
""" View for settings the part category for multiple parts at once """
|
""" View for settings the part category for multiple parts at once """
|
||||||
|
|
||||||
@ -251,7 +301,6 @@ class MakePartVariant(AjaxCreateView):
|
|||||||
form = super(AjaxCreateView, self).get_form()
|
form = super(AjaxCreateView, self).get_form()
|
||||||
|
|
||||||
# Hide some variant-related fields
|
# Hide some variant-related fields
|
||||||
form.fields['is_template'].widget = HiddenInput()
|
|
||||||
form.fields['variant_of'].widget = HiddenInput()
|
form.fields['variant_of'].widget = HiddenInput()
|
||||||
|
|
||||||
return form
|
return form
|
||||||
|
@ -29,3 +29,40 @@
|
|||||||
result: True
|
result: True
|
||||||
date: 2020-05-17
|
date: 2020-05-17
|
||||||
notes: 'Passed temperature test by making it cooler'
|
notes: 'Passed temperature test by making it cooler'
|
||||||
|
|
||||||
|
- model: stock.stockitemtestresult
|
||||||
|
fields:
|
||||||
|
stock_item: 522
|
||||||
|
test: 'applypaint'
|
||||||
|
result: True
|
||||||
|
date: 2020-05-17
|
||||||
|
|
||||||
|
- model: stock.stockitemtestresult
|
||||||
|
fields:
|
||||||
|
stock_item: 522
|
||||||
|
test: 'applypaint'
|
||||||
|
result: False
|
||||||
|
date: 2020-05-18
|
||||||
|
|
||||||
|
- model: stock.stockitemtestresult
|
||||||
|
fields:
|
||||||
|
stock_item: 522
|
||||||
|
test: 'Attach Legs'
|
||||||
|
result: True
|
||||||
|
date: 2020-05-17
|
||||||
|
|
||||||
|
- model: stock.stockitemtestresult
|
||||||
|
fields:
|
||||||
|
stock_item: 522
|
||||||
|
test: 'Check that chair is GreEn '
|
||||||
|
result: True
|
||||||
|
date: 2020-05-17
|
||||||
|
|
||||||
|
- model: stock.stockitemtestresult
|
||||||
|
pk: 12345
|
||||||
|
fields:
|
||||||
|
stock_item: 522
|
||||||
|
test: 'test strength of chair'
|
||||||
|
result: False
|
||||||
|
value: 100kg
|
||||||
|
date: 2020-05-17
|
@ -46,6 +46,7 @@ class EditStockItemTestResultForm(HelperForm):
|
|||||||
'test',
|
'test',
|
||||||
'result',
|
'result',
|
||||||
'value',
|
'value',
|
||||||
|
'attachment',
|
||||||
'notes',
|
'notes',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -276,10 +276,6 @@ class StockItem(MPTTModel):
|
|||||||
# Serial numbered items cannot be deleted on depletion
|
# Serial numbered items cannot be deleted on depletion
|
||||||
self.delete_on_deplete = False
|
self.delete_on_deplete = False
|
||||||
|
|
||||||
# A template part cannot be instantiated as a StockItem
|
|
||||||
if self.part.is_template:
|
|
||||||
raise ValidationError({'part': _('Stock item cannot be created for a template Part')})
|
|
||||||
|
|
||||||
except PartModels.Part.DoesNotExist:
|
except PartModels.Part.DoesNotExist:
|
||||||
# This gets thrown if self.supplier_part is null
|
# This gets thrown if self.supplier_part is null
|
||||||
# TODO - Find a test than can be perfomed...
|
# TODO - Find a test than can be perfomed...
|
||||||
@ -962,10 +958,57 @@ class StockItem(MPTTModel):
|
|||||||
result_map = {}
|
result_map = {}
|
||||||
|
|
||||||
for result in results:
|
for result in results:
|
||||||
result_map[result.test] = result
|
key = helpers.generateTestKey(result.test)
|
||||||
|
result_map[key] = result
|
||||||
|
|
||||||
return result_map
|
return result_map
|
||||||
|
|
||||||
|
def requiredTestStatus(self):
|
||||||
|
"""
|
||||||
|
Return the status of the tests required for this StockItem.
|
||||||
|
|
||||||
|
return:
|
||||||
|
A dict containing the following items:
|
||||||
|
- total: Number of required tests
|
||||||
|
- passed: Number of tests that have passed
|
||||||
|
- failed: Number of tests that have failed
|
||||||
|
"""
|
||||||
|
|
||||||
|
# All the tests required by the part object
|
||||||
|
required = self.part.getRequiredTests()
|
||||||
|
|
||||||
|
results = self.testResultMap()
|
||||||
|
|
||||||
|
total = len(required)
|
||||||
|
passed = 0
|
||||||
|
failed = 0
|
||||||
|
|
||||||
|
for test in required:
|
||||||
|
key = helpers.generateTestKey(test.test_name)
|
||||||
|
|
||||||
|
if key in results:
|
||||||
|
result = results[key]
|
||||||
|
|
||||||
|
if result.result:
|
||||||
|
passed += 1
|
||||||
|
else:
|
||||||
|
failed += 1
|
||||||
|
|
||||||
|
return {
|
||||||
|
'total': total,
|
||||||
|
'passed': passed,
|
||||||
|
'failed': failed,
|
||||||
|
}
|
||||||
|
|
||||||
|
def hasRequiredTests(self):
|
||||||
|
return self.part.getRequiredTests().count() > 0
|
||||||
|
|
||||||
|
def passedAllRequiredTests(self):
|
||||||
|
|
||||||
|
status = self.requiredTestStatus()
|
||||||
|
|
||||||
|
return status['passed'] >= status['total']
|
||||||
|
|
||||||
|
|
||||||
@receiver(pre_delete, sender=StockItem, dispatch_uid='stock_item_pre_delete_log')
|
@receiver(pre_delete, sender=StockItem, dispatch_uid='stock_item_pre_delete_log')
|
||||||
def before_delete_stock_item(sender, instance, using, **kwargs):
|
def before_delete_stock_item(sender, instance, using, **kwargs):
|
||||||
|
@ -15,6 +15,12 @@ InvenTree | {% trans "Stock Item" %} - {{ item }}
|
|||||||
{% block pre_content %}
|
{% block pre_content %}
|
||||||
{% include 'stock/loc_link.html' with location=item.location %}
|
{% include 'stock/loc_link.html' with location=item.location %}
|
||||||
|
|
||||||
|
{% if item.hasRequiredTests and not item.passedAllRequiredTests %}
|
||||||
|
<div class='alert alert-block alert-danger'>
|
||||||
|
{% trans "This stock item has not passed all required tests" %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% for allocation in item.sales_order_allocations.all %}
|
{% for allocation in item.sales_order_allocations.all %}
|
||||||
<div class='alert alert-block alert-info'>
|
<div class='alert alert-block alert-info'>
|
||||||
{% trans "This stock item is allocated to Sales Order" %} <a href="{% url 'so-detail' allocation.line.order.id %}"><b>#{{ allocation.line.order.reference }}</b></a> ({% trans "Quantity" %}: {% decimal allocation.quantity %})
|
{% trans "This stock item is allocated to Sales Order" %} <a href="{% url 'so-detail' allocation.line.order.id %}"><b>#{{ allocation.line.order.reference }}</b></a> ({% trans "Quantity" %}: {% decimal allocation.quantity %})
|
||||||
@ -221,6 +227,13 @@ InvenTree | {% trans "Stock Item" %} - {{ item }}
|
|||||||
<td>{% trans "Status" %}</td>
|
<td>{% trans "Status" %}</td>
|
||||||
<td>{% stock_status_label item.status %}</td>
|
<td>{% stock_status_label item.status %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
{% if item.hasRequiredTests %}
|
||||||
|
<tr>
|
||||||
|
<td><span class='fas fa-vial'></span></td>
|
||||||
|
<td>{% trans "Tests" %}</td>
|
||||||
|
<td>{{ item.requiredTestStatus.passed }} / {{ item.requiredTestStatus.total }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
</table>
|
</table>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
@ -7,13 +7,13 @@
|
|||||||
|
|
||||||
{% include "stock/tabs.html" with tab='tests' %}
|
{% include "stock/tabs.html" with tab='tests' %}
|
||||||
|
|
||||||
<h4>{% trans "Test Results" %}</h4>
|
<h4>{% trans "Test Data" %}</h4>
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
<div id='button-toolbar'>
|
<div id='button-toolbar'>
|
||||||
<div class='button-toolbar container-fluid' style="float: right;">
|
<div class='button-toolbar container-fluid' style="float: right;">
|
||||||
<div class='btn-group' role='group'>
|
<div class='btn-group' role='group'>
|
||||||
<button type='button' class='btn btn-success' id='add-test-result'>{% trans "Add Test Result" %}</button>
|
<button type='button' class='btn btn-success' id='add-test-result'>{% trans "Add Test Data" %}</button>
|
||||||
</div>
|
</div>
|
||||||
<div class='filter-list' id='filter-list-stocktests'>
|
<div class='filter-list' id='filter-list-stocktests'>
|
||||||
<!-- Empty div -->
|
<!-- Empty div -->
|
||||||
@ -30,16 +30,14 @@
|
|||||||
|
|
||||||
loadStockTestResultsTable(
|
loadStockTestResultsTable(
|
||||||
$("#test-result-table"), {
|
$("#test-result-table"), {
|
||||||
params: {
|
part: {{ item.part.id }},
|
||||||
stock_item: {{ item.id }},
|
stock_item: {{ item.id }},
|
||||||
user_detail: true,
|
|
||||||
attachment_detail: true,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
function reloadTable() {
|
function reloadTable() {
|
||||||
$("#test-result-table").bootstrapTable("refresh");
|
location.reload();
|
||||||
|
//$("#test-result-table").bootstrapTable("refresh");
|
||||||
}
|
}
|
||||||
|
|
||||||
$("#add-test-result").click(function() {
|
$("#add-test-result").click(function() {
|
||||||
@ -53,6 +51,22 @@ $("#add-test-result").click(function() {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$("#test-result-table").on('click', '.button-test-add', function() {
|
||||||
|
var button = $(this);
|
||||||
|
|
||||||
|
var test_name = button.attr('pk');
|
||||||
|
|
||||||
|
launchModalForm(
|
||||||
|
"{% url 'stock-item-test-create' %}", {
|
||||||
|
data: {
|
||||||
|
stock_item: {{ item.id }},
|
||||||
|
test: test_name
|
||||||
|
},
|
||||||
|
success: reloadTable,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
$("#test-result-table").on('click', '.button-test-edit', function() {
|
$("#test-result-table").on('click', '.button-test-edit', function() {
|
||||||
var button = $(this);
|
var button = $(this);
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
{% if item.part.trackable %}
|
{% if item.part.trackable %}
|
||||||
<li{% if tab == 'tests' %} class='active'{% endif %}>
|
<li{% if tab == 'tests' %} class='active'{% endif %}>
|
||||||
<a href="{% url 'stock-item-test-results' item.id %}">
|
<a href="{% url 'stock-item-test-results' item.id %}">
|
||||||
{% trans "Test Results" %}
|
{% trans "Test Data" %}
|
||||||
{% if item.test_results.count > 0 %}<span class='badge'>{{ item.test_results.count }}</span>{% endif %}
|
{% if item.test_results.count > 0 %}<span class='badge'>{{ item.test_results.count }}</span>{% endif %}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
@ -4,6 +4,7 @@ from django.contrib.auth import get_user_model
|
|||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
|
|
||||||
from .models import StockLocation, StockItem, StockItemTracking
|
from .models import StockLocation, StockItem, StockItemTracking
|
||||||
|
from .models import StockItemTestResult
|
||||||
from part.models import Part
|
from part.models import Part
|
||||||
|
|
||||||
|
|
||||||
@ -15,6 +16,7 @@ class StockTest(TestCase):
|
|||||||
fixtures = [
|
fixtures = [
|
||||||
'category',
|
'category',
|
||||||
'part',
|
'part',
|
||||||
|
'test_templates',
|
||||||
'location',
|
'location',
|
||||||
'stock',
|
'stock',
|
||||||
'stock_tests',
|
'stock_tests',
|
||||||
@ -429,5 +431,30 @@ class TestResultTest(StockTest):
|
|||||||
|
|
||||||
self.assertEqual(len(result_map), 3)
|
self.assertEqual(len(result_map), 3)
|
||||||
|
|
||||||
for test in ['Firmware Version', 'Settings Checksum', 'Temperature Test']:
|
# Keys are all lower-case and do not contain spaces
|
||||||
|
for test in ['firmwareversion', 'settingschecksum', 'temperaturetest']:
|
||||||
self.assertIn(test, result_map.keys())
|
self.assertIn(test, result_map.keys())
|
||||||
|
|
||||||
|
def test_test_results(self):
|
||||||
|
item = StockItem.objects.get(pk=522)
|
||||||
|
|
||||||
|
status = item.requiredTestStatus()
|
||||||
|
|
||||||
|
self.assertEqual(status['total'], 5)
|
||||||
|
self.assertEqual(status['passed'], 3)
|
||||||
|
self.assertEqual(status['failed'], 1)
|
||||||
|
|
||||||
|
self.assertFalse(item.passedAllRequiredTests())
|
||||||
|
|
||||||
|
# Add some new test results to make it pass!
|
||||||
|
test = StockItemTestResult.objects.get(pk=12345)
|
||||||
|
test.result = True
|
||||||
|
test.save()
|
||||||
|
|
||||||
|
StockItemTestResult.objects.create(
|
||||||
|
stock_item=item,
|
||||||
|
test='sew cushion',
|
||||||
|
result=True
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertTrue(item.passedAllRequiredTests())
|
||||||
|
@ -254,6 +254,8 @@ class StockItemTestResultCreate(AjaxCreateView):
|
|||||||
except (ValueError, StockItem.DoesNotExist):
|
except (ValueError, StockItem.DoesNotExist):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
initials['test'] = self.request.GET.get('test', '')
|
||||||
|
|
||||||
return initials
|
return initials
|
||||||
|
|
||||||
def get_form(self):
|
def get_form(self):
|
||||||
@ -261,6 +263,17 @@ class StockItemTestResultCreate(AjaxCreateView):
|
|||||||
form = super().get_form()
|
form = super().get_form()
|
||||||
form.fields['stock_item'].widget = HiddenInput()
|
form.fields['stock_item'].widget = HiddenInput()
|
||||||
|
|
||||||
|
# Extract the StockItem object
|
||||||
|
item_id = form['stock_item'].value()
|
||||||
|
|
||||||
|
# Limit the options for the file attachments
|
||||||
|
try:
|
||||||
|
stock_item = StockItem.objects.get(pk=item_id)
|
||||||
|
form.fields['attachment'].queryset = stock_item.attachments.all()
|
||||||
|
except (ValueError, StockItem.DoesNotExist):
|
||||||
|
# Hide the attachments field
|
||||||
|
form.fields['attachment'].widget = HiddenInput()
|
||||||
|
|
||||||
return form
|
return form
|
||||||
|
|
||||||
|
|
||||||
@ -279,6 +292,8 @@ class StockItemTestResultEdit(AjaxUpdateView):
|
|||||||
|
|
||||||
form.fields['stock_item'].widget = HiddenInput()
|
form.fields['stock_item'].widget = HiddenInput()
|
||||||
|
|
||||||
|
form.fields['attachment'].queryset = self.object.stock_item.attachments.all()
|
||||||
|
|
||||||
return form
|
return form
|
||||||
|
|
||||||
|
|
||||||
|
@ -285,3 +285,84 @@ function loadPartTable(table, url, options={}) {
|
|||||||
location.href = '/part/export/?parts=' + parts;
|
location.href = '/part/export/?parts=' + parts;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function loadPartTestTemplateTable(table, options) {
|
||||||
|
/*
|
||||||
|
* Load PartTestTemplate table.
|
||||||
|
*/
|
||||||
|
|
||||||
|
var params = options.params || {};
|
||||||
|
|
||||||
|
var part = options.part || null;
|
||||||
|
|
||||||
|
var filterListElement = options.filterList || '#filter-list-parttests';
|
||||||
|
|
||||||
|
var filters = loadTableFilters("parttests");
|
||||||
|
|
||||||
|
var original = {};
|
||||||
|
|
||||||
|
for (var key in params) {
|
||||||
|
original[key] = params[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
setupFilterList("parttests", table, filterListElement);
|
||||||
|
|
||||||
|
// Override the default values, or add new ones
|
||||||
|
for (var key in params) {
|
||||||
|
filters[key] = params[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
table.inventreeTable({
|
||||||
|
method: 'get',
|
||||||
|
formatNoMatches: function() {
|
||||||
|
return '{% trans "No test templates matching query" %}';
|
||||||
|
},
|
||||||
|
url: "{% url 'api-part-test-template-list' %}",
|
||||||
|
queryParams: filters,
|
||||||
|
original: original,
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
field: 'pk',
|
||||||
|
title: 'ID',
|
||||||
|
visible: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'test_name',
|
||||||
|
title: "{% trans "Test Name" %}",
|
||||||
|
sortable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'required',
|
||||||
|
title: "{% trans 'Required' %}",
|
||||||
|
sortable: true,
|
||||||
|
formatter: function(value) {
|
||||||
|
if (value) {
|
||||||
|
return `<span class='label label-green'>{% trans "YES" %}</span>`;
|
||||||
|
} else {
|
||||||
|
return `<span class='label label-yellow'>{% trans "NO" %}</span>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'buttons',
|
||||||
|
formatter: function(value, row) {
|
||||||
|
var pk = row.pk;
|
||||||
|
|
||||||
|
if (row.part == part) {
|
||||||
|
var html = `<div class='btn-group float-right' role='group'>`;
|
||||||
|
|
||||||
|
html += makeIconButton('fa-edit icon-blue', 'button-test-edit', pk, '{% trans "Edit test result" %}');
|
||||||
|
html += makeIconButton('fa-trash-alt icon-red', 'button-test-delete', pk, '{% trans "Delete test result" %}');
|
||||||
|
|
||||||
|
html += `</div>`;
|
||||||
|
|
||||||
|
return html;
|
||||||
|
} else {
|
||||||
|
return '{% trans "This test is defined for a parent part" %}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
@ -22,79 +22,102 @@ function removeStockRow(e) {
|
|||||||
function passFailBadge(result) {
|
function passFailBadge(result) {
|
||||||
|
|
||||||
if (result) {
|
if (result) {
|
||||||
return `<span class='label label-green'>{% trans "PASS" %}</span>`;
|
return `<span class='label label-green float-right'>{% trans "PASS" %}</span>`;
|
||||||
} else {
|
} else {
|
||||||
return `<span class='label label-red'>{% trans "FAIL" %}</span>`;
|
return `<span class='label label-red float-right'>{% trans "FAIL" %}</span>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function noResultBadge() {
|
||||||
|
return `<span class='label label-blue float-right'>{% trans "NO RESULT" %}</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function testKey(test_name) {
|
||||||
|
// Convert test name to a unique key without any illegal chars
|
||||||
|
|
||||||
|
test_name = test_name.trim().toLowerCase();
|
||||||
|
test_name = test_name.replace(' ', '');
|
||||||
|
|
||||||
|
test_name = test_name.replace(/[^0-9a-z]/gi, '');
|
||||||
|
|
||||||
|
return test_name;
|
||||||
|
}
|
||||||
|
|
||||||
function loadStockTestResultsTable(table, options) {
|
function loadStockTestResultsTable(table, options) {
|
||||||
/*
|
/*
|
||||||
* Load StockItemTestResult table
|
* Load StockItemTestResult table
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var params = options.params || {};
|
function formatDate(row) {
|
||||||
|
// Function for formatting date field
|
||||||
|
var html = row.date;
|
||||||
|
|
||||||
// HTML element to setup the filtering
|
if (row.user_detail) {
|
||||||
var filterListElement = options.filterList || '#filter-list-stocktests';
|
html += `<span class='badge'>${row.user_detail.username}</span>`;
|
||||||
|
|
||||||
var filters = {};
|
|
||||||
|
|
||||||
filters = loadTableFilters("stocktests");
|
|
||||||
|
|
||||||
var original = {};
|
|
||||||
|
|
||||||
for (var key in params) {
|
|
||||||
original[key] = params[key];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setupFilterList("stocktests", table, filterListElement);
|
|
||||||
|
|
||||||
// Override the default values, or add new ones
|
|
||||||
for (var key in params) {
|
|
||||||
filters[key] = params[key];
|
|
||||||
}
|
|
||||||
|
|
||||||
table.inventreeTable({
|
|
||||||
method: 'get',
|
|
||||||
formatNoMatches: function() {
|
|
||||||
return '{% trans "No test results matching query" %}';
|
|
||||||
},
|
|
||||||
url: "{% url 'api-stock-test-result-list' %}",
|
|
||||||
queryParams: filters,
|
|
||||||
original: original,
|
|
||||||
columns: [
|
|
||||||
{
|
|
||||||
field: 'pk',
|
|
||||||
title: 'ID',
|
|
||||||
visible: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'test',
|
|
||||||
title: '{% trans "Test" %}',
|
|
||||||
sortable: true,
|
|
||||||
formatter: function(value, row) {
|
|
||||||
var html = value;
|
|
||||||
|
|
||||||
if (row.attachment_detail) {
|
if (row.attachment_detail) {
|
||||||
html += `<a href='${row.attachment_detail.attachment}'><span class='fas fa-file-alt label-right'></span></a>`;
|
html += `<a href='${row.attachment_detail.attachment}'><span class='fas fa-file-alt label-right'></span></a>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return html;
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeButtons(row, grouped) {
|
||||||
|
var html = `<div class='btn-group float-right' role='group'>`;
|
||||||
|
|
||||||
|
html += makeIconButton('fa-plus icon-green', 'button-test-add', row.test_name, '{% trans "Add test result" %}');
|
||||||
|
|
||||||
|
if (!grouped && row.result != null) {
|
||||||
|
var pk = row.pk;
|
||||||
|
html += makeIconButton('fa-edit icon-blue', 'button-test-edit', pk, '{% trans "Edit test result" %}');
|
||||||
|
html += makeIconButton('fa-trash-alt icon-red', 'button-test-delete', pk, '{% trans "Delete test result" %}');
|
||||||
|
}
|
||||||
|
|
||||||
|
html += "</div>";
|
||||||
|
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
// First, load all the test templates
|
||||||
|
table.inventreeTable({
|
||||||
|
url: "{% url 'api-part-test-template-list' %}",
|
||||||
|
method: 'get',
|
||||||
|
formatNoMatches: function() {
|
||||||
|
return "{% trans 'No test results found' %}";
|
||||||
},
|
},
|
||||||
|
queryParams: {
|
||||||
|
part: options.part,
|
||||||
|
},
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
field: 'pk',
|
||||||
|
title: 'ID',
|
||||||
|
visible: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'result',
|
field: 'test_name',
|
||||||
title: "{% trans "Result" %}",
|
title: "{% trans "Test Name" %}",
|
||||||
sortable: true,
|
sortable: true,
|
||||||
formatter: function(value) {
|
formatter: function(value, row) {
|
||||||
return passFailBadge(value);
|
var html = value;
|
||||||
|
|
||||||
|
if (row.required) {
|
||||||
|
html = `<b>${value}</b>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (row.result == null) {
|
||||||
|
html += noResultBadge();
|
||||||
|
} else {
|
||||||
|
html += passFailBadge(row.result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return html;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'value',
|
field: 'value',
|
||||||
title: "{% trans "Value" %}",
|
title: '{% trans "Value" %}',
|
||||||
sortable: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'notes',
|
field: 'notes',
|
||||||
@ -102,35 +125,98 @@ function loadStockTestResultsTable(table, options) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'date',
|
field: 'date',
|
||||||
title: '{% trans "Uploaded" %}',
|
title: '{% trans "Test Date" %}',
|
||||||
sortable: true,
|
|
||||||
formatter: function(value, row) {
|
formatter: function(value, row) {
|
||||||
var html = value;
|
return formatDate(row);
|
||||||
|
|
||||||
if (row.user_detail) {
|
|
||||||
html += `<span class='badge'>${row.user_detail.username}</span>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return html;
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'buttons',
|
field: 'buttons',
|
||||||
formatter: function(value, row) {
|
formatter: function(value, row) {
|
||||||
|
return makeButtons(row, false);
|
||||||
var pk = row.pk;
|
|
||||||
|
|
||||||
var html = `<div class='btn-group float-right' role='group'>`;
|
|
||||||
|
|
||||||
html += makeIconButton('fa-edit icon-blue', 'button-test-edit', pk, '{% trans "Edit test result" %}');
|
|
||||||
html += makeIconButton('fa-trash-alt icon-red', 'button-test-delete', pk, '{% trans "Delete test result" %}');
|
|
||||||
|
|
||||||
html += `</div>`;
|
|
||||||
|
|
||||||
return html;
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
]
|
],
|
||||||
|
groupBy: true,
|
||||||
|
groupByField: 'test_name',
|
||||||
|
groupByFormatter: function(field, id, data) {
|
||||||
|
|
||||||
|
// Extract the "latest" row (data are returned in date order from the server)
|
||||||
|
var latest = data[data.length-1];
|
||||||
|
|
||||||
|
switch (field) {
|
||||||
|
case 'test_name':
|
||||||
|
return latest.test_name + ` <i>(${data.length})</i>` + passFailBadge(latest.result);
|
||||||
|
case 'value':
|
||||||
|
return latest.value;
|
||||||
|
case 'notes':
|
||||||
|
return latest.notes;
|
||||||
|
case 'date':
|
||||||
|
return formatDate(latest);
|
||||||
|
case 'buttons':
|
||||||
|
// Buttons are done differently for grouped rows
|
||||||
|
return makeButtons(latest, true);
|
||||||
|
default:
|
||||||
|
return "---";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLoadSuccess: function(tableData) {
|
||||||
|
// Once the test template data are loaded, query for results
|
||||||
|
inventreeGet(
|
||||||
|
"{% url 'api-stock-test-result-list' %}",
|
||||||
|
{
|
||||||
|
stock_item: options.stock_item,
|
||||||
|
user_detail: true,
|
||||||
|
attachment_detail: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
success: function(data) {
|
||||||
|
|
||||||
|
// Iterate through the returned test result data, and group by test
|
||||||
|
data.forEach(function(item) {
|
||||||
|
var match = false;
|
||||||
|
var override = false;
|
||||||
|
|
||||||
|
var key = testKey(item.test);
|
||||||
|
|
||||||
|
// Try to associate this result with a test row
|
||||||
|
tableData.forEach(function(row, index) {
|
||||||
|
|
||||||
|
|
||||||
|
// The result matches the test template row
|
||||||
|
if (key == testKey(row.test_name)) {
|
||||||
|
|
||||||
|
// Force the names to be the same!
|
||||||
|
item.test_name = row.test_name;
|
||||||
|
item.required = row.required;
|
||||||
|
|
||||||
|
if (row.result == null) {
|
||||||
|
// The original row has not recorded a result - override!
|
||||||
|
tableData[index] = item;
|
||||||
|
override = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
match = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// No match could be found (this is a new test!)
|
||||||
|
if (!match) {
|
||||||
|
|
||||||
|
item.test_name = item.test;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!override) {
|
||||||
|
tableData.push(item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Finally, push the data back into the table!
|
||||||
|
table.bootstrapTable("load", tableData);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,7 +44,17 @@ function getAvailableTableFilters(tableKey) {
|
|||||||
type: 'bool',
|
type: 'bool',
|
||||||
title: "{% trans 'Test result' %}",
|
title: "{% trans 'Test result' %}",
|
||||||
},
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Filters for the 'part test template' table
|
||||||
|
if (tableKey == 'parttests') {
|
||||||
|
return {
|
||||||
|
required: {
|
||||||
|
type: 'bool',
|
||||||
|
title: "{% trans "Required" %}",
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filters for the "Build" table
|
// Filters for the "Build" table
|
||||||
|
Loading…
Reference in New Issue
Block a user