diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py
index b658577936..b819ffee73 100644
--- a/InvenTree/part/api.py
+++ b/InvenTree/part/api.py
@@ -367,6 +367,31 @@ class PartAttachmentDetail(AttachmentMixin, RetrieveUpdateDestroyAPI):
serializer_class = part_serializers.PartAttachmentSerializer
+class PartTestTemplateFilter(rest_filters.FilterSet):
+ """Custom filterset class for the PartTestTemplateList endpoint."""
+
+ class Meta:
+ """Metaclass options for this filterset."""
+
+ model = PartTestTemplate
+ fields = ['required', 'requires_value', 'requires_attachment']
+
+ part = rest_filters.ModelChoiceFilter(
+ queryset=Part.objects.filter(trackable=True),
+ label='Part',
+ field_name='part',
+ method='filter_part',
+ )
+
+ def filter_part(self, queryset, name, part):
+ """Filter by the 'part' field.
+
+ Note that for the 'part' field, we also include any parts "above" the specified part.
+ """
+ variants = part.get_ancestors(include_self=True)
+ return queryset.filter(part__in=variants)
+
+
class PartTestTemplateDetail(RetrieveUpdateDestroyAPI):
"""Detail endpoint for PartTestTemplate model."""
@@ -375,45 +400,20 @@ class PartTestTemplateDetail(RetrieveUpdateDestroyAPI):
class PartTestTemplateList(ListCreateAPI):
- """API endpoint for listing (and creating) a PartTestTemplate.
-
- TODO: Add filterset class for this view
- """
+ """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=str2bool(required))
-
- return queryset
+ filterset_class = PartTestTemplateFilter
filter_backends = SEARCH_ORDER_FILTER
+ search_fields = ['test_name', 'description']
+
+ ordering_fields = ['test_name', 'required', 'requires_value', 'requires_attachment']
+
+ ordering = 'test_name'
+
class PartThumbs(ListAPI):
"""API endpoint for retrieving information on available Part thumbnails."""
diff --git a/src/frontend/src/components/tables/ColumnRenderers.tsx b/src/frontend/src/components/tables/ColumnRenderers.tsx
index 0913f87efa..560614f07e 100644
--- a/src/frontend/src/components/tables/ColumnRenderers.tsx
+++ b/src/frontend/src/components/tables/ColumnRenderers.tsx
@@ -20,25 +20,38 @@ export function PartColumn(part: any) {
export function BooleanColumn({
accessor,
- title
+ title,
+ sortable,
+ switchable
}: {
accessor: string;
title: string;
+ sortable?: boolean;
+ switchable?: boolean;
}): TableColumn {
return {
accessor: accessor,
title: title,
- sortable: true,
+ sortable: sortable ?? true,
+ switchable: switchable ?? true,
render: (record: any) =>
};
}
-export function DescriptionColumn(): TableColumn {
+export function DescriptionColumn({
+ accessor,
+ sortable,
+ switchable
+}: {
+ accessor?: string;
+ sortable?: boolean;
+ switchable?: boolean;
+}): TableColumn {
return {
- accessor: 'description',
+ accessor: accessor ?? 'description',
title: t`Description`,
- sortable: false,
- switchable: true
+ sortable: sortable ?? false,
+ switchable: switchable ?? true
};
}
diff --git a/src/frontend/src/components/tables/company/CompanyTable.tsx b/src/frontend/src/components/tables/company/CompanyTable.tsx
index 12ca29fa84..a096497fce 100644
--- a/src/frontend/src/components/tables/company/CompanyTable.tsx
+++ b/src/frontend/src/components/tables/company/CompanyTable.tsx
@@ -44,7 +44,7 @@ export function CompanyTable({
);
}
},
- DescriptionColumn(),
+ DescriptionColumn({}),
{
accessor: 'website',
title: t`Website`,
diff --git a/src/frontend/src/components/tables/part/PartCategoryTable.tsx b/src/frontend/src/components/tables/part/PartCategoryTable.tsx
index 8ec22999c0..78b984014b 100644
--- a/src/frontend/src/components/tables/part/PartCategoryTable.tsx
+++ b/src/frontend/src/components/tables/part/PartCategoryTable.tsx
@@ -27,7 +27,7 @@ export function PartCategoryTable({ params = {} }: { params?: any }) {
sortable: true,
switchable: false
},
- DescriptionColumn(),
+ DescriptionColumn({}),
{
accessor: 'pathstring',
title: t`Path`,
diff --git a/src/frontend/src/components/tables/part/PartParameterTemplateTable.tsx b/src/frontend/src/components/tables/part/PartParameterTemplateTable.tsx
index de3fbff064..2e7df5db99 100644
--- a/src/frontend/src/components/tables/part/PartParameterTemplateTable.tsx
+++ b/src/frontend/src/components/tables/part/PartParameterTemplateTable.tsx
@@ -57,7 +57,7 @@ export default function PartParameterTemplateTable() {
title: t`Units`,
sortable: true
},
- DescriptionColumn(),
+ DescriptionColumn({}),
{
accessor: 'checkbox',
title: t`Checkbox`
diff --git a/src/frontend/src/components/tables/part/PartTable.tsx b/src/frontend/src/components/tables/part/PartTable.tsx
index 0e0bfe0a2a..9f612e4c11 100644
--- a/src/frontend/src/components/tables/part/PartTable.tsx
+++ b/src/frontend/src/components/tables/part/PartTable.tsx
@@ -45,7 +45,7 @@ function partTableColumns(): TableColumn[] {
sortable: true,
title: t`Units`
},
- DescriptionColumn(),
+ DescriptionColumn({}),
{
accessor: 'category',
title: t`Category`,
diff --git a/src/frontend/src/components/tables/part/PartTestTemplateTable.tsx b/src/frontend/src/components/tables/part/PartTestTemplateTable.tsx
new file mode 100644
index 0000000000..488b502cd8
--- /dev/null
+++ b/src/frontend/src/components/tables/part/PartTestTemplateTable.tsx
@@ -0,0 +1,149 @@
+import { t } from '@lingui/macro';
+import { useCallback, useMemo } from 'react';
+
+import { ApiPaths } from '../../../enums/ApiEndpoints';
+import { UserRoles } from '../../../enums/Roles';
+import { partTestTemplateFields } from '../../../forms/PartForms';
+import {
+ openCreateApiForm,
+ openDeleteApiForm,
+ openEditApiForm
+} from '../../../functions/forms';
+import { useTable } from '../../../hooks/UseTable';
+import { apiUrl } from '../../../states/ApiState';
+import { useUserState } from '../../../states/UserState';
+import { AddItemButton } from '../../buttons/AddItemButton';
+import { TableColumn } from '../Column';
+import { BooleanColumn, DescriptionColumn } from '../ColumnRenderers';
+import { TableFilter } from '../Filter';
+import { InvenTreeTable } from '../InvenTreeTable';
+import { RowDeleteAction, RowEditAction } from '../RowActions';
+
+export default function PartTestTemplateTable({ partId }: { partId: number }) {
+ const table = useTable('part-test-template');
+ const user = useUserState();
+
+ const tableColumns: TableColumn[] = useMemo(() => {
+ return [
+ {
+ accessor: 'test_name',
+ title: t`Test Name`,
+ switchable: false,
+ sortable: true
+ },
+ DescriptionColumn({
+ switchable: false
+ }),
+ BooleanColumn({
+ accessor: 'required',
+ title: t`Required`
+ }),
+ BooleanColumn({
+ accessor: 'requires_value',
+ title: t`Requires Value`
+ }),
+ BooleanColumn({
+ accessor: 'requires_attachment',
+ title: t`Requires Attachment`
+ })
+ ];
+ }, []);
+
+ const tableFilters: TableFilter[] = useMemo(() => {
+ return [
+ {
+ name: 'required',
+ label: t`Required`,
+ description: t`Show required tests`
+ },
+ {
+ name: 'requires_value',
+ label: t`Requires Value`,
+ description: t`Show tests that require a value`
+ },
+ {
+ name: 'requires_attachment',
+ label: t`Requires Attachment`,
+ description: t`Show tests that require an attachment`
+ }
+ ];
+ }, []);
+
+ const rowActions = useCallback(
+ (record: any) => {
+ let can_edit = user.hasChangeRole(UserRoles.part);
+ let can_delete = user.hasDeleteRole(UserRoles.part);
+
+ return [
+ RowEditAction({
+ hidden: !can_edit,
+ onClick: () => {
+ openEditApiForm({
+ url: ApiPaths.part_test_template_list,
+ pk: record.pk,
+ title: t`Edit Test Template`,
+ fields: partTestTemplateFields(),
+ successMessage: t`Template updated`,
+ onFormSuccess: table.refreshTable
+ });
+ }
+ }),
+ RowDeleteAction({
+ hidden: !can_delete,
+ onClick: () => {
+ openDeleteApiForm({
+ url: ApiPaths.part_test_template_list,
+ pk: record.pk,
+ title: t`Delete Test Template`,
+ successMessage: t`Test Template deleted`,
+ onFormSuccess: table.refreshTable
+ });
+ }
+ })
+ ];
+ },
+ [user]
+ );
+
+ const addTestTemplate = useCallback(() => {
+ let fields = partTestTemplateFields();
+
+ fields['part'].value = partId;
+
+ openCreateApiForm({
+ url: ApiPaths.part_test_template_list,
+ title: t`Create Test Template`,
+ fields: fields,
+ successMessage: t`Template created`,
+ onFormSuccess: table.refreshTable
+ });
+ }, [partId]);
+
+ const tableActions = useMemo(() => {
+ let can_add = user.hasAddRole(UserRoles.part);
+
+ return [
+
+ ];
+ }, [user]);
+
+ return (
+
+ );
+}
diff --git a/src/frontend/src/components/tables/purchasing/ManufacturerPartTable.tsx b/src/frontend/src/components/tables/purchasing/ManufacturerPartTable.tsx
index f5c5c56265..17276977ba 100644
--- a/src/frontend/src/components/tables/purchasing/ManufacturerPartTable.tsx
+++ b/src/frontend/src/components/tables/purchasing/ManufacturerPartTable.tsx
@@ -52,7 +52,7 @@ export function ManufacturerPartTable({ params }: { params: any }): ReactNode {
title: t`Manufacturer Part Number`,
sortable: true
},
- DescriptionColumn(),
+ DescriptionColumn({}),
LinkColumn()
];
}, [params]);
diff --git a/src/frontend/src/components/tables/purchasing/PurchaseOrderTable.tsx b/src/frontend/src/components/tables/purchasing/PurchaseOrderTable.tsx
index 0d9739508a..4582dbec13 100644
--- a/src/frontend/src/components/tables/purchasing/PurchaseOrderTable.tsx
+++ b/src/frontend/src/components/tables/purchasing/PurchaseOrderTable.tsx
@@ -63,7 +63,7 @@ export function PurchaseOrderTable({ params }: { params?: any }) {
switchable: false
// TODO: Display extra information if order is overdue
},
- DescriptionColumn(),
+ DescriptionColumn({}),
{
accessor: 'supplier__name',
title: t`Supplier`,
diff --git a/src/frontend/src/components/tables/purchasing/SupplierPartTable.tsx b/src/frontend/src/components/tables/purchasing/SupplierPartTable.tsx
index b8456df01e..af8000378e 100644
--- a/src/frontend/src/components/tables/purchasing/SupplierPartTable.tsx
+++ b/src/frontend/src/components/tables/purchasing/SupplierPartTable.tsx
@@ -57,7 +57,7 @@ export function SupplierPartTable({ params }: { params: any }): ReactNode {
title: t`Supplier Part`,
sortable: true
},
- DescriptionColumn(),
+ DescriptionColumn({}),
{
accessor: 'manufacturer',
diff --git a/src/frontend/src/components/tables/sales/ReturnOrderTable.tsx b/src/frontend/src/components/tables/sales/ReturnOrderTable.tsx
index 2b5b05dbe3..6d0b7420dc 100644
--- a/src/frontend/src/components/tables/sales/ReturnOrderTable.tsx
+++ b/src/frontend/src/components/tables/sales/ReturnOrderTable.tsx
@@ -76,7 +76,7 @@ export function ReturnOrderTable({ params }: { params?: any }) {
accessor: 'customer_reference',
title: t`Customer Reference`
},
- DescriptionColumn(),
+ DescriptionColumn({}),
LineItemsProgressColumn(),
StatusColumn(ModelType.returnorder),
ProjectCodeColumn(),
diff --git a/src/frontend/src/components/tables/sales/SalesOrderTable.tsx b/src/frontend/src/components/tables/sales/SalesOrderTable.tsx
index b6bd435d17..f510c8c5e2 100644
--- a/src/frontend/src/components/tables/sales/SalesOrderTable.tsx
+++ b/src/frontend/src/components/tables/sales/SalesOrderTable.tsx
@@ -80,7 +80,7 @@ export function SalesOrderTable({ params }: { params?: any }) {
accessor: 'customer_reference',
title: t`Customer Reference`
},
- DescriptionColumn(),
+ DescriptionColumn({}),
LineItemsProgressColumn(),
StatusColumn(ModelType.salesorder),
ProjectCodeColumn(),
diff --git a/src/frontend/src/components/tables/settings/ProjectCodeTable.tsx b/src/frontend/src/components/tables/settings/ProjectCodeTable.tsx
index e2f3e9dc64..9afadbe99e 100644
--- a/src/frontend/src/components/tables/settings/ProjectCodeTable.tsx
+++ b/src/frontend/src/components/tables/settings/ProjectCodeTable.tsx
@@ -32,7 +32,7 @@ export default function ProjectCodeTable() {
sortable: true,
title: t`Project Code`
},
- DescriptionColumn(),
+ DescriptionColumn({}),
ResponsibleColumn()
];
}, []);
diff --git a/src/frontend/src/components/tables/stock/StockLocationTable.tsx b/src/frontend/src/components/tables/stock/StockLocationTable.tsx
index 66513cbcbc..285f6d917a 100644
--- a/src/frontend/src/components/tables/stock/StockLocationTable.tsx
+++ b/src/frontend/src/components/tables/stock/StockLocationTable.tsx
@@ -51,7 +51,7 @@ export function StockLocationTable({ params = {} }: { params?: any }) {
title: t`Name`,
switchable: false
},
- DescriptionColumn(),
+ DescriptionColumn({}),
{
accessor: 'pathstring',
title: t`Path`,
diff --git a/src/frontend/src/enums/ApiEndpoints.tsx b/src/frontend/src/enums/ApiEndpoints.tsx
index 071d9c6f8c..d5f676c73a 100644
--- a/src/frontend/src/enums/ApiEndpoints.tsx
+++ b/src/frontend/src/enums/ApiEndpoints.tsx
@@ -58,6 +58,7 @@ export enum ApiPaths {
part_attachment_list = 'api-part-attachment-list',
part_parameter_list = 'api-part-parameter-list',
part_parameter_template_list = 'api-part-parameter-template-list',
+ part_test_template_list = 'api-part-test-template-list',
// Company URLs
company_list = 'api-company-list',
diff --git a/src/frontend/src/forms/PartForms.tsx b/src/frontend/src/forms/PartForms.tsx
index 0a46a2c8de..d3f1475993 100644
--- a/src/frontend/src/forms/PartForms.tsx
+++ b/src/frontend/src/forms/PartForms.tsx
@@ -164,3 +164,16 @@ export function partParameterTemplateFields(): ApiFormFieldSet {
checkbox: {}
};
}
+
+export function partTestTemplateFields(): ApiFormFieldSet {
+ return {
+ part: {
+ hidden: true
+ },
+ test_name: {},
+ description: {},
+ required: {},
+ requires_value: {},
+ requires_attachment: {}
+ };
+}
diff --git a/src/frontend/src/pages/part/PartDetail.tsx b/src/frontend/src/pages/part/PartDetail.tsx
index 6ef88f759f..98427f74a6 100644
--- a/src/frontend/src/pages/part/PartDetail.tsx
+++ b/src/frontend/src/pages/part/PartDetail.tsx
@@ -1,5 +1,5 @@
import { t } from '@lingui/macro';
-import { Group, LoadingOverlay, Stack, Text } from '@mantine/core';
+import { Group, LoadingOverlay, Skeleton, Stack, Text } from '@mantine/core';
import {
IconBookmarks,
IconBuilding,
@@ -44,6 +44,7 @@ import { UsedInTable } from '../../components/tables/bom/UsedInTable';
import { BuildOrderTable } from '../../components/tables/build/BuildOrderTable';
import { AttachmentTable } from '../../components/tables/general/AttachmentTable';
import { PartParameterTable } from '../../components/tables/part/PartParameterTable';
+import PartTestTemplateTable from '../../components/tables/part/PartTestTemplateTable';
import { PartVariantTable } from '../../components/tables/part/PartVariantTable';
import { RelatedPartTable } from '../../components/tables/part/RelatedPartTable';
import { ManufacturerPartTable } from '../../components/tables/purchasing/ManufacturerPartTable';
@@ -189,12 +190,14 @@ export default function PartDetail() {
label: t`Sales Orders`,
icon: ,
hidden: !part.salable,
- content: part.pk && (
+ content: part.pk ? (
+ ) : (
+
)
},
{
@@ -211,7 +214,12 @@ export default function PartDetail() {
name: 'test_templates',
label: t`Test Templates`,
icon: ,
- hidden: !part.trackable
+ hidden: !part.trackable,
+ content: part?.pk ? (
+
+ ) : (
+
+ )
},
{
name: 'related_parts',
diff --git a/src/frontend/src/states/ApiState.tsx b/src/frontend/src/states/ApiState.tsx
index 0508242d2a..473e84bb6d 100644
--- a/src/frontend/src/states/ApiState.tsx
+++ b/src/frontend/src/states/ApiState.tsx
@@ -151,6 +151,8 @@ export function apiEndpoint(path: ApiPaths): string {
return 'part/related/';
case ApiPaths.part_attachment_list:
return 'part/attachment/';
+ case ApiPaths.part_test_template_list:
+ return 'part/test-template/';
case ApiPaths.company_list:
return 'company/';
case ApiPaths.contact_list: