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();