diff --git a/docs/docs/assets/images/build/build_panel_allocated_stock.png b/docs/docs/assets/images/build/build_panel_allocated_stock.png new file mode 100644 index 0000000000..33ab5bba0b Binary files /dev/null and b/docs/docs/assets/images/build/build_panel_allocated_stock.png differ diff --git a/docs/docs/assets/images/build/build_panel_details.png b/docs/docs/assets/images/build/build_panel_details.png new file mode 100644 index 0000000000..aa37b138d5 Binary files /dev/null and b/docs/docs/assets/images/build/build_panel_details.png differ diff --git a/docs/docs/assets/images/build/build_panel_line_items.png b/docs/docs/assets/images/build/build_panel_line_items.png new file mode 100644 index 0000000000..859e0f34b8 Binary files /dev/null and b/docs/docs/assets/images/build/build_panel_line_items.png differ diff --git a/docs/docs/assets/images/build/build_panel_test_results.png b/docs/docs/assets/images/build/build_panel_test_results.png new file mode 100644 index 0000000000..c6cee64531 Binary files /dev/null and b/docs/docs/assets/images/build/build_panel_test_results.png differ diff --git a/docs/docs/assets/images/build/build_panel_test_statistics.png b/docs/docs/assets/images/build/build_panel_test_statistics.png new file mode 100644 index 0000000000..a71edb7ab5 Binary files /dev/null and b/docs/docs/assets/images/build/build_panel_test_statistics.png differ diff --git a/docs/docs/build/build.md b/docs/docs/build/build.md index 594b98846a..f35fdcaa51 100644 --- a/docs/docs/build/build.md +++ b/docs/docs/build/build.md @@ -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 %} diff --git a/docs/docs/start/config.md b/docs/docs/start/config.md index e038c78753..5653b8a659 100644 --- a/docs/docs/start/config.md +++ b/docs/docs/start/config.md @@ -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 | diff --git a/docs/main.py b/docs/main.py index 4dcac9fcf8..11066c4057 100644 --- a/docs/main.py +++ b/docs/main.py @@ -2,6 +2,7 @@ import os import subprocess +import textwrap import requests import yaml diff --git a/src/backend/InvenTree/build/api.py b/src/backend/InvenTree/build/api.py index e21c60037d..5aacea397d 100644 --- a/src/backend/InvenTree/build/api.py +++ b/src/backend/InvenTree/build/api.py @@ -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 = [ diff --git a/src/frontend/src/components/details/Details.tsx b/src/frontend/src/components/details/Details.tsx index 2d66ad7069..f58136d628 100644 --- a/src/frontend/src/components/details/Details.tsx +++ b/src/frontend/src/components/details/Details.tsx @@ -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 {}; }); } }); diff --git a/src/frontend/src/components/forms/ApiForm.tsx b/src/frontend/src/components/forms/ApiForm.tsx index 67159e6398..609b7b2b57 100644 --- a/src/frontend/src/components/forms/ApiForm.tsx +++ b/src/frontend/src/components/forms/ApiForm.tsx @@ -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({ diff --git a/src/frontend/src/forms/StockForms.tsx b/src/frontend/src/forms/StockForms.tsx index fb44f68917..8688680579 100644 --- a/src/frontend/src/forms/StockForms.tsx +++ b/src/frontend/src/forms/StockForms.tsx @@ -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]); } diff --git a/src/frontend/src/pages/build/BuildDetail.tsx b/src/frontend/src/pages/build/BuildDetail.tsx index ea10ca0baf..550d795dcb 100644 --- a/src/frontend/src/pages/build/BuildDetail.tsx +++ b/src/frontend/src/pages/build/BuildDetail.tsx @@ -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() { ) }, + { + name: 'test-results', + label: t`Test Results`, + icon: , + hidden: !build.part_detail?.trackable, + content: build.pk ? ( + + ) : ( + + ) + }, { name: 'test-statistics', label: t`Test Statistics`, diff --git a/src/frontend/src/tables/build/BuildOrderTestTable.tsx b/src/frontend/src/tables/build/BuildOrderTestTable.tsx new file mode 100644 index 0000000000..371e7530a6 --- /dev/null +++ b/src/frontend/src/tables/build/BuildOrderTestTable.tsx @@ -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(0); + const [selectedTemplate, setSelectedTemplate] = useState(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 ( + + {t`No Result`} + + { + setSelectedOutput(record.pk); + setSelectedTemplate(template.pk); + createTestResult.open(); + }} + > + + + + + ); + } + + let extra: ReactNode[] = []; + + if (test.value) { + extra.push( + + {t`Value`}: {test.value} + + ); + } + + if (test.notes) { + extra.push( + + {t`Notes`}: {test.notes} + + ); + } + + if (test.date) { + extra.push( + + {t`Date`}: {formatDate(test.date)} + + ); + } + + if (test.user_detail) { + extra.push(); + } + + return ( + } + 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( + + {t`Batch Code`}: {record.batch} + + ); + } + + return ( + + {t`Quantity`}: {record.quantity} + + } + 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} + + + ); +} diff --git a/src/frontend/src/tables/build/BuildOutputTable.tsx b/src/frontend/src/tables/build/BuildOutputTable.tsx index b98d7a4dec..b1c65ddb75 100644 --- a/src/frontend/src/tables/build/BuildOutputTable.tsx +++ b/src/frontend/src/tables/build/BuildOutputTable.tsx @@ -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 ( <> diff --git a/src/frontend/tests/pages/pui_build.spec.ts b/src/frontend/tests/pages/pui_build.spec.ts index 93df2ca397..e7c7a3275b 100644 --- a/src/frontend/tests/pages/pui_build.spec.ts +++ b/src/frontend/tests/pages/pui_build.spec.ts @@ -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();