2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-06-15 11:35:41 +00:00

[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
This commit is contained in:
Oliver
2024-08-16 14:54:33 +10:00
committed by GitHub
parent 70a52c9385
commit 3880e6f07f
29 changed files with 151 additions and 21 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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" %}',
{

View File

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

View File

@ -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" %}',

View File

@ -55,6 +55,7 @@ export function usePartFields({
component: {},
assembly: {},
is_template: {},
testable: {},
trackable: {},
purchaseable: {},
salable: {},

View File

@ -129,6 +129,7 @@ const icons = {
shipment: IconTruckDelivery,
scheduling: IconCalendarStats,
test_templates: IconTestPipe,
test: IconTestPipe,
related_parts: IconLayersLinked,
attachments: IconPaperclip,
note: IconNotes,

View File

@ -322,7 +322,7 @@ export default function BuildDetail() {
name: 'test-results',
label: t`Test Results`,
icon: <IconChecklist />,
hidden: !build.part_detail?.trackable,
hidden: !build.part_detail?.testable,
content: build.pk ? (
<BuildOrderTestTable buildId={build.pk} partId={build.part} />
) : (
@ -341,7 +341,7 @@ export default function BuildDetail() {
}}
/>
),
hidden: !build?.part_detail?.trackable
hidden: !build?.part_detail?.testable
},
{
name: 'attachments',

View File

@ -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: <IconTestPipe />,
hidden: !part.trackable,
hidden: !part.testable,
content: part?.pk ? (
<PartTestTemplateTable partId={part?.pk} partLocked={part.locked} />
) : (
@ -685,7 +691,7 @@ export default function PartDetail() {
name: 'test_statistics',
label: t`Test Statistics`,
icon: <IconReportAnalytics />,
hidden: !part.trackable,
hidden: !part.testable,
content: part?.pk ? (
<TestStatisticsTable
params={{

View File

@ -119,7 +119,7 @@ export default function StockDetail() {
name: 'tests',
label: `Completed Tests`,
icon: 'progress',
hidden: !part?.trackable
hidden: !part?.testable
},
{
type: 'text',
@ -348,7 +348,7 @@ export default function StockDetail() {
name: 'testdata',
label: t`Test Data`,
icon: <IconChecklist />,
hidden: !stockitem?.part_detail?.trackable,
hidden: !stockitem?.part_detail?.testable,
content: stockitem?.pk ? (
<StockItemTestResultTable
itemId={stockitem.pk}

View File

@ -299,6 +299,11 @@ export function BomTable({
const tableFilters: TableFilter[] = useMemo(() => {
return [
{
name: 'sub_part_testable',
label: t`Testable Part`,
description: t`Show testable items`
},
{
name: 'sub_part_trackable',
label: t`Trackable Part`,

View File

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

View File

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

View File

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