diff --git a/InvenTree/InvenTree/api_version.py b/InvenTree/InvenTree/api_version.py
index 4364f8e6e7..283accadde 100644
--- a/InvenTree/InvenTree/api_version.py
+++ b/InvenTree/InvenTree/api_version.py
@@ -1,11 +1,16 @@
"""InvenTree API version information."""
# InvenTree API version
-INVENTREE_API_VERSION = 171
+INVENTREE_API_VERSION = 172
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
INVENTREE_API_TEXT = """
+v172 - 2024-02-20 : https://github.com/inventree/InvenTree/pull/6526
+ - Adds "enabled" field to the PartTestTemplate API endpoint
+ - Adds "enabled" filter to the PartTestTemplate list
+ - Adds "enabled" filter to the StockItemTestResult list
+
v171 - 2024-02-19 : https://github.com/inventree/InvenTree/pull/6516
- Adds "key" as a filterable parameter to PartTestTemplate list endpoint
diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py
index 89bbe6cb39..6f4f612481 100644
--- a/InvenTree/part/api.py
+++ b/InvenTree/part/api.py
@@ -375,7 +375,7 @@ class PartTestTemplateFilter(rest_filters.FilterSet):
"""Metaclass options for this filterset."""
model = PartTestTemplate
- fields = ['required', 'requires_value', 'requires_attachment', 'key']
+ fields = ['enabled', 'key', 'required', 'requires_attachment', 'requires_value']
part = rest_filters.ModelChoiceFilter(
queryset=Part.objects.filter(trackable=True),
@@ -440,11 +440,12 @@ class PartTestTemplateList(PartTestTemplateMixin, ListCreateAPI):
search_fields = ['test_name', 'description']
ordering_fields = [
- 'test_name',
+ 'enabled',
'required',
'requires_value',
'requires_attachment',
'results',
+ 'test_name',
]
ordering = 'test_name'
diff --git a/InvenTree/part/migrations/0122_parttesttemplate_enabled.py b/InvenTree/part/migrations/0122_parttesttemplate_enabled.py
new file mode 100644
index 0000000000..f7d1fece00
--- /dev/null
+++ b/InvenTree/part/migrations/0122_parttesttemplate_enabled.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.2.10 on 2024-02-20 01:35
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('part', '0121_auto_20240207_0344'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='parttesttemplate',
+ name='enabled',
+ field=models.BooleanField(default=True, help_text='Is this test enabled?', verbose_name='Enabled'),
+ ),
+ ]
diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py
index eaa2613863..b07c320626 100644
--- a/InvenTree/part/models.py
+++ b/InvenTree/part/models.py
@@ -2124,7 +2124,7 @@ class Part(
parameter.save()
- def getTestTemplates(self, required=None, include_parent=True):
+ def getTestTemplates(self, required=None, include_parent=True, enabled=None):
"""Return a list of all test templates associated with this Part.
These are used for validation of a StockItem.
@@ -2143,6 +2143,9 @@ class Part(
if required is not None:
tests = tests.filter(required=required)
+ if enabled is not None:
+ tests = tests.filter(enabled=enabled)
+
return tests
def getTestTemplateMap(self, **kwargs):
@@ -2154,9 +2157,16 @@ class Part(
return templates
- def getRequiredTests(self):
- """Return the tests which are required by this part."""
- return self.getTestTemplates(required=True)
+ def getRequiredTests(self, include_parent=True, enabled=True):
+ """Return the tests which are required by this part.
+
+ Arguments:
+ include_parent: If True, include tests which are defined for parent parts
+ enabled: If set (either True or False), filter by template "enabled" status
+ """
+ return self.getTestTemplates(
+ required=True, enabled=enabled, include_parent=include_parent
+ )
@property
def attachment_count(self):
@@ -3466,6 +3476,10 @@ class PartTestTemplate(InvenTree.models.InvenTreeMetadataModel):
help_text=_('Enter description for this test'),
)
+ enabled = models.BooleanField(
+ default=True, verbose_name=_('Enabled'), help_text=_('Is this test enabled?')
+ )
+
required = models.BooleanField(
default=True,
verbose_name=_('Required'),
diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py
index b636bca568..36ab1ba7a3 100644
--- a/InvenTree/part/serializers.py
+++ b/InvenTree/part/serializers.py
@@ -153,6 +153,7 @@ class PartTestTemplateSerializer(InvenTree.serializers.InvenTreeModelSerializer)
'part',
'test_name',
'description',
+ 'enabled',
'required',
'requires_value',
'requires_attachment',
diff --git a/InvenTree/part/test_part.py b/InvenTree/part/test_part.py
index 2247d034ec..3523e8e50b 100644
--- a/InvenTree/part/test_part.py
+++ b/InvenTree/part/test_part.py
@@ -382,6 +382,17 @@ class TestTemplateTest(TestCase):
self.assertEqual(variant.getTestTemplates(include_parent=False).count(), 0)
self.assertEqual(variant.getTestTemplates(required=True).count(), 5)
+ # Test the 'enabled' status check
+ self.assertEqual(variant.getTestTemplates(enabled=True).count(), 6)
+ self.assertEqual(variant.getTestTemplates(enabled=False).count(), 0)
+
+ template = variant.getTestTemplates().first()
+ template.enabled = False
+ template.save()
+
+ self.assertEqual(variant.getTestTemplates(enabled=True).count(), 5)
+ self.assertEqual(variant.getTestTemplates(enabled=False).count(), 1)
+
def test_uniqueness(self):
"""Test names must be unique for this part and also parts above."""
variant = Part.objects.get(pk=10004)
diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py
index c5892acacd..a6bb0c9c41 100644
--- a/InvenTree/stock/api.py
+++ b/InvenTree/stock/api.py
@@ -1247,6 +1247,10 @@ class StockItemTestResultFilter(rest_filters.FilterSet):
label='Required', field_name='template__required'
)
+ enabled = rest_filters.BooleanFilter(
+ label='Enabled', field_name='template__enabled'
+ )
+
test = rest_filters.CharFilter(
label='Test name (case insensitive)', method='filter_test_name'
)
diff --git a/InvenTree/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js
index e8cf8ac4b7..1ac5b477da 100644
--- a/InvenTree/templates/js/translated/build.js
+++ b/InvenTree/templates/js/translated/build.js
@@ -1127,6 +1127,8 @@ function loadBuildOutputTable(build_info, options={}) {
'{% url "api-part-test-template-list" %}',
{
part: build_info.part,
+ required: true,
+ enabled: true,
},
{
async: false,
diff --git a/InvenTree/templates/js/translated/part.js b/InvenTree/templates/js/translated/part.js
index b24411f2e8..240734e958 100644
--- a/InvenTree/templates/js/translated/part.js
+++ b/InvenTree/templates/js/translated/part.js
@@ -2818,6 +2818,7 @@ function partTestTemplateFields(options={}) {
required: {},
requires_value: {},
requires_attachment: {},
+ enabled: {},
part: {
hidden: true,
}
@@ -2862,6 +2863,7 @@ function loadPartTestTemplateTable(table, options) {
field: 'pk',
title: 'ID',
visible: false,
+ switchable: false,
},
{
field: 'test_name',
@@ -2884,6 +2886,14 @@ function loadPartTestTemplateTable(table, options) {
field: 'description',
title: '{% trans "Description" %}',
},
+ {
+ field: 'enabled',
+ title: '{% trans "Enabled" %}',
+ sortable: true,
+ formatter: function(value) {
+ return yesNoLabel(value);
+ }
+ },
{
field: 'required',
title: '{% trans "Required" %}',
diff --git a/InvenTree/templates/js/translated/stock.js b/InvenTree/templates/js/translated/stock.js
index 0b442fecc1..c6a1f5f2de 100644
--- a/InvenTree/templates/js/translated/stock.js
+++ b/InvenTree/templates/js/translated/stock.js
@@ -1421,6 +1421,7 @@ function loadStockTestResultsTable(table, options) {
let params = {
part: options.part,
include_inherited: true,
+ enabled: true,
};
var filters = loadTableFilters(filterKey, params);
@@ -1558,6 +1559,7 @@ function loadStockTestResultsTable(table, options) {
var query_params = {
stock_item: options.stock_item,
user_detail: true,
+ enabled: true,
attachment_detail: true,
template_detail: false,
ordering: '-date',
diff --git a/InvenTree/templates/js/translated/table_filters.js b/InvenTree/templates/js/translated/table_filters.js
index 0ea7215c1e..91786c3e59 100644
--- a/InvenTree/templates/js/translated/table_filters.js
+++ b/InvenTree/templates/js/translated/table_filters.js
@@ -476,6 +476,10 @@ function getPartTestTemplateFilters() {
type: 'bool',
title: '{% trans "Required" %}',
},
+ enabled: {
+ type: 'bool',
+ title: '{% trans "Enabled" %}',
+ },
};
}
diff --git a/docs/docs/part/test.md b/docs/docs/part/test.md
index 286e1bd42c..cab5779522 100644
--- a/docs/docs/part/test.md
+++ b/docs/docs/part/test.md
@@ -53,6 +53,10 @@ If this flag is set, then a corresponding test result against a stock item must
If this flag is set, then a corresponding test result against a stock item must provide a file attachment uploaded.
+#### Enabled
+
+Tests can be *disabled* by setting the *enabled* flag to `False`. This can be useful if a test is no longer required, but the test template should be retained for historical purposes. Note that *deleting* a test template will also delete any associated test results. So, if a test template is no longer required, it is better to disable it rather than delete it.
+
### Test Results
Individual stock item objects can have test results associated with them which correspond to test templates. Refer to the [stock test result](../stock/test.md) documentation for further information.
diff --git a/src/frontend/src/tables/part/PartTestTemplateTable.tsx b/src/frontend/src/tables/part/PartTestTemplateTable.tsx
index 85ada56752..1a02014a1a 100644
--- a/src/frontend/src/tables/part/PartTestTemplateTable.tsx
+++ b/src/frontend/src/tables/part/PartTestTemplateTable.tsx
@@ -36,7 +36,12 @@ export default function PartTestTemplateTable({ partId }: { partId: number }) {
sortable: true,
render: (record: any) => {
return (
- {record.test_name}
+
+ {record.test_name}
+
);
}
},
@@ -52,6 +57,9 @@ export default function PartTestTemplateTable({ partId }: { partId: number }) {
DescriptionColumn({
switchable: false
}),
+ BooleanColumn({
+ accessor: 'enabled'
+ }),
BooleanColumn({
accessor: 'required'
}),
@@ -70,6 +78,10 @@ export default function PartTestTemplateTable({ partId }: { partId: number }) {
name: 'required',
description: t`Show required tests`
},
+ {
+ name: 'enabled',
+ description: t`Show enabled tests`
+ },
{
name: 'requires_value',
description: t`Show tests that require a value`
@@ -100,7 +112,8 @@ export default function PartTestTemplateTable({ partId }: { partId: number }) {
description: {},
required: {},
requires_value: {},
- requires_attachment: {}
+ requires_attachment: {},
+ enabled: {}
};
}, [user]);
diff --git a/src/frontend/src/tables/stock/StockItemTestResultTable.tsx b/src/frontend/src/tables/stock/StockItemTestResultTable.tsx
index 7cd7176385..310eebba04 100644
--- a/src/frontend/src/tables/stock/StockItemTestResultTable.tsx
+++ b/src/frontend/src/tables/stock/StockItemTestResultTable.tsx
@@ -55,7 +55,8 @@ export default function StockItemTestResultTable({
.get(apiUrl(ApiEndpoints.part_test_template_list), {
params: {
part: partId,
- include_inherited: true
+ include_inherited: true,
+ enabled: true
}
})
.then((response) => response.data)
@@ -126,12 +127,17 @@ export default function StockItemTestResultTable({
sortable: true,
render: (record: any) => {
let required = record.required ?? record.template_detail?.required;
+ let enabled = record.enabled ?? record.template_detail?.enabled;
let installed =
record.stock_item != undefined && record.stock_item != itemId;
return (
-
+
{!record.templateId && '- '}
{record.test_name ?? record.template_detail?.test_name}
@@ -419,7 +425,8 @@ export default function StockItemTestResultTable({
stock_item: itemId,
user_detail: true,
attachment_detail: true,
- template_detail: true
+ template_detail: true,
+ enabled: true
}
}}
/>