diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py
index b20eff7453..2f2b34a409 100644
--- a/InvenTree/part/models.py
+++ b/InvenTree/part/models.py
@@ -3833,6 +3833,28 @@ class PartCategoryParameterTemplate(InvenTree.models.InvenTreeMetadataModel):
return f'{self.category.name} | {self.parameter_template.name} | {self.default_value}'
return f'{self.category.name} | {self.parameter_template.name}'
+ def clean(self):
+ """Validate this PartCategoryParameterTemplate instance.
+
+ Checks the provided 'default_value', and (if not blank), ensure it is valid.
+ """
+ super().clean()
+
+ self.default_value = (
+ '' if self.default_value is None else str(self.default_value.strip())
+ )
+
+ if self.default_value and InvenTreeSetting.get_setting(
+ 'PART_PARAMETER_ENFORCE_UNITS', True, cache=False, create=False
+ ):
+ if self.parameter_template.units:
+ try:
+ InvenTree.conversion.convert_physical_value(
+ self.default_value, self.parameter_template.units
+ )
+ except ValidationError as e:
+ raise ValidationError({'default_value': e.message})
+
category = models.ForeignKey(
PartCategory,
on_delete=models.CASCADE,
diff --git a/InvenTree/part/test_api.py b/InvenTree/part/test_api.py
index 4862750ae8..216d424066 100644
--- a/InvenTree/part/test_api.py
+++ b/InvenTree/part/test_api.py
@@ -196,6 +196,11 @@ class PartCategoryAPITest(InvenTreeAPITestCase):
# Add some more category templates via the API
n = PartParameterTemplate.objects.count()
+ # Ensure validation of parameter values is disabled for these checks
+ InvenTreeSetting.set_setting(
+ 'PART_PARAMETER_ENFORCE_UNITS', False, change_user=None
+ )
+
for template in PartParameterTemplate.objects.all():
response = self.post(
url,
diff --git a/src/frontend/src/enums/ApiEndpoints.tsx b/src/frontend/src/enums/ApiEndpoints.tsx
index 5a35fa87cd..f0910099da 100644
--- a/src/frontend/src/enums/ApiEndpoints.tsx
+++ b/src/frontend/src/enums/ApiEndpoints.tsx
@@ -63,6 +63,7 @@ export enum ApiEndpoints {
part_stocktake_list = 'part/stocktake/',
category_list = 'part/category/',
category_tree = 'part/category/tree/',
+ category_parameter_list = 'part/category/parameters/',
related_part_list = 'part/related/',
part_attachment_list = 'part/attachment/',
part_test_template_list = 'part/test-template/',
diff --git a/src/frontend/src/pages/Index/Settings/AdminCenter/Index.tsx b/src/frontend/src/pages/Index/Settings/AdminCenter/Index.tsx
index 67becb3fc9..7298f76b92 100644
--- a/src/frontend/src/pages/Index/Settings/AdminCenter/Index.tsx
+++ b/src/frontend/src/pages/Index/Settings/AdminCenter/Index.tsx
@@ -9,6 +9,7 @@ import {
IconListDetails,
IconPlugConnected,
IconScale,
+ IconSitemap,
IconTemplate,
IconUsersGroup
} from '@tabler/icons-react';
@@ -52,6 +53,10 @@ const PartParameterTemplateTable = Loadable(
lazy(() => import('../../../../tables/part/PartParameterTemplateTable'))
);
+const PartCategoryTemplateTable = Loadable(
+ lazy(() => import('../../../../tables/part/PartCategoryTemplateTable'))
+);
+
const CurrencyTable = Loadable(
lazy(() => import('../../../../tables/settings/CurrencyTable'))
);
@@ -106,11 +111,17 @@ export default function AdminCenter() {
content:
},
{
- name: 'parameters',
+ name: 'part-parameters',
label: t`Part Parameters`,
icon: ,
content:
},
+ {
+ name: 'category-parameters',
+ label: t`Category Parameters`,
+ icon: ,
+ content:
+ },
{
name: 'templates',
label: t`Templates`,
diff --git a/src/frontend/src/tables/InvenTreeTable.tsx b/src/frontend/src/tables/InvenTreeTable.tsx
index 16fc9c8df3..f27299bc82 100644
--- a/src/frontend/src/tables/InvenTreeTable.tsx
+++ b/src/frontend/src/tables/InvenTreeTable.tsx
@@ -378,7 +378,7 @@ export function InvenTreeTable({
return api
.get(url, {
params: queryParams,
- timeout: 30 * 1000
+ timeout: 5 * 1000
})
.then(function (response) {
switch (response.status) {
diff --git a/src/frontend/src/tables/company/AddressTable.tsx b/src/frontend/src/tables/company/AddressTable.tsx
index 8a5d8fc598..96af3c7d9f 100644
--- a/src/frontend/src/tables/company/AddressTable.tsx
+++ b/src/frontend/src/tables/company/AddressTable.tsx
@@ -184,7 +184,7 @@ export function AddressTable({
newAddress.open()}
- disabled={!can_add}
+ hidden={!can_add}
/>
];
}, [user]);
diff --git a/src/frontend/src/tables/company/ContactTable.tsx b/src/frontend/src/tables/company/ContactTable.tsx
index f5a5ed7938..6641131e39 100644
--- a/src/frontend/src/tables/company/ContactTable.tsx
+++ b/src/frontend/src/tables/company/ContactTable.tsx
@@ -130,7 +130,7 @@ export function ContactTable({
newContact.open()}
- disabled={!can_add}
+ hidden={!can_add}
/>
];
}, [user]);
diff --git a/src/frontend/src/tables/part/PartCategoryTable.tsx b/src/frontend/src/tables/part/PartCategoryTable.tsx
index 7b698e72c3..46cbe72b2f 100644
--- a/src/frontend/src/tables/part/PartCategoryTable.tsx
+++ b/src/frontend/src/tables/part/PartCategoryTable.tsx
@@ -105,7 +105,7 @@ export function PartCategoryTable({ parentId }: { parentId?: any }) {
newCategory.open()}
- disabled={!can_add}
+ hidden={!can_add}
/>
];
}, [user]);
diff --git a/src/frontend/src/tables/part/PartCategoryTemplateTable.tsx b/src/frontend/src/tables/part/PartCategoryTemplateTable.tsx
new file mode 100644
index 0000000000..9987cc509c
--- /dev/null
+++ b/src/frontend/src/tables/part/PartCategoryTemplateTable.tsx
@@ -0,0 +1,156 @@
+import { t } from '@lingui/macro';
+import { Group, Text } from '@mantine/core';
+import { useCallback, useMemo, useState } from 'react';
+import { set } from 'react-hook-form';
+
+import { AddItemButton } from '../../components/buttons/AddItemButton';
+import { ApiFormFieldSet } from '../../components/forms/fields/ApiFormField';
+import { ApiEndpoints } from '../../enums/ApiEndpoints';
+import { UserRoles } from '../../enums/Roles';
+import {
+ useCreateApiFormModal,
+ useDeleteApiFormModal,
+ useEditApiFormModal
+} from '../../hooks/UseForm';
+import { useTable } from '../../hooks/UseTable';
+import { apiUrl } from '../../states/ApiState';
+import { useUserState } from '../../states/UserState';
+import { TableColumn } from '../Column';
+import { TableFilter } from '../Filter';
+import { InvenTreeTable } from '../InvenTreeTable';
+import { RowDeleteAction, RowEditAction } from '../RowActions';
+
+export default function PartCategoryTemplateTable({}: {}) {
+ const table = useTable('part-category-parameter-templates');
+ const user = useUserState();
+
+ const formFields: ApiFormFieldSet = useMemo(() => {
+ return {
+ category: {},
+ parameter_template: {},
+ default_value: {}
+ };
+ }, []);
+
+ const [selectedTemplate, setSelectedTemplate] = useState(0);
+
+ const newTemplate = useCreateApiFormModal({
+ url: ApiEndpoints.category_parameter_list,
+ title: t`Add Category Parameter`,
+ fields: formFields,
+ onFormSuccess: table.refreshTable
+ });
+
+ const editTemplate = useEditApiFormModal({
+ url: ApiEndpoints.category_parameter_list,
+ pk: selectedTemplate,
+ title: t`Edit Category Parameter`,
+ fields: formFields,
+ onFormSuccess: (record: any) => table.updateRecord(record)
+ });
+
+ const deleteTemplate = useDeleteApiFormModal({
+ url: ApiEndpoints.category_parameter_list,
+ pk: selectedTemplate,
+ title: t`Delete Category Parameter`,
+ onFormSuccess: table.refreshTable
+ });
+
+ const tableFilters: TableFilter[] = useMemo(() => {
+ // TODO
+ return [];
+ }, []);
+
+ const tableColumns: TableColumn[] = useMemo(() => {
+ return [
+ {
+ accessor: 'category_detail.name',
+ title: t`Category`,
+ sortable: true,
+ switchable: false
+ },
+ {
+ accessor: 'category_detail.pathstring'
+ },
+ {
+ accessor: 'parameter_template_detail.name',
+ title: t`Parameter Template`,
+ sortable: true,
+ switchable: false
+ },
+ {
+ accessor: 'default_value',
+ sortable: true,
+ switchable: false,
+ render: (record: any) => {
+ if (!record?.default_value) {
+ return '-';
+ }
+
+ let units = '';
+
+ if (record?.parameter_template_detail?.units) {
+ units = t`[${record.parameter_template_detail.units}]`;
+ }
+
+ return (
+
+ {record.default_value}
+ {units && {units}}
+
+ );
+ }
+ }
+ ];
+ }, []);
+
+ const rowActions = useCallback(
+ (record: any) => {
+ return [
+ RowEditAction({
+ hidden: !user.hasChangeRole(UserRoles.part),
+ onClick: () => {
+ setSelectedTemplate(record.pk);
+ editTemplate.open();
+ }
+ }),
+ RowDeleteAction({
+ hidden: !user.hasDeleteRole(UserRoles.part),
+ onClick: () => {
+ setSelectedTemplate(record.pk);
+ deleteTemplate.open();
+ }
+ })
+ ];
+ },
+ [user]
+ );
+
+ const tableActions = useMemo(() => {
+ return [
+ newTemplate.open()}
+ hidden={!user.hasAddRole(UserRoles.part)}
+ />
+ ];
+ }, [user]);
+
+ return (
+ <>
+ {newTemplate.modal}
+ {editTemplate.modal}
+ {deleteTemplate.modal}
+
+ >
+ );
+}
diff --git a/src/frontend/src/tables/part/PartParameterTemplateTable.tsx b/src/frontend/src/tables/part/PartParameterTemplateTable.tsx
index 13d0fbe13a..0442eb36de 100644
--- a/src/frontend/src/tables/part/PartParameterTemplateTable.tsx
+++ b/src/frontend/src/tables/part/PartParameterTemplateTable.tsx
@@ -134,7 +134,7 @@ export default function PartParameterTemplateTable() {
newTemplate.open()}
- disabled={!user.hasAddRole(UserRoles.part)}
+ hidden={!user.hasAddRole(UserRoles.part)}
/>
];
}, [user]);
diff --git a/src/frontend/src/tables/part/PartTestTemplateTable.tsx b/src/frontend/src/tables/part/PartTestTemplateTable.tsx
index ebd20c08fb..411dca9e60 100644
--- a/src/frontend/src/tables/part/PartTestTemplateTable.tsx
+++ b/src/frontend/src/tables/part/PartTestTemplateTable.tsx
@@ -190,7 +190,7 @@ export default function PartTestTemplateTable({ partId }: { partId: number }) {
newTestTemplate.open()}
- disabled={!can_add}
+ hidden={!can_add}
/>
];
}, [user]);