From 3880e6f07f0683c9384084eb41ab98e221220216 Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 16 Aug 2024 14:54:33 +1000 Subject: [PATCH] [WIP] Testable parts (#7888) * Add "testable" field to the Part model - Default = False - Determines whether a particular part can have tests defined against it * Adds data migration to set default 'testable' state * Update part serializers * CUI: Update table filters * PUI: Update tables and filters * CUI: Update part detail page * PUI: Update part detail page * Update CUI * Update build pages * Update BuildLine serializer * Bump API version * Update docs * Add 'testable' to fieldset --- docs/docs/part/part.md | 4 ++ docs/docs/part/test.md | 2 +- docs/docs/part/views.md | 4 +- docs/docs/stock/test.md | 2 +- .../InvenTree/InvenTree/api_version.py | 6 ++- src/backend/InvenTree/build/api.py | 1 + src/backend/InvenTree/build/serializers.py | 2 + .../build/templates/build/build_base.html | 2 +- .../build/templates/build/detail.html | 1 + .../build/templates/build/sidebar.html | 2 +- src/backend/InvenTree/part/api.py | 18 +++++++-- .../part/migrations/0128_part_testable.py | 18 +++++++++ .../migrations/0129_auto_20240815_0214.py | 37 +++++++++++++++++++ src/backend/InvenTree/part/models.py | 7 ++++ src/backend/InvenTree/part/serializers.py | 2 + .../part/templates/part/part_sidebar.html | 2 +- .../stock/templates/stock/stock_sidebar.html | 2 +- .../templates/js/translated/build.js | 2 +- .../InvenTree/templates/js/translated/part.js | 3 ++ .../templates/js/translated/table_filters.js | 12 ++++++ src/frontend/src/forms/PartForms.tsx | 1 + src/frontend/src/functions/icons.tsx | 1 + src/frontend/src/pages/build/BuildDetail.tsx | 4 +- src/frontend/src/pages/part/PartDetail.tsx | 10 ++++- src/frontend/src/pages/stock/StockDetail.tsx | 4 +- src/frontend/src/tables/bom/BomTable.tsx | 5 +++ .../src/tables/build/BuildLineTable.tsx | 5 +++ .../src/tables/build/BuildOutputTable.tsx | 7 +++- src/frontend/src/tables/part/PartTable.tsx | 6 +++ 29 files changed, 151 insertions(+), 21 deletions(-) create mode 100644 src/backend/InvenTree/part/migrations/0128_part_testable.py create mode 100644 src/backend/InvenTree/part/migrations/0129_auto_20240815_0214.py diff --git a/docs/docs/part/part.md b/docs/docs/part/part.md index 5b39e1c77f..280d787fcb 100644 --- a/docs/docs/part/part.md +++ b/docs/docs/part/part.md @@ -47,6 +47,10 @@ If a part is designated as an *Assembly* it can be created (or built) from other If a part is designated as a *Component* it can be used as a sub-component of an *Assembly*. [Read further information about BOM management here](../build/bom.md) +### Testable + +Testable parts can have test templates defined against the part, allowing test results to be recorded against any stock items for that part. For more information on testing, refer to the [testing documentation](./test.md). + ### Trackable Trackable parts can be assigned batch numbers or serial numbers which uniquely identify a particular stock item. Trackable parts also provide other features (and restrictions) in the InvenTree ecosystem. diff --git a/docs/docs/part/test.md b/docs/docs/part/test.md index cab5779522..56e947099d 100644 --- a/docs/docs/part/test.md +++ b/docs/docs/part/test.md @@ -4,7 +4,7 @@ title: Part Test Templates ## Part Test Templates -Parts which are designated as *trackable* (meaning they can be uniquely serialized) can define templates for tests which are to be performed against individual stock items corresponding to the part. +Parts which are designated as [testable](./part.md#testable) can define templates for tests which are to be performed against individual stock items corresponding to the part. A test template defines the parameters of the test; the individual stock items can then have associated test results which correspond to a test template. diff --git a/docs/docs/part/views.md b/docs/docs/part/views.md index c574bccf7e..2371a7f26d 100644 --- a/docs/docs/part/views.md +++ b/docs/docs/part/views.md @@ -131,9 +131,9 @@ The *Scheduling* tab provides an overview of the *predicted* future availability The *Stocktake* tab provide historical stock level information, based on user-provided stocktake data. Refer to the [stocktake documentation](./stocktake.md) for further information. -### Tests +### Test Templates -If a part is marked as *trackable*, the user can define tests which must be performed on any stock items which are instances of this part. [Read more about testing](./test.md). +If a part is marked as *testable*, the user can define tests which must be performed on any stock items which are instances of this part. [Read more about testing](./test.md). ### Related Parts diff --git a/docs/docs/stock/test.md b/docs/docs/stock/test.md index c284f50f3f..1374266cf3 100644 --- a/docs/docs/stock/test.md +++ b/docs/docs/stock/test.md @@ -4,7 +4,7 @@ title: Stock Test Result ## Stock Test Result -Stock items which are associated with a *trackable* part can have associated test data - this is particularly useful for tracking unit testing / commissioning / acceptance data against a serialized stock item. +Stock items which are associated with a [testable part](../part/part.md#testable) can have associated test data - this is particularly useful for tracking unit testing / commissioning / acceptance data against a serialized stock item. The master "Part" record for the stock item can define multiple [test templates](../part/test.md), against which test data can be uploaded. Additionally, arbitrary test information can be assigned to the stock item. diff --git a/src/backend/InvenTree/InvenTree/api_version.py b/src/backend/InvenTree/InvenTree/api_version.py index 77c816cf99..c21e67059c 100644 --- a/src/backend/InvenTree/InvenTree/api_version.py +++ b/src/backend/InvenTree/InvenTree/api_version.py @@ -1,13 +1,17 @@ """InvenTree API version information.""" # InvenTree API version -INVENTREE_API_VERSION = 238 +INVENTREE_API_VERSION = 239 """Increment this API version number whenever there is a significant change to the API that any clients need to know about.""" INVENTREE_API_TEXT = """ +v239 - 2024-08-15 : https://github.com/inventree/InvenTree/pull/7888 + - Adds "testable" field to the Part model + - Adds associated filters to various API endpoints + v238 - 2024-08-14 : https://github.com/inventree/InvenTree/pull/7874 - Add "assembly" filter to BuildLine API endpoint diff --git a/src/backend/InvenTree/build/api.py b/src/backend/InvenTree/build/api.py index f1e4b4db9d..7c848281a5 100644 --- a/src/backend/InvenTree/build/api.py +++ b/src/backend/InvenTree/build/api.py @@ -292,6 +292,7 @@ class BuildLineFilter(rest_filters.FilterSet): optional = rest_filters.BooleanFilter(label=_('Optional'), field_name='bom_item__optional') assembly = rest_filters.BooleanFilter(label=_('Assembly'), field_name='bom_item__sub_part__assembly') tracked = rest_filters.BooleanFilter(label=_('Tracked'), field_name='bom_item__sub_part__trackable') + testable = rest_filters.BooleanFilter(label=_('Testable'), field_name='bom_item__sub_part__testable') allocated = rest_filters.BooleanFilter(label=_('Allocated'), method='filter_allocated') diff --git a/src/backend/InvenTree/build/serializers.py b/src/backend/InvenTree/build/serializers.py index ef73686a9b..9598daac85 100644 --- a/src/backend/InvenTree/build/serializers.py +++ b/src/backend/InvenTree/build/serializers.py @@ -1238,6 +1238,7 @@ class BuildLineSerializer(DataImportExportSerializerMixin, InvenTreeModelSeriali 'reference', 'consumable', 'optional', + 'testable', 'trackable', 'inherited', 'allow_variants', @@ -1282,6 +1283,7 @@ class BuildLineSerializer(DataImportExportSerializerMixin, InvenTreeModelSeriali reference = serializers.CharField(source='bom_item.reference', label=_('Reference'), read_only=True) consumable = serializers.BooleanField(source='bom_item.consumable', label=_('Consumable'), read_only=True) optional = serializers.BooleanField(source='bom_item.optional', label=_('Optional'), read_only=True) + testable = serializers.BooleanField(source='bom_item.sub_part.testable', label=_('Testable'), read_only=True) trackable = serializers.BooleanField(source='bom_item.sub_part.trackable', label=_('Trackable'), read_only=True) inherited = serializers.BooleanField(source='bom_item.inherited', label=_('Inherited'), read_only=True) allow_variants = serializers.BooleanField(source='bom_item.allow_variants', label=_('Allow Variants'), read_only=True) diff --git a/src/backend/InvenTree/build/templates/build/build_base.html b/src/backend/InvenTree/build/templates/build/build_base.html index 536b95c6ec..6c2760558f 100644 --- a/src/backend/InvenTree/build/templates/build/build_base.html +++ b/src/backend/InvenTree/build/templates/build/build_base.html @@ -332,7 +332,7 @@ src="{% static 'img/blank_image.png' %}" }); }); - {% if build.part.trackable > 0 %} + {% if build.part.testable %} onPanelLoad("test-statistics", function() { prepareTestStatisticsTable('build', '{% url "api-test-statistics-by-build" build.pk %}') }); diff --git a/src/backend/InvenTree/build/templates/build/detail.html b/src/backend/InvenTree/build/templates/build/detail.html index e1b2c47c61..15e0612d70 100644 --- a/src/backend/InvenTree/build/templates/build/detail.html +++ b/src/backend/InvenTree/build/templates/build/detail.html @@ -388,6 +388,7 @@ onPanelLoad('outputs', function() { source_location: {{ build.take_from.pk }}, {% endif %} tracked_parts: true, + testable: {% js_bool build.part.testable %}, trackable: {% js_bool build.part.trackable %} }; diff --git a/src/backend/InvenTree/build/templates/build/sidebar.html b/src/backend/InvenTree/build/templates/build/sidebar.html index ef3ca6f14d..978b730f77 100644 --- a/src/backend/InvenTree/build/templates/build/sidebar.html +++ b/src/backend/InvenTree/build/templates/build/sidebar.html @@ -20,7 +20,7 @@ {% include "sidebar_item.html" with label='consumed' text=text icon="fa-tasks" %} {% trans "Child Build Orders" as text %} {% include "sidebar_item.html" with label='children' text=text icon="fa-sitemap" %} -{% if build.part.trackable %} +{% if build.part.testable %} {% trans "Test Statistics" as text %} {% include "sidebar_item.html" with label='test-statistics' text=text icon="fa-chart-line" %} {% endif %} diff --git a/src/backend/InvenTree/part/api.py b/src/backend/InvenTree/part/api.py index 513048a03c..1c7fe79d4a 100644 --- a/src/backend/InvenTree/part/api.py +++ b/src/backend/InvenTree/part/api.py @@ -1157,6 +1157,8 @@ class PartFilter(rest_filters.FilterSet): trackable = rest_filters.BooleanFilter() + testable = rest_filters.BooleanFilter() + purchaseable = rest_filters.BooleanFilter() salable = rest_filters.BooleanFilter() @@ -1748,20 +1750,28 @@ class BomFilter(rest_filters.FilterSet): # Filters for linked 'part' part_active = rest_filters.BooleanFilter( - label='Master part is active', field_name='part__active' + label='Assembly part is active', field_name='part__active' ) part_trackable = rest_filters.BooleanFilter( - label='Master part is trackable', field_name='part__trackable' + label='Assembly part is trackable', field_name='part__trackable' + ) + + part_testable = rest_filters.BooleanFilter( + label=_('Assembly part is testable'), field_name='part__testable' ) # Filters for linked 'sub_part' sub_part_trackable = rest_filters.BooleanFilter( - label='Sub part is trackable', field_name='sub_part__trackable' + label='Component part is trackable', field_name='sub_part__trackable' + ) + + sub_part_testable = rest_filters.BooleanFilter( + label=_('Component part is testable'), field_name='sub_part__testable' ) sub_part_assembly = rest_filters.BooleanFilter( - label='Sub part is an assembly', field_name='sub_part__assembly' + label='Component part is an assembly', field_name='sub_part__assembly' ) available_stock = rest_filters.BooleanFilter( diff --git a/src/backend/InvenTree/part/migrations/0128_part_testable.py b/src/backend/InvenTree/part/migrations/0128_part_testable.py new file mode 100644 index 0000000000..094299a8de --- /dev/null +++ b/src/backend/InvenTree/part/migrations/0128_part_testable.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.15 on 2024-08-15 02:12 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('part', '0127_remove_partcategory_icon_partcategory__icon'), + ] + + operations = [ + migrations.AddField( + model_name='part', + name='testable', + field=models.BooleanField(default=False, help_text='Can this part have test results recorded against it?', verbose_name='Testable'), + ), + ] diff --git a/src/backend/InvenTree/part/migrations/0129_auto_20240815_0214.py b/src/backend/InvenTree/part/migrations/0129_auto_20240815_0214.py new file mode 100644 index 0000000000..0f406286a3 --- /dev/null +++ b/src/backend/InvenTree/part/migrations/0129_auto_20240815_0214.py @@ -0,0 +1,37 @@ +# Generated by Django 4.2.15 on 2024-08-15 02:14 + +from django.db import migrations + + +def set_testable(apps, schema_editor): + """Set the 'testable' status to True for certain parts. + + Prior to migration part.0128, the 'trackable' attribute + was used to determine if parts could have tests associated with them. + + However, 'trackable' comes with other restrictions + (such as requiring a unique serial number). + + So, we have added a new field 'testable' to the Part model, + which is updated in this migration to match the value of the 'trackable' field. + """ + + Part = apps.get_model('part', 'Part') + + # By default, 'testable' is False - so we only need to update parts marked as 'trackable' + trackable_parts = Part.objects.filter(trackable=True) + + if trackable_parts.count() > 0: + print(f"\nMarking {trackable_parts.count()} Part objects as 'testable'") + trackable_parts.update(testable=True) + + +class Migration(migrations.Migration): + + dependencies = [ + ('part', '0128_part_testable'), + ] + + operations = [ + migrations.RunPython(set_testable, reverse_code=migrations.RunPython.noop) + ] diff --git a/src/backend/InvenTree/part/models.py b/src/backend/InvenTree/part/models.py index 465c7f6920..baeb9d8ef4 100644 --- a/src/backend/InvenTree/part/models.py +++ b/src/backend/InvenTree/part/models.py @@ -403,6 +403,7 @@ class Part( component: Can this part be used to make other parts? purchaseable: Can this part be purchased from suppliers? trackable: Trackable parts can have unique serial numbers assigned, etc, etc + testable: Testable parts can have test results recorded against their stock items active: Is this part active? Parts are deactivated instead of being deleted locked: This part is locked and cannot be edited virtual: Is this part "virtual"? e.g. a software product or similar @@ -1166,6 +1167,12 @@ class Part( help_text=_('Does this part have tracking for unique items?'), ) + testable = models.BooleanField( + default=False, + verbose_name=_('Testable'), + help_text=_('Can this part have test results recorded against it?'), + ) + purchaseable = models.BooleanField( default=part_settings.part_purchaseable_default, verbose_name=_('Purchaseable'), diff --git a/src/backend/InvenTree/part/serializers.py b/src/backend/InvenTree/part/serializers.py index 66307ea89d..bca2aa391a 100644 --- a/src/backend/InvenTree/part/serializers.py +++ b/src/backend/InvenTree/part/serializers.py @@ -323,6 +323,7 @@ class PartBriefSerializer(InvenTree.serializers.InvenTreeModelSerializer): 'is_template', 'purchaseable', 'salable', + 'testable', 'trackable', 'virtual', 'units', @@ -673,6 +674,7 @@ class PartSerializer( 'salable', 'starred', 'thumbnail', + 'testable', 'trackable', 'units', 'variant_of', diff --git a/src/backend/InvenTree/part/templates/part/part_sidebar.html b/src/backend/InvenTree/part/templates/part/part_sidebar.html index 8579cbd5cc..0c74a77c99 100644 --- a/src/backend/InvenTree/part/templates/part/part_sidebar.html +++ b/src/backend/InvenTree/part/templates/part/part_sidebar.html @@ -50,7 +50,7 @@ {% trans "Stocktake" as text %} {% include "sidebar_item.html" with label="stocktake" text=text icon="fa-clipboard-check" %} {% endif %} -{% if part.trackable %} +{% if part.testable %} {% trans "Test Templates" as text %} {% include "sidebar_item.html" with label="test-templates" text=text icon="fa-vial" %} {% trans "Test Statistics" as text %} diff --git a/src/backend/InvenTree/stock/templates/stock/stock_sidebar.html b/src/backend/InvenTree/stock/templates/stock/stock_sidebar.html index 3032bab5bf..84a7eae3b2 100644 --- a/src/backend/InvenTree/stock/templates/stock/stock_sidebar.html +++ b/src/backend/InvenTree/stock/templates/stock/stock_sidebar.html @@ -8,7 +8,7 @@ {% trans "Allocations" as text %} {% include "sidebar_item.html" with label="allocations" text=text icon="fa-bookmark" %} {% endif %} -{% if item.part.trackable %} +{% if item.part.testable %} {% trans "Test Data" as text %} {% include "sidebar_item.html" with label='test-data' text=text icon="fa-vial" %} {% endif %} diff --git a/src/backend/InvenTree/templates/js/translated/build.js b/src/backend/InvenTree/templates/js/translated/build.js index 5901ce820c..a21e140b2f 100644 --- a/src/backend/InvenTree/templates/js/translated/build.js +++ b/src/backend/InvenTree/templates/js/translated/build.js @@ -1297,7 +1297,7 @@ function loadBuildOutputTable(build_info, options={}) { }); // Request list of required tests for the part being assembled - if (build_info.trackable) { + if (build_info.testable) { inventreeGet( '{% url "api-part-test-template-list" %}', { diff --git a/src/backend/InvenTree/templates/js/translated/part.js b/src/backend/InvenTree/templates/js/translated/part.js index 3875926c38..1407f7644a 100644 --- a/src/backend/InvenTree/templates/js/translated/part.js +++ b/src/backend/InvenTree/templates/js/translated/part.js @@ -188,6 +188,9 @@ function partFields(options={}) { default: global_settings.PART_TEMPLATE, group: 'attributes', }, + testable: { + group: 'attributes', + }, trackable: { default: global_settings.PART_TRACKABLE, group: 'attributes', diff --git a/src/backend/InvenTree/templates/js/translated/table_filters.js b/src/backend/InvenTree/templates/js/translated/table_filters.js index 71b8c8a917..bb6d049a6f 100644 --- a/src/backend/InvenTree/templates/js/translated/table_filters.js +++ b/src/backend/InvenTree/templates/js/translated/table_filters.js @@ -142,6 +142,10 @@ function getVariantsTableFilters() { type: 'bool', title: '{% trans "Virtual" %}', }, + testable: { + type: 'bool', + title: '{% trans "Testable" %}', + }, trackable: { type: 'bool', title: '{% trans "Trackable" %}', @@ -153,6 +157,10 @@ function getVariantsTableFilters() { // Return a dictionary of filters for the BOM table function getBOMTableFilters() { return { + sub_part_testable: { + type: 'bool', + title: '{% trans "Testable Part" %}', + }, sub_part_trackable: { type: 'bool', title: '{% trans "Trackable Part" %}', @@ -785,6 +793,10 @@ function getPartTableFilters() { type: 'bool', title: '{% trans "Template" %}', }, + testable: { + type: 'bool', + title: '{% trans "Testable" %}', + }, trackable: { type: 'bool', title: '{% trans "Trackable" %}', diff --git a/src/frontend/src/forms/PartForms.tsx b/src/frontend/src/forms/PartForms.tsx index 9e1fcb1257..b891d241e0 100644 --- a/src/frontend/src/forms/PartForms.tsx +++ b/src/frontend/src/forms/PartForms.tsx @@ -55,6 +55,7 @@ export function usePartFields({ component: {}, assembly: {}, is_template: {}, + testable: {}, trackable: {}, purchaseable: {}, salable: {}, diff --git a/src/frontend/src/functions/icons.tsx b/src/frontend/src/functions/icons.tsx index 5f4fd8cb4e..d7b0d3c3e4 100644 --- a/src/frontend/src/functions/icons.tsx +++ b/src/frontend/src/functions/icons.tsx @@ -129,6 +129,7 @@ const icons = { shipment: IconTruckDelivery, scheduling: IconCalendarStats, test_templates: IconTestPipe, + test: IconTestPipe, related_parts: IconLayersLinked, attachments: IconPaperclip, note: IconNotes, diff --git a/src/frontend/src/pages/build/BuildDetail.tsx b/src/frontend/src/pages/build/BuildDetail.tsx index 9bb2566000..20ca1fc9dd 100644 --- a/src/frontend/src/pages/build/BuildDetail.tsx +++ b/src/frontend/src/pages/build/BuildDetail.tsx @@ -322,7 +322,7 @@ export default function BuildDetail() { name: 'test-results', label: t`Test Results`, icon: , - hidden: !build.part_detail?.trackable, + hidden: !build.part_detail?.testable, content: build.pk ? ( ) : ( @@ -341,7 +341,7 @@ export default function BuildDetail() { }} /> ), - hidden: !build?.part_detail?.trackable + hidden: !build?.part_detail?.testable }, { name: 'attachments', diff --git a/src/frontend/src/pages/part/PartDetail.tsx b/src/frontend/src/pages/part/PartDetail.tsx index 988418e965..8fed408981 100644 --- a/src/frontend/src/pages/part/PartDetail.tsx +++ b/src/frontend/src/pages/part/PartDetail.tsx @@ -315,6 +315,12 @@ export default function PartDetail() { name: 'component', label: t`Component Part` }, + { + type: 'boolean', + name: 'testable', + label: t`Testable Part`, + icon: 'test' + }, { type: 'boolean', name: 'trackable', @@ -674,7 +680,7 @@ export default function PartDetail() { name: 'test_templates', label: t`Test Templates`, icon: , - hidden: !part.trackable, + hidden: !part.testable, content: part?.pk ? ( ) : ( @@ -685,7 +691,7 @@ export default function PartDetail() { name: 'test_statistics', label: t`Test Statistics`, icon: , - hidden: !part.trackable, + hidden: !part.testable, content: part?.pk ? ( , - hidden: !stockitem?.part_detail?.trackable, + hidden: !stockitem?.part_detail?.testable, content: stockitem?.pk ? ( { return [ + { + name: 'sub_part_testable', + label: t`Testable Part`, + description: t`Show testable items` + }, { name: 'sub_part_trackable', label: t`Trackable Part`, diff --git a/src/frontend/src/tables/build/BuildLineTable.tsx b/src/frontend/src/tables/build/BuildLineTable.tsx index 7d5487efbe..3971f46f36 100644 --- a/src/frontend/src/tables/build/BuildLineTable.tsx +++ b/src/frontend/src/tables/build/BuildLineTable.tsx @@ -61,6 +61,11 @@ export default function BuildLineTable({ label: t`Assembly`, description: t`Show assembled items` }, + { + name: 'testable', + label: t`Testable`, + description: t`Show testable items` + }, { name: 'tracked', label: t`Tracked`, diff --git a/src/frontend/src/tables/build/BuildOutputTable.tsx b/src/frontend/src/tables/build/BuildOutputTable.tsx index b1c65ddb75..873f550a69 100644 --- a/src/frontend/src/tables/build/BuildOutputTable.tsx +++ b/src/frontend/src/tables/build/BuildOutputTable.tsx @@ -47,12 +47,17 @@ export default function BuildOutputTable({ build }: { build: any }) { // Fetch the test templates associated with the partId const { data: testTemplates } = useQuery({ - queryKey: ['buildoutputtests', partId], + queryKey: ['buildoutputtests', partId, build], queryFn: async () => { if (!partId || partId < 0) { return []; } + // If the part is not testable, return an empty array + if (!build?.part_detail?.testable) { + return []; + } + return api .get(apiUrl(ApiEndpoints.part_test_template_list), { params: { diff --git a/src/frontend/src/tables/part/PartTable.tsx b/src/frontend/src/tables/part/PartTable.tsx index 49bbe87c6d..0a37f35fe4 100644 --- a/src/frontend/src/tables/part/PartTable.tsx +++ b/src/frontend/src/tables/part/PartTable.tsx @@ -203,6 +203,12 @@ function partTableFilters(): TableFilter[] { description: t`Filter by component attribute`, type: 'boolean' }, + { + name: 'testable', + label: t`Testable`, + description: t`Filter by testable attribute`, + type: 'boolean' + }, { name: 'trackable', label: t`Trackable`,