mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
[PUI] Build test results (#7777)
* Skeleton for "test results" panel on build detail page * Generate table columns based on test templates * Fill out test result table in build panel * Fix for form submission with files attached - Better determination of "hasFiles" - Ignore undefined values * Add modal form to create a new test result * Add button for creating a new test result * Fix for build output table * Add extra API filtering options to BuildLine API endpoint * Improve table rendering * Adjust form fields * Account for multiple test results * Add "location" column * Docs updates * playwright tests
This commit is contained in:
parent
3cbfcc11cb
commit
97bef77d56
BIN
docs/docs/assets/images/build/build_panel_allocated_stock.png
Normal file
BIN
docs/docs/assets/images/build/build_panel_allocated_stock.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 48 KiB |
BIN
docs/docs/assets/images/build/build_panel_details.png
Normal file
BIN
docs/docs/assets/images/build/build_panel_details.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 45 KiB |
BIN
docs/docs/assets/images/build/build_panel_line_items.png
Normal file
BIN
docs/docs/assets/images/build/build_panel_line_items.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 54 KiB |
BIN
docs/docs/assets/images/build/build_panel_test_results.png
Normal file
BIN
docs/docs/assets/images/build/build_panel_test_results.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 58 KiB |
BIN
docs/docs/assets/images/build/build_panel_test_statistics.png
Normal file
BIN
docs/docs/assets/images/build/build_panel_test_statistics.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 32 KiB |
68
docs/docs/build/build.md
vendored
68
docs/docs/build/build.md
vendored
@ -103,25 +103,44 @@ For further information, refer to the [stock allocation documentation](./allocat
|
||||
|
||||
## Build Order Display
|
||||
|
||||
The detail view for a single build order provides multiple display tabs, as follows:
|
||||
The detail view for a single build order provides multiple display panels, as follows:
|
||||
|
||||
### Build Details
|
||||
|
||||
The *Build Details* tab provides an overview of the Build Order:
|
||||
The *Build Details* panel provides an overview of the Build Order:
|
||||
|
||||
{% with id="build_details", url="build/build_details.png", description="Details tab" %}
|
||||
{% with id="build_details", url="build/build_panel_details.png", description="Build details panel" %}
|
||||
{% include "img.html" %}
|
||||
{% endwith %}
|
||||
|
||||
### Line Items
|
||||
|
||||
The *Line Items* tab provides an interface to allocate required stock (as specified by the BOM) to the build:
|
||||
The *Line Items* panel displays all the line items (as defined by the [bill of materials](./bom.md)) required to complete the build order.
|
||||
|
||||
{% with id="build_allocate", url="build/build_allocate.png", description="Allocation tab" %}
|
||||
{% with id="build_allocate", url="build/build_panel_line_items.png", description="Build line items panel" %}
|
||||
{% include "img.html" %}
|
||||
{% endwith %}
|
||||
|
||||
The allocation table (as shown above) shows the stock allocation progress for this build. In the example above, there are two BOM lines, which have been partially allocated.
|
||||
The allocation table (as shown above) provides an interface to allocate required stock, and also shows the stock allocation progress for each line item in the build.
|
||||
|
||||
### Incomplete Outputs
|
||||
|
||||
The *Incomplete Outputs* panel shows the list of in-progress [build outputs](./output.md) (created stock items) associated with this build.
|
||||
|
||||
{% with id="build_outputs", url="build/build_outputs.png", description="Outputs tab" %}
|
||||
{% include "img.html" %}
|
||||
{% endwith %}
|
||||
|
||||
!!! info "Example: Build Outputs"
|
||||
In the example image above, a single output (serial number 2) has been completed, while serial numbers 1 and 4 are still in progress.
|
||||
|
||||
- Build outputs can be created from this screen, by selecting the *Create New Output* button
|
||||
- Outputs which are "in progress" can be completed or cancelled
|
||||
- Completed outputs (which are simply *stock items*) can be viewed in the stock table at the bottom of the screen
|
||||
|
||||
### Completed Outputs
|
||||
|
||||
This panel displays all the completed build outputs (stock items) which have been created by this build order:
|
||||
|
||||
### Allocated Stock
|
||||
|
||||
@ -138,28 +157,29 @@ The *Consumed Stock* tab displays all stock items which have been *consumed* by
|
||||
- [Tracked stock items](./allocate.md#tracked-stock) are consumed by specific build outputs
|
||||
- [Untracked stock items](./allocate.md#untracked-stock) are consumed by the build order
|
||||
|
||||
### Build Outputs
|
||||
|
||||
The *Build Outputs* tab shows the [build outputs](./output.md) (created stock items) associated with this build.
|
||||
|
||||
As shown below, there are separate panels for *incomplete* and *completed* build outputs.
|
||||
|
||||
{% with id="build_outputs", url="build/build_outputs.png", description="Outputs tab" %}
|
||||
{% include "img.html" %}
|
||||
{% endwith %}
|
||||
|
||||
!!! info "Example: Build Outputs"
|
||||
In the example image above, a single output (serial number 2) has been completed, while serial numbers 1 and 4 are still in progress.
|
||||
|
||||
- Build outputs can be created from this screen, by selecting the *Create New Output* button
|
||||
- Outputs which are "in progress" can be completed or cancelled
|
||||
- Completed outputs (which are simply *stock items*) can be viewed in the stock table at the bottom of the screen
|
||||
|
||||
### Child Builds
|
||||
|
||||
If there exist any build orders which are *children* of the selected build order, they are displayed in the *Child Builds* tab:
|
||||
|
||||
{% with id="build_childs", url="build/build_childs.png", description="Child builds tab" %}
|
||||
{% with id="build_childs", url="build/build_childs.png", description="Child builds panel" %}
|
||||
{% include "img.html" %}
|
||||
{% endwith %}
|
||||
|
||||
### Test Results
|
||||
|
||||
For *trackable* parts, test results can be recorded against each build output. These results are displayed in the *Test Results* panel:
|
||||
|
||||
{% with id="build_test_results", url="build/build_panel_test_results.png", description="Test Results panel" %}
|
||||
{% include "img.html" %}
|
||||
{% endwith %}
|
||||
|
||||
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.
|
||||
|
||||
### Test Statistics
|
||||
|
||||
For *trackable* parts, this panel displays a summary of the test results for all build outputs:
|
||||
|
||||
{% with id="build_test_stats", url="build/build_panel_test_statistics.png", description="Test Statistics panel" %}
|
||||
{% include "img.html" %}
|
||||
{% endwith %}
|
||||
|
||||
|
@ -54,6 +54,8 @@ The following basic options are available:
|
||||
| --- | --- | --- | --- |
|
||||
| INVENTREE_SITE_URL | site_url | Specify a fixed site URL | *Not specified* |
|
||||
| INVENTREE_DEBUG | debug | Enable [debug mode](./intro.md#debug-mode) | True |
|
||||
| INVENTREE_DEBUG_QUERYCOUNT | debug_querycount | Enable [query count logging](https://github.com/bradmontgomery/django-querycount) in the terminal | False |
|
||||
| INVENTREE_DEBUG_SHELL | debug_shell | Enable [administrator shell](https://github.com/djk2/django-admin-shell) (only in debug mode) | False |
|
||||
| INVENTREE_LOG_LEVEL | log_level | Set level of logging to terminal | WARNING |
|
||||
| INVENTREE_DB_LOGGING | db_logging | Enable logging of database messages | False |
|
||||
| INVENTREE_TIMEZONE | timezone | Server timezone | UTC |
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import textwrap
|
||||
|
||||
import requests
|
||||
import yaml
|
||||
|
@ -359,6 +359,8 @@ class BuildLineList(BuildLineEndpoint, DataExportViewMixin, ListCreateAPI):
|
||||
'unit_quantity',
|
||||
'available_stock',
|
||||
'trackable',
|
||||
'allow_variants',
|
||||
'inherited',
|
||||
]
|
||||
|
||||
ordering_field_aliases = {
|
||||
@ -368,6 +370,8 @@ class BuildLineList(BuildLineEndpoint, DataExportViewMixin, ListCreateAPI):
|
||||
'consumable': 'bom_item__consumable',
|
||||
'optional': 'bom_item__optional',
|
||||
'trackable': 'bom_item__sub_part__trackable',
|
||||
'allow_variants': 'bom_item__allow_variants',
|
||||
'inherited': 'bom_item__inherited',
|
||||
}
|
||||
|
||||
search_fields = [
|
||||
|
@ -122,11 +122,11 @@ function NameBadge({ pk, type }: { pk: string | number; type: BadgeType }) {
|
||||
case 200:
|
||||
return response.data;
|
||||
default:
|
||||
return undefined;
|
||||
return {};
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
return undefined;
|
||||
return {};
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@ -398,7 +398,7 @@ export function ApiForm({
|
||||
let value: any = data[key];
|
||||
let field_type = fields[key]?.field_type;
|
||||
|
||||
if (field_type == 'file upload') {
|
||||
if (field_type == 'file upload' && !!value) {
|
||||
hasFiles = true;
|
||||
}
|
||||
|
||||
@ -413,7 +413,9 @@ export function ApiForm({
|
||||
}
|
||||
}
|
||||
|
||||
dataForm.append(key, value);
|
||||
if (value != undefined) {
|
||||
dataForm.append(key, value);
|
||||
}
|
||||
});
|
||||
|
||||
return api({
|
||||
|
@ -28,6 +28,7 @@ import {
|
||||
useSerialNumberGenerator
|
||||
} from '../hooks/UseGenerator';
|
||||
import { apiUrl } from '../states/ApiState';
|
||||
import { useGlobalSettingsState } from '../states/SettingsState';
|
||||
|
||||
/**
|
||||
* Construct a set of fields for creating / editing a StockItem instance
|
||||
@ -932,6 +933,13 @@ export function useTestResultFields({
|
||||
// Field type for the "value" input
|
||||
const [fieldType, setFieldType] = useState<'string' | 'choice'>('string');
|
||||
|
||||
const settings = useGlobalSettingsState.getState();
|
||||
|
||||
const includeTestStation = useMemo(
|
||||
() => settings.isSet('TEST_STATION_DATA'),
|
||||
[settings]
|
||||
);
|
||||
|
||||
return useMemo(() => {
|
||||
return {
|
||||
stock_item: {
|
||||
@ -972,8 +980,15 @@ export function useTestResultFields({
|
||||
},
|
||||
attachment: {},
|
||||
notes: {},
|
||||
started_datetime: {},
|
||||
finished_datetime: {}
|
||||
started_datetime: {
|
||||
hidden: !includeTestStation
|
||||
},
|
||||
finished_datetime: {
|
||||
hidden: !includeTestStation
|
||||
},
|
||||
test_station: {
|
||||
hidden: !includeTestStation
|
||||
}
|
||||
};
|
||||
}, [choices, fieldType, partId, itemId]);
|
||||
}, [choices, fieldType, partId, itemId, includeTestStation]);
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { t } from '@lingui/macro';
|
||||
import { Grid, Skeleton, Stack } from '@mantine/core';
|
||||
import {
|
||||
IconChecklist,
|
||||
IconClipboardCheck,
|
||||
IconClipboardList,
|
||||
IconDots,
|
||||
@ -51,6 +52,7 @@ import { useUserState } from '../../states/UserState';
|
||||
import BuildAllocatedStockTable from '../../tables/build/BuildAllocatedStockTable';
|
||||
import BuildLineTable from '../../tables/build/BuildLineTable';
|
||||
import { BuildOrderTable } from '../../tables/build/BuildOrderTable';
|
||||
import BuildOrderTestTable from '../../tables/build/BuildOrderTestTable';
|
||||
import BuildOutputTable from '../../tables/build/BuildOutputTable';
|
||||
import { AttachmentTable } from '../../tables/general/AttachmentTable';
|
||||
import { StockItemTable } from '../../tables/stock/StockItemTable';
|
||||
@ -307,6 +309,17 @@ export default function BuildDetail() {
|
||||
<Skeleton />
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'test-results',
|
||||
label: t`Test Results`,
|
||||
icon: <IconChecklist />,
|
||||
hidden: !build.part_detail?.trackable,
|
||||
content: build.pk ? (
|
||||
<BuildOrderTestTable buildId={build.pk} partId={build.part} />
|
||||
) : (
|
||||
<Skeleton />
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'test-statistics',
|
||||
label: t`Test Statistics`,
|
||||
|
256
src/frontend/src/tables/build/BuildOrderTestTable.tsx
Normal file
256
src/frontend/src/tables/build/BuildOrderTestTable.tsx
Normal file
@ -0,0 +1,256 @@
|
||||
import { t } from '@lingui/macro';
|
||||
import { ActionIcon, Badge, Group, Text, Tooltip } from '@mantine/core';
|
||||
import { IconCirclePlus } from '@tabler/icons-react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { ReactNode, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { api } from '../../App';
|
||||
import { PassFailButton } from '../../components/buttons/YesNoButton';
|
||||
import { ApiFormFieldSet } from '../../components/forms/fields/ApiFormField';
|
||||
import { RenderUser } from '../../components/render/User';
|
||||
import { formatDate } from '../../defaults/formatters';
|
||||
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
||||
import { useTestResultFields } from '../../forms/StockForms';
|
||||
import { useCreateApiFormModal } from '../../hooks/UseForm';
|
||||
import { useTable } from '../../hooks/UseTable';
|
||||
import { apiUrl } from '../../states/ApiState';
|
||||
import { useUserState } from '../../states/UserState';
|
||||
import { TableColumn } from '../Column';
|
||||
import { LocationColumn } from '../ColumnRenderers';
|
||||
import { TableFilter } from '../Filter';
|
||||
import { InvenTreeTable } from '../InvenTreeTable';
|
||||
import { TableHoverCard } from '../TableHoverCard';
|
||||
|
||||
/**
|
||||
* A table which displays all "test results" for the outputs generated by a build order.
|
||||
*/
|
||||
export default function BuildOrderTestTable({
|
||||
buildId,
|
||||
partId
|
||||
}: {
|
||||
buildId: number;
|
||||
partId: number;
|
||||
}) {
|
||||
const table = useTable('build-tests');
|
||||
const user = useUserState();
|
||||
|
||||
// Fetch the test templates required for this build order
|
||||
const { data: testTemplates } = useQuery({
|
||||
queryKey: ['build-test-templates', partId, buildId],
|
||||
queryFn: async () => {
|
||||
if (!partId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return api
|
||||
.get(apiUrl(ApiEndpoints.part_test_template_list), {
|
||||
params: {
|
||||
part: partId,
|
||||
include_inherited: true,
|
||||
enabled: true,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
.then((res) => res.data)
|
||||
.catch((err) => []);
|
||||
}
|
||||
});
|
||||
|
||||
// Reload the table data whenever the set of templates changes
|
||||
useEffect(() => {
|
||||
table.refreshTable();
|
||||
}, [testTemplates]);
|
||||
|
||||
const [selectedOutput, setSelectedOutput] = useState<number>(0);
|
||||
const [selectedTemplate, setSelectedTemplate] = useState<number>(0);
|
||||
|
||||
const testResultFields: ApiFormFieldSet = useTestResultFields({
|
||||
partId: partId,
|
||||
itemId: selectedOutput
|
||||
});
|
||||
|
||||
const createTestResult = useCreateApiFormModal({
|
||||
url: apiUrl(ApiEndpoints.stock_test_result_list),
|
||||
title: t`Add Test Result`,
|
||||
fields: testResultFields,
|
||||
initialData: {
|
||||
template: selectedTemplate,
|
||||
result: true
|
||||
},
|
||||
onFormSuccess: () => table.refreshTable(),
|
||||
successMessage: t`Test result added`
|
||||
});
|
||||
|
||||
// Generate a table column for each test template
|
||||
const testColumns: TableColumn[] = useMemo(() => {
|
||||
if (!testTemplates || testTemplates.length == 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return testTemplates.map((template: any) => {
|
||||
return {
|
||||
accessor: `test_${template.pk}`,
|
||||
title: template.test_name,
|
||||
sortable: false,
|
||||
switchable: true,
|
||||
render: (record: any) => {
|
||||
let tests = record.tests || [];
|
||||
|
||||
// Find the most recent test result (highest primary key)
|
||||
let test = tests
|
||||
.filter((test: any) => test.template == template.pk)
|
||||
.sort((a: any, b: any) => b.pk - a.pk)
|
||||
.shift();
|
||||
|
||||
// No test result recorded
|
||||
if (!test || test.result === undefined) {
|
||||
return (
|
||||
<Group gap="xs" wrap="nowrap" justify="space-between">
|
||||
<Badge color="lightblue" variant="filled">{t`No Result`}</Badge>
|
||||
<Tooltip label={t`Add Test Result`}>
|
||||
<ActionIcon
|
||||
size="xs"
|
||||
color="green"
|
||||
variant="transparent"
|
||||
onClick={() => {
|
||||
setSelectedOutput(record.pk);
|
||||
setSelectedTemplate(template.pk);
|
||||
createTestResult.open();
|
||||
}}
|
||||
>
|
||||
<IconCirclePlus />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
let extra: ReactNode[] = [];
|
||||
|
||||
if (test.value) {
|
||||
extra.push(
|
||||
<Text key="value" size="sm">
|
||||
{t`Value`}: {test.value}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
if (test.notes) {
|
||||
extra.push(
|
||||
<Text key="notes" size="sm">
|
||||
{t`Notes`}: {test.notes}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
if (test.date) {
|
||||
extra.push(
|
||||
<Text key="date" size="sm">
|
||||
{t`Date`}: {formatDate(test.date)}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
if (test.user_detail) {
|
||||
extra.push(<RenderUser key="user" instance={test.user_detail} />);
|
||||
}
|
||||
|
||||
return (
|
||||
<TableHoverCard
|
||||
value={<PassFailButton value={test.result} />}
|
||||
title={template.test_name}
|
||||
extra={extra}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
});
|
||||
}, [testTemplates]);
|
||||
|
||||
const tableColumns: TableColumn[] = useMemo(() => {
|
||||
// Fixed columns
|
||||
let columns: TableColumn[] = [
|
||||
{
|
||||
accessor: 'stock',
|
||||
title: t`Build Output`,
|
||||
sortable: true,
|
||||
switchable: false,
|
||||
render: (record: any) => {
|
||||
if (record.serial) {
|
||||
return `# ${record.serial}`;
|
||||
} else {
|
||||
let extra: ReactNode[] = [];
|
||||
|
||||
if (record.batch) {
|
||||
extra.push(
|
||||
<Text key="batch" size="sm">
|
||||
{t`Batch Code`}: {record.batch}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TableHoverCard
|
||||
value={
|
||||
<Text>
|
||||
{t`Quantity`}: {record.quantity}
|
||||
</Text>
|
||||
}
|
||||
title={t`Build Output`}
|
||||
extra={extra}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
LocationColumn({
|
||||
accessor: 'location_detail'
|
||||
})
|
||||
];
|
||||
|
||||
return [...columns, ...testColumns];
|
||||
}, [testColumns]);
|
||||
|
||||
const tableFilters: TableFilter[] = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
name: 'is_building',
|
||||
label: t`In Production`,
|
||||
description: t`Show build outputs currently in production`
|
||||
}
|
||||
];
|
||||
}, []);
|
||||
|
||||
const tableActions = useMemo(() => {
|
||||
return [];
|
||||
}, []);
|
||||
|
||||
const rowActions = useCallback(
|
||||
(record: any) => {
|
||||
return [];
|
||||
},
|
||||
[user]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{createTestResult.modal}
|
||||
<InvenTreeTable
|
||||
url={apiUrl(ApiEndpoints.stock_item_list)}
|
||||
tableState={table}
|
||||
columns={tableColumns}
|
||||
props={{
|
||||
params: {
|
||||
part_detail: true,
|
||||
location_detail: true,
|
||||
tests: true,
|
||||
build: buildId
|
||||
},
|
||||
rowActions: rowActions,
|
||||
tableFilters: tableFilters,
|
||||
tableActions: tableActions
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
@ -47,9 +47,9 @@ export default function BuildOutputTable({ build }: { build: any }) {
|
||||
|
||||
// Fetch the test templates associated with the partId
|
||||
const { data: testTemplates } = useQuery({
|
||||
queryKey: ['buildoutputtests', build.part],
|
||||
queryKey: ['buildoutputtests', partId],
|
||||
queryFn: async () => {
|
||||
if (!partId) {
|
||||
if (!partId || partId < 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
@ -322,7 +322,7 @@ export default function BuildOutputTable({ build }: { build: any }) {
|
||||
}
|
||||
}
|
||||
];
|
||||
}, [buildId, partId]);
|
||||
}, [buildId, partId, testTemplates]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -24,6 +24,16 @@ test('PUI - Pages - Build Order', async ({ page }) => {
|
||||
.getByRole('cell', { name: 'R38, R39, R40, R41, R42, R43' })
|
||||
.waitFor();
|
||||
|
||||
// Check "test results"
|
||||
await page.getByRole('tab', { name: 'Test Results' }).click();
|
||||
await page.getByText('Quantity: 25').waitFor();
|
||||
await page.getByText('Continuity Checks').waitFor();
|
||||
await page
|
||||
.getByRole('row', { name: 'Quantity: 16 No location set' })
|
||||
.getByRole('button')
|
||||
.hover();
|
||||
await page.getByText('Add Test Result').waitFor();
|
||||
|
||||
// Click through to the "parent" build
|
||||
await page.getByRole('tab', { name: 'Build Details' }).click();
|
||||
await page.getByRole('link', { name: 'BO0010' }).click();
|
||||
|
Loading…
Reference in New Issue
Block a user