mirror of
https://github.com/inventree/InvenTree.git
synced 2025-10-29 12:27:41 +00:00
Bulk add test results (#10146)
* Bulk creation of test results - Add BulkCreateMixin class - Add frontend support * Refactor test result serializer - Allow lookup by template name * Updated unit test * Add unit tests * Add row actions * Docs * Fix failing tests * Bump API version * Fix playwright tests
This commit is contained in:
@@ -187,6 +187,22 @@ For *trackable* parts, test results can be recorded against each build output. T
|
|||||||
|
|
||||||
This table provides a summary of the test results for each build output, and allows test results to be quickly added for each build output.
|
This table provides a summary of the test results for each build output, and allows test results to be quickly added for each build output.
|
||||||
|
|
||||||
|
### Adding Test Results
|
||||||
|
|
||||||
|
There are multiple ways to add test results against build outputs from this table view:
|
||||||
|
|
||||||
|
#### Row Actions
|
||||||
|
|
||||||
|
Open the row actions menu for a specific build output, and select the *Add Test Result* option.
|
||||||
|
|
||||||
|
#### Table Buttons
|
||||||
|
|
||||||
|
Each available test is rendered in the table as a separate column. Any output which does not already have a result registered for that test will display a button in the table cell, labelled *Add Test Result*. Clicking on this button will open the *Add Test Result* dialog for that test.
|
||||||
|
|
||||||
|
#### Bulk Add Test Results
|
||||||
|
|
||||||
|
Select the build outputs for which you wish to add test results, then click on the *Add Test Results* button at the top of the table. This will open the *Add Test Result* dialog, allowing you to select the test and enter the result for all selected outputs.
|
||||||
|
|
||||||
### Attachments
|
### Attachments
|
||||||
|
|
||||||
Files attachments can be uploaded against the build order, and displayed in the *Attachments* tab:
|
Files attachments can be uploaded against the build order, and displayed in the *Attachments* tab:
|
||||||
|
|||||||
@@ -211,7 +211,6 @@ Configuration of stock item options
|
|||||||
{{ globalsetting("STOCK_ENFORCE_BOM_INSTALLATION") }}
|
{{ globalsetting("STOCK_ENFORCE_BOM_INSTALLATION") }}
|
||||||
{{ globalsetting("STOCK_ALLOW_OUT_OF_STOCK_TRANSFER") }}
|
{{ globalsetting("STOCK_ALLOW_OUT_OF_STOCK_TRANSFER") }}
|
||||||
{{ globalsetting("TEST_STATION_DATA") }}
|
{{ globalsetting("TEST_STATION_DATA") }}
|
||||||
{{ globalsetting("TEST_UPLOAD_CREATE_TEMPLATE") }}
|
|
||||||
|
|
||||||
### Build Orders
|
### Build Orders
|
||||||
|
|
||||||
|
|||||||
@@ -474,6 +474,35 @@ class BulkOperationMixin:
|
|||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
|
class BulkCreateMixin:
|
||||||
|
"""Mixin class for enabling 'bulk create' operations for various models.
|
||||||
|
|
||||||
|
Bulk create allows for multiple items to be created in a single API query,
|
||||||
|
rather than using multiple API calls to same endpoint.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def create(self, request, *args, **kwargs):
|
||||||
|
"""Perform a POST operation against this list endpoint."""
|
||||||
|
data = request.data
|
||||||
|
|
||||||
|
if isinstance(data, list):
|
||||||
|
created_items = []
|
||||||
|
|
||||||
|
# If data is a list, we assume it is a bulk create request
|
||||||
|
if len(data) == 0:
|
||||||
|
raise ValidationError({'non_field_errors': _('No data provided')})
|
||||||
|
|
||||||
|
for item in data:
|
||||||
|
serializer = self.get_serializer(data=item)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
self.perform_create(serializer)
|
||||||
|
created_items.append(serializer.data)
|
||||||
|
|
||||||
|
return Response(created_items, status=201)
|
||||||
|
|
||||||
|
return super().create(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class BulkUpdateMixin(BulkOperationMixin):
|
class BulkUpdateMixin(BulkOperationMixin):
|
||||||
"""Mixin class for enabling 'bulk update' operations for various models.
|
"""Mixin class for enabling 'bulk update' operations for various models.
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
"""InvenTree API version information."""
|
"""InvenTree API version information."""
|
||||||
|
|
||||||
# InvenTree API version
|
# InvenTree API version
|
||||||
INVENTREE_API_VERSION = 381
|
INVENTREE_API_VERSION = 382
|
||||||
|
|
||||||
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
||||||
|
|
||||||
INVENTREE_API_TEXT = """
|
INVENTREE_API_TEXT = """
|
||||||
|
|
||||||
|
v382 -> 2025-08-07 : https://github.com/inventree/InvenTree/pull/10146
|
||||||
|
- Adds ability to "bulk create" test results via the API
|
||||||
|
- Removes legacy functionality to auto-create test result templates based on provided test names
|
||||||
|
|
||||||
v381 -> 2025-08-06 : https://github.com/inventree/InvenTree/pull/10132
|
v381 -> 2025-08-06 : https://github.com/inventree/InvenTree/pull/10132
|
||||||
- Refactor the "return stock item" API endpoint to align with other stock adjustment actions
|
- Refactor the "return stock item" API endpoint to align with other stock adjustment actions
|
||||||
|
|
||||||
|
|||||||
@@ -1124,12 +1124,4 @@ SYSTEM_SETTINGS: dict[str, InvenTreeSettingsKeyType] = {
|
|||||||
'default': False,
|
'default': False,
|
||||||
'validator': bool,
|
'validator': bool,
|
||||||
},
|
},
|
||||||
'TEST_UPLOAD_CREATE_TEMPLATE': {
|
|
||||||
'name': _('Create Template on Upload'),
|
|
||||||
'description': _(
|
|
||||||
'Create a new test template when uploading test data which does not match an existing template'
|
|
||||||
),
|
|
||||||
'default': True,
|
|
||||||
'validator': bool,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,12 @@ from company.models import Company, SupplierPart
|
|||||||
from company.serializers import CompanySerializer
|
from company.serializers import CompanySerializer
|
||||||
from data_exporter.mixins import DataExportViewMixin
|
from data_exporter.mixins import DataExportViewMixin
|
||||||
from generic.states.api import StatusView
|
from generic.states.api import StatusView
|
||||||
from InvenTree.api import BulkUpdateMixin, ListCreateDestroyAPIView, MetadataView
|
from InvenTree.api import (
|
||||||
|
BulkCreateMixin,
|
||||||
|
BulkUpdateMixin,
|
||||||
|
ListCreateDestroyAPIView,
|
||||||
|
MetadataView,
|
||||||
|
)
|
||||||
from InvenTree.filters import (
|
from InvenTree.filters import (
|
||||||
ORDER_FILTER_ALIAS,
|
ORDER_FILTER_ALIAS,
|
||||||
SEARCH_ORDER_FILTER,
|
SEARCH_ORDER_FILTER,
|
||||||
@@ -1360,7 +1365,9 @@ class StockItemTestResultFilter(rest_filters.FilterSet):
|
|||||||
return queryset.filter(template__key=key)
|
return queryset.filter(template__key=key)
|
||||||
|
|
||||||
|
|
||||||
class StockItemTestResultList(StockItemTestResultMixin, ListCreateDestroyAPIView):
|
class StockItemTestResultList(
|
||||||
|
BulkCreateMixin, StockItemTestResultMixin, ListCreateDestroyAPIView
|
||||||
|
):
|
||||||
"""API endpoint for listing (and creating) a StockItemTestResult object."""
|
"""API endpoint for listing (and creating) a StockItemTestResult object."""
|
||||||
|
|
||||||
filterset_class = StockItemTestResultFilter
|
filterset_class = StockItemTestResultFilter
|
||||||
|
|||||||
@@ -263,15 +263,18 @@ class StockItemTestResultSerializer(
|
|||||||
"""Validate the test result data."""
|
"""Validate the test result data."""
|
||||||
stock_item = data['stock_item']
|
stock_item = data['stock_item']
|
||||||
template = data.get('template', None)
|
template = data.get('template', None)
|
||||||
|
test_name = None
|
||||||
# To support legacy API, we can accept a test name instead of a template
|
|
||||||
# In such a case, we use the test name to lookup the appropriate template
|
|
||||||
test_name = self.context['request'].data.get('test', None)
|
|
||||||
|
|
||||||
if not template and not test_name:
|
|
||||||
raise ValidationError(_('Template ID or test name must be provided'))
|
|
||||||
|
|
||||||
if not template:
|
if not template:
|
||||||
|
# To support legacy API, we can accept a test name instead of a template
|
||||||
|
# In such a case, we use the test name to lookup the appropriate template
|
||||||
|
request_data = self.context['request'].data
|
||||||
|
|
||||||
|
if type(request_data) is list and len(request_data) > 0:
|
||||||
|
request_data = request_data[0]
|
||||||
|
|
||||||
|
test_name = request_data.get('test', test_name)
|
||||||
|
|
||||||
test_key = InvenTree.helpers.generateTestKey(test_name)
|
test_key = InvenTree.helpers.generateTestKey(test_name)
|
||||||
|
|
||||||
ancestors = stock_item.part.get_ancestors(include_self=True)
|
ancestors = stock_item.part.get_ancestors(include_self=True)
|
||||||
@@ -282,16 +285,8 @@ class StockItemTestResultSerializer(
|
|||||||
).first():
|
).first():
|
||||||
data['template'] = template
|
data['template'] = template
|
||||||
|
|
||||||
elif get_global_setting('TEST_UPLOAD_CREATE_TEMPLATE', False):
|
if not template:
|
||||||
logger.debug(
|
raise ValidationError(_('Template ID or test name must be provided'))
|
||||||
"No matching test template found for '%s' - creating a new template",
|
|
||||||
test_name,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create a new test template based on the provided data
|
|
||||||
data['template'] = part_models.PartTestTemplate.objects.create(
|
|
||||||
part=stock_item.part, test_name=test_name
|
|
||||||
)
|
|
||||||
|
|
||||||
data = super().validate(data)
|
data = super().validate(data)
|
||||||
|
|
||||||
|
|||||||
@@ -1882,56 +1882,43 @@ class StockTestResultTest(StockAPITestCase):
|
|||||||
|
|
||||||
self.post(url, data={'test': 'A test', 'result': True}, expected_code=400)
|
self.post(url, data={'test': 'A test', 'result': True}, expected_code=400)
|
||||||
|
|
||||||
# This one should pass!
|
# This one should fail (no matching test template)
|
||||||
self.post(
|
self.post(
|
||||||
url,
|
url,
|
||||||
data={'test': 'A test', 'stock_item': 105, 'result': True},
|
data={'test': 'A test', 'stock_item': 105, 'result': True},
|
||||||
expected_code=201,
|
expected_code=400,
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_post(self):
|
def test_post(self):
|
||||||
"""Test creation of a new test result."""
|
"""Test creation of a new test result."""
|
||||||
url = self.get_url()
|
url = self.get_url()
|
||||||
|
|
||||||
response = self.client.get(url)
|
item = StockItem.objects.get(pk=105)
|
||||||
n = len(response.data)
|
part = item.part
|
||||||
|
|
||||||
|
# Create a new test template for this part
|
||||||
|
test_template = PartTestTemplate.objects.create(
|
||||||
|
part=part,
|
||||||
|
test_name='Checked Steam Valve',
|
||||||
|
description='Test to check the steam valve pressure',
|
||||||
|
)
|
||||||
|
|
||||||
# Test upload using test name (legacy method)
|
# Test upload using test name (legacy method)
|
||||||
# Note that a new test template will be created
|
|
||||||
data = {
|
data = {
|
||||||
'stock_item': 105,
|
'stock_item': 105,
|
||||||
'test': 'Checked Steam Valve',
|
'test': 'checkedsteamvalve',
|
||||||
'result': False,
|
'result': False,
|
||||||
'value': '150kPa',
|
'value': '150kPa',
|
||||||
'notes': 'I guess there was just too much pressure?',
|
'notes': 'I guess there was just too much pressure?',
|
||||||
}
|
}
|
||||||
|
|
||||||
# First, test with TEST_UPLOAD_CREATE_TEMPLATE set to False
|
data = self.post(url, data, expected_code=201).data
|
||||||
InvenTreeSetting.set_setting('TEST_UPLOAD_CREATE_TEMPLATE', False, self.user)
|
|
||||||
|
|
||||||
response = self.post(url, data, expected_code=400)
|
self.assertEqual(data['result'], False)
|
||||||
|
self.assertEqual(data['stock_item'], 105)
|
||||||
|
self.assertEqual(data['template'], test_template.pk)
|
||||||
|
|
||||||
# Again, with the setting enabled
|
# Test upload using template reference
|
||||||
InvenTreeSetting.set_setting('TEST_UPLOAD_CREATE_TEMPLATE', True, self.user)
|
|
||||||
|
|
||||||
response = self.post(url, data, expected_code=201)
|
|
||||||
|
|
||||||
# Check that a new test template has been created
|
|
||||||
test_template = PartTestTemplate.objects.get(key='checkedsteamvalve')
|
|
||||||
|
|
||||||
response = self.client.get(url)
|
|
||||||
self.assertEqual(len(response.data), n + 1)
|
|
||||||
|
|
||||||
# And read out again
|
|
||||||
response = self.client.get(url, data={'test': 'Checked Steam Valve'})
|
|
||||||
|
|
||||||
self.assertEqual(len(response.data), 1)
|
|
||||||
|
|
||||||
test = response.data[0]
|
|
||||||
self.assertEqual(test['value'], '150kPa')
|
|
||||||
self.assertEqual(test['user'], self.user.pk)
|
|
||||||
|
|
||||||
# Test upload using template reference (new method)
|
|
||||||
data = {
|
data = {
|
||||||
'stock_item': 105,
|
'stock_item': 105,
|
||||||
'template': test_template.pk,
|
'template': test_template.pk,
|
||||||
@@ -1941,7 +1928,7 @@ class StockTestResultTest(StockAPITestCase):
|
|||||||
|
|
||||||
response = self.post(url, data, expected_code=201)
|
response = self.post(url, data, expected_code=201)
|
||||||
|
|
||||||
# Check that a new test template has been created
|
# Check that a new test result has been created
|
||||||
self.assertEqual(test_template.test_results.all().count(), 2)
|
self.assertEqual(test_template.test_results.all().count(), 2)
|
||||||
|
|
||||||
# List test results against the template
|
# List test results against the template
|
||||||
@@ -1952,6 +1939,43 @@ class StockTestResultTest(StockAPITestCase):
|
|||||||
for item in response.data:
|
for item in response.data:
|
||||||
self.assertEqual(item['template'], test_template.pk)
|
self.assertEqual(item['template'], test_template.pk)
|
||||||
|
|
||||||
|
def test_bulk_create(self):
|
||||||
|
"""Test bulk creation of test results against the API."""
|
||||||
|
url = self.get_url()
|
||||||
|
|
||||||
|
test_template = PartTestTemplate.objects.get(pk=9)
|
||||||
|
part = test_template.part
|
||||||
|
|
||||||
|
N = test_template.test_results.count()
|
||||||
|
|
||||||
|
location = StockLocation.objects.filter(structural=False).first()
|
||||||
|
|
||||||
|
stock_items = [
|
||||||
|
StockItem.objects.create(part=part, quantity=1, location=location)
|
||||||
|
for _ in range(10)
|
||||||
|
]
|
||||||
|
|
||||||
|
# Generate data to bulk-create test results
|
||||||
|
test_data = [
|
||||||
|
{
|
||||||
|
'stock_item': item.pk,
|
||||||
|
'template': test_template.pk,
|
||||||
|
'result': True,
|
||||||
|
'value': f'Test value: {item.pk}',
|
||||||
|
}
|
||||||
|
for item in stock_items
|
||||||
|
]
|
||||||
|
|
||||||
|
data = self.post(url, data=test_data, expected_code=201).data
|
||||||
|
|
||||||
|
self.assertEqual(len(data), 10)
|
||||||
|
self.assertEqual(test_template.test_results.count(), N + 10)
|
||||||
|
|
||||||
|
for item in data:
|
||||||
|
item_id = item['stock_item']
|
||||||
|
self.assertEqual(item['template'], test_template.pk)
|
||||||
|
self.assertEqual(item['value'], f'Test value: {item_id}')
|
||||||
|
|
||||||
def test_post_bitmap(self):
|
def test_post_bitmap(self):
|
||||||
"""2021-08-25.
|
"""2021-08-25.
|
||||||
|
|
||||||
@@ -1998,18 +2022,25 @@ class StockTestResultTest(StockAPITestCase):
|
|||||||
p.testable = True
|
p.testable = True
|
||||||
p.save()
|
p.save()
|
||||||
|
|
||||||
|
# Create a test template to record test results against
|
||||||
|
test_template = PartTestTemplate.objects.create(
|
||||||
|
part=p, test_name='Test Template', description='A test template for testing'
|
||||||
|
)
|
||||||
|
|
||||||
# Create some objects (via the API)
|
# Create some objects (via the API)
|
||||||
for _ii in range(50):
|
for _ii in range(50):
|
||||||
response = self.post(
|
response = self.post(
|
||||||
url,
|
url,
|
||||||
{
|
{
|
||||||
'stock_item': stock_item.pk,
|
'stock_item': stock_item.pk,
|
||||||
'test': f'Some test {_ii}',
|
'test': test_template.key,
|
||||||
'result': True,
|
'result': True,
|
||||||
'value': 'Test result value',
|
'value': 'Test result value',
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.data['template'], test_template.pk)
|
||||||
|
|
||||||
tests.append(response.data['pk'])
|
tests.append(response.data['pk'])
|
||||||
|
|
||||||
self.assertEqual(StockItemTestResult.objects.count(), n + 50)
|
self.assertEqual(StockItemTestResult.objects.count(), n + 50)
|
||||||
|
|||||||
@@ -356,12 +356,7 @@ export function ApiForm({
|
|||||||
|
|
||||||
let hasFiles = false;
|
let hasFiles = false;
|
||||||
|
|
||||||
// Optionally pre-process the data before submitting it
|
let jsonData = { ...data };
|
||||||
if (props.processFormData) {
|
|
||||||
data = props.processFormData(data, form);
|
|
||||||
}
|
|
||||||
|
|
||||||
const jsonData = { ...data };
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
|
|
||||||
Object.keys(data).forEach((key: string) => {
|
Object.keys(data).forEach((key: string) => {
|
||||||
@@ -397,6 +392,11 @@ export function ApiForm({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Optionally pre-process the data before submitting it
|
||||||
|
if (props.processFormData) {
|
||||||
|
jsonData = props.processFormData(jsonData, form);
|
||||||
|
}
|
||||||
|
|
||||||
/* Set the timeout for the request:
|
/* Set the timeout for the request:
|
||||||
* - If a timeout is provided in the props, use that
|
* - If a timeout is provided in the props, use that
|
||||||
* - If the form contains files, use a longer timeout
|
* - If the form contains files, use a longer timeout
|
||||||
|
|||||||
@@ -209,6 +209,10 @@ export function RenderInlineModel({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (typeof suffix === 'string') {
|
||||||
|
suffix = <Text size='xs'>{suffix}</Text>;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Group gap='xs' justify='space-between' wrap='nowrap' title={tooltip}>
|
<Group gap='xs' justify='space-between' wrap='nowrap' title={tooltip}>
|
||||||
<Group gap='xs' justify='left' wrap='nowrap'>
|
<Group gap='xs' justify='left' wrap='nowrap'>
|
||||||
@@ -226,7 +230,7 @@ export function RenderInlineModel({
|
|||||||
{suffix && (
|
{suffix && (
|
||||||
<>
|
<>
|
||||||
<Space />
|
<Space />
|
||||||
<div style={{ fontSize: 'xs', lineHeight: 'xs' }}>{suffix}</div>
|
{suffix}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Group>
|
</Group>
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ export function RenderPartTestTemplate({
|
|||||||
return (
|
return (
|
||||||
<RenderInlineModel
|
<RenderInlineModel
|
||||||
primary={instance.test_name}
|
primary={instance.test_name}
|
||||||
secondary={instance.description}
|
suffix={instance.description}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -237,8 +237,7 @@ export default function SystemSettings() {
|
|||||||
'STOCK_SHOW_INSTALLED_ITEMS',
|
'STOCK_SHOW_INSTALLED_ITEMS',
|
||||||
'STOCK_ENFORCE_BOM_INSTALLATION',
|
'STOCK_ENFORCE_BOM_INSTALLATION',
|
||||||
'STOCK_ALLOW_OUT_OF_STOCK_TRANSFER',
|
'STOCK_ALLOW_OUT_OF_STOCK_TRANSFER',
|
||||||
'TEST_STATION_DATA',
|
'TEST_STATION_DATA'
|
||||||
'TEST_UPLOAD_CREATE_TEMPLATE'
|
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,16 +2,24 @@ import { t } from '@lingui/core/macro';
|
|||||||
import { ActionIcon, Badge, Group, Text, Tooltip } from '@mantine/core';
|
import { ActionIcon, Badge, Group, Text, Tooltip } from '@mantine/core';
|
||||||
import { IconCirclePlus } from '@tabler/icons-react';
|
import { IconCirclePlus } from '@tabler/icons-react';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { type ReactNode, useEffect, useMemo, useState } from 'react';
|
import {
|
||||||
|
type ReactNode,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useState
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
import { PassFailButton } from '@lib/components/YesNoButton';
|
import { PassFailButton } from '@lib/components/YesNoButton';
|
||||||
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
|
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
|
||||||
import { ModelType } from '@lib/enums/ModelType';
|
import { ModelType } from '@lib/enums/ModelType';
|
||||||
import { apiUrl } from '@lib/functions/Api';
|
import { apiUrl } from '@lib/functions/Api';
|
||||||
import { cancelEvent } from '@lib/functions/Events';
|
import { cancelEvent } from '@lib/functions/Events';
|
||||||
|
import { AddItemButton } from '@lib/index';
|
||||||
import type { TableFilter } from '@lib/types/Filters';
|
import type { TableFilter } from '@lib/types/Filters';
|
||||||
import type { ApiFormFieldSet } from '@lib/types/Forms';
|
import type { ApiFormFieldSet } from '@lib/types/Forms';
|
||||||
import type { TableColumn } from '@lib/types/Tables';
|
import type { TableColumn } from '@lib/types/Tables';
|
||||||
|
import type { UseFormReturn } from 'react-hook-form';
|
||||||
import { RenderUser } from '../../components/render/User';
|
import { RenderUser } from '../../components/render/User';
|
||||||
import { useApi } from '../../contexts/ApiContext';
|
import { useApi } from '../../contexts/ApiContext';
|
||||||
import { formatDate } from '../../defaults/formatters';
|
import { formatDate } from '../../defaults/formatters';
|
||||||
@@ -62,7 +70,9 @@ export default function BuildOrderTestTable({
|
|||||||
}, [testTemplates]);
|
}, [testTemplates]);
|
||||||
|
|
||||||
const [selectedOutput, setSelectedOutput] = useState<number>(0);
|
const [selectedOutput, setSelectedOutput] = useState<number>(0);
|
||||||
const [selectedTemplate, setSelectedTemplate] = useState<number>(0);
|
const [selectedTemplate, setSelectedTemplate] = useState<number | undefined>(
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
|
||||||
const testResultFields: ApiFormFieldSet = useTestResultFields({
|
const testResultFields: ApiFormFieldSet = useTestResultFields({
|
||||||
partId: partId,
|
partId: partId,
|
||||||
@@ -82,6 +92,48 @@ export default function BuildOrderTestTable({
|
|||||||
successMessage: t`Test result added`
|
successMessage: t`Test result added`
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const multipleTestResultFields: ApiFormFieldSet = useMemo(() => {
|
||||||
|
const fields: ApiFormFieldSet = { ...testResultFields };
|
||||||
|
|
||||||
|
// Do not allow attachment for multiple test results
|
||||||
|
delete fields.attachment;
|
||||||
|
delete fields.stock_item;
|
||||||
|
|
||||||
|
fields.template.disabled = false;
|
||||||
|
|
||||||
|
return fields;
|
||||||
|
}, [partId, testResultFields]);
|
||||||
|
|
||||||
|
const generateTestResults = useCallback(
|
||||||
|
(data: any, form: UseFormReturn) => {
|
||||||
|
// Generate a list of test results for each selected output
|
||||||
|
const results = table.selectedRecords.map((record: any) => {
|
||||||
|
return {
|
||||||
|
...data,
|
||||||
|
stock_item: record.pk
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return results;
|
||||||
|
},
|
||||||
|
[table.selectedIds]
|
||||||
|
);
|
||||||
|
|
||||||
|
const createTestResultMultiple = useCreateApiFormModal({
|
||||||
|
url: apiUrl(ApiEndpoints.stock_test_result_list),
|
||||||
|
title: t`Add Test Results`,
|
||||||
|
fields: multipleTestResultFields,
|
||||||
|
initialData: {
|
||||||
|
result: true
|
||||||
|
},
|
||||||
|
onFormSuccess: () => {
|
||||||
|
table.clearSelectedRecords();
|
||||||
|
table.refreshTable();
|
||||||
|
},
|
||||||
|
processFormData: generateTestResults,
|
||||||
|
successMessage: t`Test results added`
|
||||||
|
});
|
||||||
|
|
||||||
// Generate a table column for each test template
|
// Generate a table column for each test template
|
||||||
const testColumns: TableColumn[] = useMemo(() => {
|
const testColumns: TableColumn[] = useMemo(() => {
|
||||||
if (!testTemplates || testTemplates.length == 0) {
|
if (!testTemplates || testTemplates.length == 0) {
|
||||||
@@ -112,6 +164,7 @@ export default function BuildOrderTestTable({
|
|||||||
<ActionIcon
|
<ActionIcon
|
||||||
size='lg'
|
size='lg'
|
||||||
color='green'
|
color='green'
|
||||||
|
aria-label='add-test-result'
|
||||||
variant='transparent'
|
variant='transparent'
|
||||||
onClick={(event: any) => {
|
onClick={(event: any) => {
|
||||||
cancelEvent(event);
|
cancelEvent(event);
|
||||||
@@ -224,12 +277,37 @@ export default function BuildOrderTestTable({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const tableActions = useMemo(() => {
|
const tableActions = useMemo(() => {
|
||||||
return [];
|
return [
|
||||||
|
<AddItemButton
|
||||||
|
key='add-test-result'
|
||||||
|
tooltip={t`Add Test Result`}
|
||||||
|
disabled={!table.hasSelectedRecords}
|
||||||
|
onClick={(event: any) => {
|
||||||
|
createTestResultMultiple.open();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
];
|
||||||
|
}, [table.hasSelectedRecords]);
|
||||||
|
|
||||||
|
const rowActions = useCallback((record: any) => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
icon: <IconCirclePlus />,
|
||||||
|
color: 'green',
|
||||||
|
title: t`Add Test Result`,
|
||||||
|
onClick: (event: any) => {
|
||||||
|
setSelectedOutput(record.pk);
|
||||||
|
setSelectedTemplate(undefined);
|
||||||
|
createTestResult.open();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{createTestResult.modal}
|
{createTestResult.modal}
|
||||||
|
{createTestResultMultiple.modal}
|
||||||
<InvenTreeTable
|
<InvenTreeTable
|
||||||
url={apiUrl(ApiEndpoints.stock_item_list)}
|
url={apiUrl(ApiEndpoints.stock_item_list)}
|
||||||
tableState={table}
|
tableState={table}
|
||||||
@@ -241,6 +319,8 @@ export default function BuildOrderTestTable({
|
|||||||
tests: true,
|
tests: true,
|
||||||
build: buildId
|
build: buildId
|
||||||
},
|
},
|
||||||
|
enableSelection: true,
|
||||||
|
rowActions: rowActions,
|
||||||
tableFilters: tableFilters,
|
tableFilters: tableFilters,
|
||||||
tableActions: tableActions,
|
tableActions: tableActions,
|
||||||
modelType: ModelType.stockitem
|
modelType: ModelType.stockitem
|
||||||
|
|||||||
@@ -79,11 +79,14 @@ test('Build Order - Basic Tests', async ({ browser }) => {
|
|||||||
await loadTab(page, 'Test Results');
|
await loadTab(page, 'Test Results');
|
||||||
await page.getByText('Quantity: 25').waitFor();
|
await page.getByText('Quantity: 25').waitFor();
|
||||||
await page.getByText('Continuity Checks').waitFor();
|
await page.getByText('Continuity Checks').waitFor();
|
||||||
await page
|
|
||||||
|
const button = await page
|
||||||
.getByRole('row', { name: 'Quantity: 16' })
|
.getByRole('row', { name: 'Quantity: 16' })
|
||||||
.getByRole('button')
|
.getByLabel('add-test-result');
|
||||||
.hover();
|
|
||||||
await page.getByText('Add Test Result').waitFor();
|
await button.click();
|
||||||
|
await page.getByRole('textbox', { name: 'text-field-value' }).waitFor();
|
||||||
|
await page.getByRole('button', { name: 'Cancel' }).click();
|
||||||
|
|
||||||
// Click through to the "parent" build
|
// Click through to the "parent" build
|
||||||
await loadTab(page, 'Build Details');
|
await loadTab(page, 'Build Details');
|
||||||
|
|||||||
Reference in New Issue
Block a user