mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge branch 'master' into optimize_test_statistic_queries
This commit is contained in:
33
.github/workflows/release.yaml
vendored
33
.github/workflows/release.yaml
vendored
@ -1,13 +1,16 @@
|
||||
# Runs on releases
|
||||
|
||||
name: Publish release notes
|
||||
name: Publish release
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
stable:
|
||||
runs-on: ubuntu-latest
|
||||
name: Write release to stable branch
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
@ -28,11 +31,13 @@ jobs:
|
||||
branch: stable
|
||||
force: true
|
||||
|
||||
publish-build:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
name: Build and attest frontend
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: write
|
||||
pull-requests: write
|
||||
attestations: write
|
||||
steps:
|
||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # pin@v4.1.7
|
||||
- name: Environment Setup
|
||||
@ -43,6 +48,11 @@ jobs:
|
||||
run: cd src/frontend && yarn install
|
||||
- name: Build frontend
|
||||
run: cd src/frontend && npm run compile && npm run build
|
||||
- name: Create SBOM for frontend
|
||||
uses: anchore/sbom-action@v0
|
||||
with:
|
||||
artifact-name: frontend-build.spdx
|
||||
path: src/frontend
|
||||
- name: Write version file - SHA
|
||||
run: cd src/backend/InvenTree/web/static/web/.vite && echo "$GITHUB_SHA" > sha.txt
|
||||
- name: Write version file - TAG
|
||||
@ -51,10 +61,25 @@ jobs:
|
||||
run: |
|
||||
cd src/backend/InvenTree/web/static/web
|
||||
zip -r ../frontend-build.zip * .vite
|
||||
- uses: svenstaro/upload-release-action@04733e069f2d7f7f0b4aebc4fbdbce8613b03ccd # pin@2.9.0
|
||||
- name: Attest Build Provenance
|
||||
id: attest
|
||||
uses: actions/attest-build-provenance@v1
|
||||
with:
|
||||
subject-path: "${{ github.workspace }}/src/backend/InvenTree/web/static/frontend-build.zip"
|
||||
|
||||
- name: Upload frontend
|
||||
uses: svenstaro/upload-release-action@04733e069f2d7f7f0b4aebc4fbdbce8613b03ccd # pin@2.9.0
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
file: src/backend/InvenTree/web/static/frontend-build.zip
|
||||
asset_name: frontend-build.zip
|
||||
tag: ${{ github.ref }}
|
||||
overwrite: true
|
||||
- name: Upload Attestation
|
||||
uses: svenstaro/upload-release-action@04733e069f2d7f7f0b4aebc4fbdbce8613b03ccd # pin@2.9.0
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
asset_name: frontend-build.intoto.jsonl
|
||||
file: ${{ steps.attest.outputs.bundle-path}}
|
||||
tag: ${{ github.ref }}
|
||||
overwrite: true
|
||||
|
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
|
||||
|
@ -87,4 +87,4 @@ known_django="django"
|
||||
sections=["FUTURE","STDLIB","DJANGO","THIRDPARTY","FIRSTPARTY","LOCALFOLDER"]
|
||||
|
||||
[tool.codespell]
|
||||
ignore-words-list = ["assertIn","SME"]
|
||||
ignore-words-list = ["assertIn","SME","intoto"]
|
||||
|
@ -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 = [
|
||||
|
@ -296,15 +296,12 @@ def annotate_default_location(reference=''):
|
||||
rght__gt=OuterRef(f'{reference}rght'),
|
||||
level__lte=OuterRef(f'{reference}level'),
|
||||
parent__isnull=False,
|
||||
)
|
||||
default_location__isnull=False,
|
||||
).order_by('-level')
|
||||
|
||||
return Coalesce(
|
||||
F(f'{reference}default_location'),
|
||||
Subquery(
|
||||
subquery.order_by('-level')
|
||||
.filter(default_location__isnull=False)
|
||||
.values('default_location')
|
||||
),
|
||||
Subquery(subquery.values('default_location')[:1]),
|
||||
Value(None),
|
||||
output_field=IntegerField(),
|
||||
)
|
||||
|
@ -505,6 +505,82 @@ class PartCategoryAPITest(InvenTreeAPITestCase):
|
||||
self.assertEqual(item['parent'], parent)
|
||||
self.assertEqual(item['subcategories'], subcategories)
|
||||
|
||||
def test_part_category_default_location(self):
|
||||
"""Test default location propagation through location trees."""
|
||||
"""Making a tree structure like this:
|
||||
main
|
||||
loc 2
|
||||
sub1
|
||||
sub2
|
||||
loc 3
|
||||
sub3
|
||||
loc 4
|
||||
sub4
|
||||
sub5
|
||||
Expected behaviour:
|
||||
main parent loc: Out of test scope. Parent category data not controlled by the test
|
||||
sub1 parent loc: loc 2
|
||||
sub2 parent loc: loc 2
|
||||
sub3 parent loc: loc 3
|
||||
sub4 parent loc: loc 4
|
||||
sub5 parent loc: loc 3
|
||||
"""
|
||||
main = PartCategory.objects.create(
|
||||
name='main',
|
||||
parent=PartCategory.objects.first(),
|
||||
default_location=StockLocation.objects.get(id=2),
|
||||
)
|
||||
sub1 = PartCategory.objects.create(name='sub1', parent=main)
|
||||
sub2 = PartCategory.objects.create(
|
||||
name='sub2', parent=sub1, default_location=StockLocation.objects.get(id=3)
|
||||
)
|
||||
sub3 = PartCategory.objects.create(
|
||||
name='sub3', parent=sub2, default_location=StockLocation.objects.get(id=4)
|
||||
)
|
||||
sub4 = PartCategory.objects.create(name='sub4', parent=sub3)
|
||||
sub5 = PartCategory.objects.create(name='sub5', parent=sub2)
|
||||
part = Part.objects.create(name='test', category=sub4)
|
||||
PartCategory.objects.rebuild()
|
||||
|
||||
# This query will trigger an internal server error if annotation results are not limited to 1
|
||||
url = reverse('api-part-list')
|
||||
response = self.get(url, expected_code=200)
|
||||
|
||||
# sub1, expect main to be propagated
|
||||
url = reverse('api-part-category-detail', kwargs={'pk': sub1.pk})
|
||||
response = self.get(url, expected_code=200)
|
||||
self.assertEqual(
|
||||
response.data['parent_default_location'], main.default_location.pk
|
||||
)
|
||||
|
||||
# sub2, expect main to be propagated
|
||||
url = reverse('api-part-category-detail', kwargs={'pk': sub2.pk})
|
||||
response = self.get(url, expected_code=200)
|
||||
self.assertEqual(
|
||||
response.data['parent_default_location'], main.default_location.pk
|
||||
)
|
||||
|
||||
# sub3, expect sub2 to be propagated
|
||||
url = reverse('api-part-category-detail', kwargs={'pk': sub3.pk})
|
||||
response = self.get(url, expected_code=200)
|
||||
self.assertEqual(
|
||||
response.data['parent_default_location'], sub2.default_location.pk
|
||||
)
|
||||
|
||||
# sub4, expect sub3 to be propagated
|
||||
url = reverse('api-part-category-detail', kwargs={'pk': sub4.pk})
|
||||
response = self.get(url, expected_code=200)
|
||||
self.assertEqual(
|
||||
response.data['parent_default_location'], sub3.default_location.pk
|
||||
)
|
||||
|
||||
# sub5, expect sub2 to be propagated
|
||||
url = reverse('api-part-category-detail', kwargs={'pk': sub5.pk})
|
||||
response = self.get(url, expected_code=200)
|
||||
self.assertEqual(
|
||||
response.data['parent_default_location'], sub2.default_location.pk
|
||||
)
|
||||
|
||||
|
||||
class PartOptionsAPITest(InvenTreeAPITestCase):
|
||||
"""Tests for the various OPTIONS endpoints in the /part/ API.
|
||||
|
@ -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({
|
||||
|
@ -50,49 +50,3 @@ export function Thumbnail({
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
export function ThumbnailHoverCard({
|
||||
src,
|
||||
text,
|
||||
link = '',
|
||||
alt = t`Thumbnail`,
|
||||
size = 20
|
||||
}: {
|
||||
src: string;
|
||||
text: string;
|
||||
link?: string;
|
||||
alt?: string;
|
||||
size?: number;
|
||||
}) {
|
||||
const card = useMemo(() => {
|
||||
return (
|
||||
<Group justify="left" gap={10} wrap="nowrap">
|
||||
<Thumbnail src={src} alt={alt} size={size} />
|
||||
<Text>{text}</Text>
|
||||
</Group>
|
||||
);
|
||||
}, [src, text, alt, size]);
|
||||
|
||||
if (link) {
|
||||
return (
|
||||
<Anchor href={link} style={{ textDecoration: 'none' }}>
|
||||
{card}
|
||||
</Anchor>
|
||||
);
|
||||
}
|
||||
|
||||
return <div>{card}</div>;
|
||||
}
|
||||
|
||||
export function PartHoverCard({ part }: { part: any }) {
|
||||
return part ? (
|
||||
<ThumbnailHoverCard
|
||||
src={part.thumbnail || part.image}
|
||||
text={part.full_name}
|
||||
alt={part.description}
|
||||
link=""
|
||||
/>
|
||||
) : (
|
||||
<Skeleton />
|
||||
);
|
||||
}
|
||||
|
@ -47,6 +47,43 @@ export function useSalesOrderFields(): ApiFormFieldSet {
|
||||
}, []);
|
||||
}
|
||||
|
||||
export function useSalesOrderLineItemFields({
|
||||
customerId,
|
||||
orderId,
|
||||
create
|
||||
}: {
|
||||
customerId?: number;
|
||||
orderId?: number;
|
||||
create?: boolean;
|
||||
}): ApiFormFieldSet {
|
||||
const fields = useMemo(() => {
|
||||
return {
|
||||
order: {
|
||||
filters: {
|
||||
customer_detail: true
|
||||
},
|
||||
disabled: true,
|
||||
value: create ? orderId : undefined
|
||||
},
|
||||
part: {
|
||||
filters: {
|
||||
active: true,
|
||||
salable: true
|
||||
}
|
||||
},
|
||||
reference: {},
|
||||
quantity: {},
|
||||
sale_price: {},
|
||||
sale_price_currency: {},
|
||||
target_date: {},
|
||||
notes: {},
|
||||
link: {}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
export function useReturnOrderFields(): ApiFormFieldSet {
|
||||
return useMemo(() => {
|
||||
return {
|
||||
|
@ -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`,
|
||||
|
@ -47,6 +47,7 @@ import { useInstance } from '../../hooks/UseInstance';
|
||||
import { useUserState } from '../../states/UserState';
|
||||
import { BuildOrderTable } from '../../tables/build/BuildOrderTable';
|
||||
import { AttachmentTable } from '../../tables/general/AttachmentTable';
|
||||
import SalesOrderLineItemTable from '../../tables/sales/SalesOrderLineItemTable';
|
||||
|
||||
/**
|
||||
* Detail page for a single SalesOrder
|
||||
@ -249,7 +250,12 @@ export default function SalesOrderDetail() {
|
||||
name: 'line-items',
|
||||
label: t`Line Items`,
|
||||
icon: <IconList />,
|
||||
content: <PlaceholderPanel />
|
||||
content: (
|
||||
<SalesOrderLineItemTable
|
||||
orderId={order.pk}
|
||||
customerId={order.customer}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'pending-shipments',
|
||||
|
@ -281,7 +281,7 @@ export function InvenTreeTable<T = any>({
|
||||
// If row actions are available, add a column for them
|
||||
if (tableProps.rowActions) {
|
||||
cols.push({
|
||||
accessor: 'actions',
|
||||
accessor: '--actions--',
|
||||
title: ' ',
|
||||
hidden: false,
|
||||
switchable: false,
|
||||
@ -540,19 +540,29 @@ export function InvenTreeTable<T = any>({
|
||||
}
|
||||
});
|
||||
|
||||
// Callback when a row is clicked
|
||||
const handleRowClick = useCallback(
|
||||
// Callback when a cell is clicked
|
||||
const handleCellClick = useCallback(
|
||||
({
|
||||
event,
|
||||
record,
|
||||
index
|
||||
index,
|
||||
column,
|
||||
columnIndex
|
||||
}: {
|
||||
event: React.MouseEvent;
|
||||
record: any;
|
||||
index: number;
|
||||
column: any;
|
||||
columnIndex: number;
|
||||
}) => {
|
||||
if (props.onRowClick) {
|
||||
// If a custom row click handler is provided, use that
|
||||
// Ignore any click on the 'actions' column
|
||||
if (column.accessor == '--actions--') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (props.onCellClick) {
|
||||
props.onCellClick({ event, record, index, column, columnIndex });
|
||||
} else if (props.onRowClick) {
|
||||
props.onRowClick(record, index, event);
|
||||
} else if (tableProps.modelType) {
|
||||
const accessor = tableProps.modelField ?? 'pk';
|
||||
@ -566,7 +576,7 @@ export function InvenTreeTable<T = any>({
|
||||
}
|
||||
}
|
||||
},
|
||||
[props.onRowClick]
|
||||
[props.onRowClick, props.onCellClick]
|
||||
);
|
||||
|
||||
return (
|
||||
@ -705,8 +715,7 @@ export function InvenTreeTable<T = any>({
|
||||
noRecordsText={missingRecordsText}
|
||||
records={tableState.records}
|
||||
columns={dataColumns}
|
||||
onRowClick={handleRowClick}
|
||||
onCellClick={tableProps.onCellClick}
|
||||
onCellClick={handleCellClick}
|
||||
defaultColumnProps={{
|
||||
noWrap: true,
|
||||
textAlign: 'left',
|
||||
|
@ -2,14 +2,13 @@ import { t } from '@lingui/macro';
|
||||
import { Group, Text } from '@mantine/core';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { PartHoverCard } from '../../components/images/Thumbnail';
|
||||
import { formatDecimal } from '../../defaults/formatters';
|
||||
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
||||
import { ModelType } from '../../enums/ModelType';
|
||||
import { useTable } from '../../hooks/UseTable';
|
||||
import { apiUrl } from '../../states/ApiState';
|
||||
import { TableColumn } from '../Column';
|
||||
import { ReferenceColumn } from '../ColumnRenderers';
|
||||
import { PartColumn, ReferenceColumn } from '../ColumnRenderers';
|
||||
import { TableFilter } from '../Filter';
|
||||
import { InvenTreeTable } from '../InvenTreeTable';
|
||||
|
||||
@ -31,12 +30,14 @@ export function UsedInTable({
|
||||
accessor: 'part',
|
||||
switchable: false,
|
||||
sortable: true,
|
||||
render: (record: any) => <PartHoverCard part={record.part_detail} />
|
||||
title: t`Assembly`,
|
||||
render: (record: any) => PartColumn(record.part_detail)
|
||||
},
|
||||
{
|
||||
accessor: 'sub_part',
|
||||
sortable: true,
|
||||
render: (record: any) => <PartHoverCard part={record.sub_part_detail} />
|
||||
title: t`Component`,
|
||||
render: (record: any) => PartColumn(record.sub_part_detail)
|
||||
},
|
||||
{
|
||||
accessor: 'quantity',
|
||||
|
@ -7,7 +7,6 @@ import {
|
||||
} from '@tabler/icons-react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
import { PartHoverCard } from '../../components/images/Thumbnail';
|
||||
import { ProgressBar } from '../../components/items/ProgressBar';
|
||||
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
||||
import { ModelType } from '../../enums/ModelType';
|
||||
@ -15,7 +14,7 @@ import { useTable } from '../../hooks/UseTable';
|
||||
import { apiUrl } from '../../states/ApiState';
|
||||
import { useUserState } from '../../states/UserState';
|
||||
import { TableColumn } from '../Column';
|
||||
import { BooleanColumn } from '../ColumnRenderers';
|
||||
import { BooleanColumn, PartColumn } from '../ColumnRenderers';
|
||||
import { TableFilter } from '../Filter';
|
||||
import { InvenTreeTable } from '../InvenTreeTable';
|
||||
import { TableHoverCard } from '../TableHoverCard';
|
||||
@ -131,7 +130,7 @@ export default function BuildLineTable({ params = {} }: { params?: any }) {
|
||||
ordering: 'part',
|
||||
sortable: true,
|
||||
switchable: false,
|
||||
render: (record: any) => <PartHoverCard part={record.part_detail} />
|
||||
render: (record: any) => PartColumn(record.part_detail)
|
||||
},
|
||||
{
|
||||
accessor: 'bom_item_detail.reference',
|
||||
|
@ -2,7 +2,6 @@ import { t } from '@lingui/macro';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { AddItemButton } from '../../components/buttons/AddItemButton';
|
||||
import { PartHoverCard } from '../../components/images/Thumbnail';
|
||||
import { ProgressBar } from '../../components/items/ProgressBar';
|
||||
import { RenderUser } from '../../components/render/User';
|
||||
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
||||
@ -22,6 +21,7 @@ import { TableColumn } from '../Column';
|
||||
import {
|
||||
CreationDateColumn,
|
||||
DateColumn,
|
||||
PartColumn,
|
||||
ProjectCodeColumn,
|
||||
ReferenceColumn,
|
||||
ResponsibleColumn,
|
||||
@ -41,7 +41,7 @@ function buildOrderTableColumns(): TableColumn[] {
|
||||
accessor: 'part',
|
||||
sortable: true,
|
||||
switchable: false,
|
||||
render: (record: any) => <PartHoverCard part={record.part_detail} />
|
||||
render: (record: any) => PartColumn(record.part_detail)
|
||||
},
|
||||
{
|
||||
accessor: 'title',
|
||||
|
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 (
|
||||
<>
|
||||
|
@ -5,7 +5,6 @@ import { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import { ActionButton } from '../../components/buttons/ActionButton';
|
||||
import { AddItemButton } from '../../components/buttons/AddItemButton';
|
||||
import { Thumbnail } from '../../components/images/Thumbnail';
|
||||
import ImporterDrawer from '../../components/importer/ImporterDrawer';
|
||||
import { ProgressBar } from '../../components/items/ProgressBar';
|
||||
import { RenderStockLocation } from '../../components/render/Stock';
|
||||
@ -30,6 +29,7 @@ import {
|
||||
CurrencyColumn,
|
||||
LinkColumn,
|
||||
NoteColumn,
|
||||
PartColumn,
|
||||
ReferenceColumn,
|
||||
TargetDateColumn,
|
||||
TotalPriceColumn
|
||||
@ -124,14 +124,7 @@ export function PurchaseOrderLineItemTable({
|
||||
title: t`Internal Part`,
|
||||
sortable: true,
|
||||
switchable: false,
|
||||
render: (record: any) => {
|
||||
return (
|
||||
<Thumbnail
|
||||
text={record?.part_detail?.name}
|
||||
src={record?.part_detail?.thumbnail ?? record?.part_detail?.image}
|
||||
/>
|
||||
);
|
||||
}
|
||||
render: (record: any) => PartColumn(record.part_detail)
|
||||
},
|
||||
{
|
||||
accessor: 'description',
|
||||
|
272
src/frontend/src/tables/sales/SalesOrderLineItemTable.tsx
Normal file
272
src/frontend/src/tables/sales/SalesOrderLineItemTable.tsx
Normal file
@ -0,0 +1,272 @@
|
||||
import { t } from '@lingui/macro';
|
||||
import { Text } from '@mantine/core';
|
||||
import { IconSquareArrowRight } from '@tabler/icons-react';
|
||||
import { ReactNode, useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import { AddItemButton } from '../../components/buttons/AddItemButton';
|
||||
import { ProgressBar } from '../../components/items/ProgressBar';
|
||||
import { formatCurrency } from '../../defaults/formatters';
|
||||
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
||||
import { ModelType } from '../../enums/ModelType';
|
||||
import { UserRoles } from '../../enums/Roles';
|
||||
import { useSalesOrderLineItemFields } from '../../forms/SalesOrderForms';
|
||||
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 { DateColumn, LinkColumn, PartColumn } from '../ColumnRenderers';
|
||||
import { InvenTreeTable } from '../InvenTreeTable';
|
||||
import {
|
||||
RowDeleteAction,
|
||||
RowDuplicateAction,
|
||||
RowEditAction
|
||||
} from '../RowActions';
|
||||
import { TableHoverCard } from '../TableHoverCard';
|
||||
|
||||
export default function SalesOrderLineItemTable({
|
||||
orderId,
|
||||
customerId
|
||||
}: {
|
||||
orderId: number;
|
||||
customerId: number;
|
||||
}) {
|
||||
const user = useUserState();
|
||||
const table = useTable('sales-order-line-item');
|
||||
|
||||
const tableColumns: TableColumn[] = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
accessor: 'part',
|
||||
sortable: true,
|
||||
switchable: false,
|
||||
render: (record: any) => PartColumn(record?.part_detail)
|
||||
},
|
||||
{
|
||||
accessor: 'part_detail.IPN',
|
||||
title: t`IPN`,
|
||||
switchable: true
|
||||
},
|
||||
{
|
||||
accessor: 'part_detail.description',
|
||||
title: t`Description`,
|
||||
sortable: false,
|
||||
switchable: true
|
||||
},
|
||||
{
|
||||
accessor: 'reference',
|
||||
sortable: false,
|
||||
switchable: true
|
||||
},
|
||||
{
|
||||
accessor: 'quantity',
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
accessor: 'sale_price',
|
||||
render: (record: any) =>
|
||||
formatCurrency(record.sale_price, {
|
||||
currency: record.sale_price_currency
|
||||
})
|
||||
},
|
||||
{
|
||||
accessor: 'total_price',
|
||||
title: t`Total Price`,
|
||||
render: (record: any) =>
|
||||
formatCurrency(record.sale_price, {
|
||||
currency: record.sale_price_currency,
|
||||
multiplier: record.quantity
|
||||
})
|
||||
},
|
||||
DateColumn({
|
||||
accessor: 'target_date',
|
||||
sortable: true,
|
||||
title: t`Target Date`
|
||||
}),
|
||||
{
|
||||
accessor: 'stock',
|
||||
title: t`Available Stock`,
|
||||
render: (record: any) => {
|
||||
let part_stock = record?.available_stock ?? 0;
|
||||
let variant_stock = record?.available_variant_stock ?? 0;
|
||||
let available = part_stock + variant_stock;
|
||||
|
||||
let required = Math.max(
|
||||
record.quantity - record.allocated - record.shipped,
|
||||
0
|
||||
);
|
||||
|
||||
let color: string | undefined = undefined;
|
||||
let text: string = `${available}`;
|
||||
|
||||
let extra: ReactNode[] = [];
|
||||
|
||||
if (available <= 0) {
|
||||
color = 'red';
|
||||
text = t`No stock available`;
|
||||
} else if (available < required) {
|
||||
color = 'orange';
|
||||
}
|
||||
|
||||
if (variant_stock > 0) {
|
||||
extra.push(<Text size="sm">{t`Includes variant stock`}</Text>);
|
||||
}
|
||||
|
||||
return (
|
||||
<TableHoverCard
|
||||
value={<Text color={color}>{text}</Text>}
|
||||
extra={extra}
|
||||
title={t`Stock Information`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
accessor: 'allocated',
|
||||
render: (record: any) => (
|
||||
<ProgressBar
|
||||
progressLabel={true}
|
||||
value={record.allocated}
|
||||
maximum={record.quantity}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
accessor: 'shipped',
|
||||
render: (record: any) => (
|
||||
<ProgressBar
|
||||
progressLabel={true}
|
||||
value={record.shipped}
|
||||
maximum={record.quantity}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
accessor: 'notes'
|
||||
},
|
||||
LinkColumn({
|
||||
accessor: 'link'
|
||||
})
|
||||
];
|
||||
}, []);
|
||||
|
||||
const [selectedLine, setSelectedLine] = useState<number>(0);
|
||||
|
||||
const [initialData, setInitialData] = useState({});
|
||||
|
||||
const createLineFields = useSalesOrderLineItemFields({
|
||||
orderId: orderId,
|
||||
customerId: customerId,
|
||||
create: true
|
||||
});
|
||||
|
||||
const newLine = useCreateApiFormModal({
|
||||
url: ApiEndpoints.sales_order_line_list,
|
||||
title: t`Add Line Item`,
|
||||
fields: createLineFields,
|
||||
initialData: initialData,
|
||||
table: table
|
||||
});
|
||||
|
||||
const editLineFields = useSalesOrderLineItemFields({
|
||||
orderId: orderId,
|
||||
customerId: customerId,
|
||||
create: false
|
||||
});
|
||||
|
||||
const editLine = useEditApiFormModal({
|
||||
url: ApiEndpoints.sales_order_line_list,
|
||||
pk: selectedLine,
|
||||
title: t`Edit Line Item`,
|
||||
fields: editLineFields,
|
||||
table: table
|
||||
});
|
||||
|
||||
const deleteLine = useDeleteApiFormModal({
|
||||
url: ApiEndpoints.sales_order_line_list,
|
||||
pk: selectedLine,
|
||||
title: t`Delete Line Item`,
|
||||
table: table
|
||||
});
|
||||
|
||||
const tableActions = useMemo(() => {
|
||||
return [
|
||||
<AddItemButton
|
||||
tooltip={t`Add line item`}
|
||||
onClick={() => {
|
||||
setInitialData({
|
||||
order: orderId
|
||||
});
|
||||
newLine.open();
|
||||
}}
|
||||
hidden={!user.hasAddRole(UserRoles.sales_order)}
|
||||
/>
|
||||
];
|
||||
}, [user]);
|
||||
|
||||
const rowActions = useCallback(
|
||||
(record: any) => {
|
||||
const allocated = (record?.allocated ?? 0) > (record?.quantity ?? 0);
|
||||
|
||||
return [
|
||||
{
|
||||
hidden: allocated || !user.hasChangeRole(UserRoles.sales_order),
|
||||
title: t`Allocate stock`,
|
||||
icon: <IconSquareArrowRight />,
|
||||
color: 'green'
|
||||
},
|
||||
RowEditAction({
|
||||
hidden: !user.hasChangeRole(UserRoles.sales_order),
|
||||
onClick: () => {
|
||||
setSelectedLine(record.pk);
|
||||
editLine.open();
|
||||
}
|
||||
}),
|
||||
RowDuplicateAction({
|
||||
hidden: !user.hasAddRole(UserRoles.sales_order),
|
||||
onClick: () => {
|
||||
setInitialData(record);
|
||||
newLine.open();
|
||||
}
|
||||
}),
|
||||
RowDeleteAction({
|
||||
hidden: !user.hasDeleteRole(UserRoles.sales_order),
|
||||
onClick: () => {
|
||||
setSelectedLine(record.pk);
|
||||
deleteLine.open();
|
||||
}
|
||||
})
|
||||
];
|
||||
},
|
||||
[user]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{editLine.modal}
|
||||
{deleteLine.modal}
|
||||
{newLine.modal}
|
||||
<InvenTreeTable
|
||||
url={apiUrl(ApiEndpoints.sales_order_line_list)}
|
||||
tableState={table}
|
||||
columns={tableColumns}
|
||||
props={{
|
||||
enableSelection: true,
|
||||
enableDownload: true,
|
||||
params: {
|
||||
order: orderId,
|
||||
part_detail: true
|
||||
},
|
||||
rowActions: rowActions,
|
||||
tableActions: tableActions,
|
||||
modelType: ModelType.part,
|
||||
modelField: 'part'
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
@ -11,6 +11,7 @@ import {
|
||||
TemplateEditor
|
||||
} from '../../components/editors/TemplateEditor';
|
||||
import { ApiFormFieldSet } from '../../components/forms/fields/ApiFormField';
|
||||
import { AttachmentLink } from '../../components/items/AttachmentLink';
|
||||
import { DetailDrawer } from '../../components/nav/DetailDrawer';
|
||||
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
||||
import { ModelType } from '../../enums/ModelType';
|
||||
@ -134,7 +135,11 @@ export function TemplateTable({
|
||||
sortable: false,
|
||||
switchable: true,
|
||||
render: (record: any) => {
|
||||
return record.template?.split('/')?.pop() ?? '-';
|
||||
if (!record.template) {
|
||||
return '-';
|
||||
}
|
||||
|
||||
return <AttachmentLink attachment={record.template} />;
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -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();
|
||||
|
Reference in New Issue
Block a user