Build allocation table updates (#7106)

* Update build line allocation table

- Allow display of "tracked" items in main allocation table

* Add resolveItem function for finding nested items

* Update BuildLineTable

* Allow BuildLineList to be ordered by 'trackable' field

* Bump API version
This commit is contained in:
Oliver 2024-04-23 11:50:24 +10:00 committed by GitHub
parent 8f2ef39282
commit 3e52e5fd69
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 64 additions and 22 deletions

View File

@ -1,11 +1,14 @@
"""InvenTree API version information.""" """InvenTree API version information."""
# InvenTree API version # InvenTree API version
INVENTREE_API_VERSION = 191 INVENTREE_API_VERSION = 192
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about.""" """Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
INVENTREE_API_TEXT = """ INVENTREE_API_TEXT = """
v192 - 2024-04-23 : https://github.com/inventree/InvenTree/pull/7106
- Adds 'trackable' ordering option to BuildLineLabel API endpoint
v191 - 2024-04-22 : https://github.com/inventree/InvenTree/pull/7079 v191 - 2024-04-22 : https://github.com/inventree/InvenTree/pull/7079
- Adds API endpoints for Contenttype model - Adds API endpoints for Contenttype model

View File

@ -349,6 +349,7 @@ class BuildLineList(BuildLineEndpoint, ListCreateAPI):
'optional', 'optional',
'unit_quantity', 'unit_quantity',
'available_stock', 'available_stock',
'trackable',
] ]
ordering_field_aliases = { ordering_field_aliases = {
@ -357,6 +358,7 @@ class BuildLineList(BuildLineEndpoint, ListCreateAPI):
'unit_quantity': 'bom_item__quantity', 'unit_quantity': 'bom_item__quantity',
'consumable': 'bom_item__consumable', 'consumable': 'bom_item__consumable',
'optional': 'bom_item__optional', 'optional': 'bom_item__optional',
'trackable': 'bom_item__sub_part__trackable',
} }
search_fields = [ search_fields = [

View File

@ -2436,11 +2436,9 @@ function loadBuildLineTable(table, build_id, options={}) {
params.build = build_id; params.build = build_id;
if (output) { if (output) {
params.tracked = true;
params.output = output; params.output = output;
name += `-${output}`; name += `-${output}`;
} else {
// Default to untracked parts for the build
params.tracked = false;
} }
let filters = loadTableFilters('buildlines', params); let filters = loadTableFilters('buildlines', params);
@ -2649,7 +2647,11 @@ function loadBuildLineTable(table, build_id, options={}) {
if (row.part_detail.trackable && !options.output) { if (row.part_detail.trackable && !options.output) {
// Tracked parts must be allocated to a specific build output // Tracked parts must be allocated to a specific build output
return `<em>{% trans "Tracked item" %}</em>`; return `
<div>
<em>{% trans "Tracked item" %}</em>
<span title='{% trans "Allocate tracked items against individual build outputs" %}' class='fas fa-info-circle icon-blue' />
</div>`;
} }
if (row.allocated < row.quantity) { if (row.allocated < row.quantity) {

View File

@ -19,3 +19,16 @@ export function isTrue(value: any): boolean {
return ['true', 'yes', '1', 'on', 't', 'y'].includes(s); return ['true', 'yes', '1', 'on', 't', 'y'].includes(s);
} }
/*
* Resolve a nested item in an object.
* Returns the resolved item, if it exists.
*
* e.g. resolveItem(data, "sub.key.accessor")
*
* Allows for retrieval of nested items in an object.
*/
export function resolveItem(obj: any, path: string): any {
let properties = path.split('.');
return properties.reduce((prev, curr) => prev?.[curr], obj);
}

View File

@ -203,8 +203,7 @@ export default function BuildDetail() {
content: build?.pk ? ( content: build?.pk ? (
<BuildLineTable <BuildLineTable
params={{ params={{
build: id, build: id
tracked: false
}} }}
/> />
) : ( ) : (

View File

@ -3,6 +3,7 @@
*/ */
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { Anchor } from '@mantine/core'; import { Anchor } from '@mantine/core';
import { access } from 'fs';
import { YesNoButton } from '../components/buttons/YesNoButton'; import { YesNoButton } from '../components/buttons/YesNoButton';
import { Thumbnail } from '../components/images/Thumbnail'; import { Thumbnail } from '../components/images/Thumbnail';
@ -11,6 +12,7 @@ import { TableStatusRenderer } from '../components/render/StatusRenderer';
import { RenderOwner } from '../components/render/User'; import { RenderOwner } from '../components/render/User';
import { formatCurrency, renderDate } from '../defaults/formatters'; import { formatCurrency, renderDate } from '../defaults/formatters';
import { ModelType } from '../enums/ModelType'; import { ModelType } from '../enums/ModelType';
import { resolveItem } from '../functions/conversion';
import { cancelEvent } from '../functions/events'; import { cancelEvent } from '../functions/events';
import { TableColumn } from './Column'; import { TableColumn } from './Column';
import { ProjectCodeHoverCard } from './TableHoverCard'; import { ProjectCodeHoverCard } from './TableHoverCard';
@ -29,19 +31,24 @@ export function BooleanColumn({
accessor, accessor,
title, title,
sortable, sortable,
switchable switchable,
ordering
}: { }: {
accessor: string; accessor: string;
title?: string; title?: string;
ordering?: string;
sortable?: boolean; sortable?: boolean;
switchable?: boolean; switchable?: boolean;
}): TableColumn { }): TableColumn {
return { return {
accessor: accessor, accessor: accessor,
title: title, title: title,
ordering: ordering,
sortable: sortable ?? true, sortable: sortable ?? true,
switchable: switchable ?? true, switchable: switchable ?? true,
render: (record: any) => <YesNoButton value={record[accessor]} /> render: (record: any) => (
<YesNoButton value={resolveItem(record, accessor)} />
)
}; };
} }
@ -71,7 +78,7 @@ export function LinkColumn({
accessor: accessor, accessor: accessor,
sortable: false, sortable: false,
render: (record: any) => { render: (record: any) => {
let url = record[accessor]; let url = resolveItem(record, accessor);
if (!url) { if (!url) {
return '-'; return '-';

View File

@ -28,6 +28,7 @@ import { ActionButton } from '../components/buttons/ActionButton';
import { ButtonMenu } from '../components/buttons/ButtonMenu'; import { ButtonMenu } from '../components/buttons/ButtonMenu';
import { ApiFormFieldSet } from '../components/forms/fields/ApiFormField'; import { ApiFormFieldSet } from '../components/forms/fields/ApiFormField';
import { ModelType } from '../enums/ModelType'; import { ModelType } from '../enums/ModelType';
import { resolveItem } from '../functions/conversion';
import { extractAvailableFields, mapFields } from '../functions/forms'; import { extractAvailableFields, mapFields } from '../functions/forms';
import { getDetailUrl } from '../functions/urls'; import { getDetailUrl } from '../functions/urls';
import { TableState } from '../hooks/UseTable'; import { TableState } from '../hooks/UseTable';
@ -519,7 +520,8 @@ export function InvenTreeTable<T = any>({
// If a custom row click handler is provided, use that // If a custom row click handler is provided, use that
props.onRowClick(record, index, event); props.onRowClick(record, index, event);
} else if (tableProps.modelType) { } else if (tableProps.modelType) {
const pk = record?.[tableProps.modelField ?? 'pk']; const accessor = tableProps.modelField ?? 'pk';
const pk = resolveItem(record, accessor);
if (pk) { if (pk) {
// If a model type is provided, navigate to the detail view for that model // If a model type is provided, navigate to the detail view for that model

View File

@ -6,13 +6,11 @@ import {
IconTool IconTool
} from '@tabler/icons-react'; } from '@tabler/icons-react';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { PartHoverCard } from '../../components/images/Thumbnail'; import { PartHoverCard } from '../../components/images/Thumbnail';
import { ProgressBar } from '../../components/items/ProgressBar'; import { ProgressBar } from '../../components/items/ProgressBar';
import { ApiEndpoints } from '../../enums/ApiEndpoints'; import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType'; import { ModelType } from '../../enums/ModelType';
import { getDetailUrl } from '../../functions/urls';
import { useTable } from '../../hooks/UseTable'; import { useTable } from '../../hooks/UseTable';
import { apiUrl } from '../../states/ApiState'; import { apiUrl } from '../../states/ApiState';
import { useUserState } from '../../states/UserState'; import { useUserState } from '../../states/UserState';
@ -25,7 +23,6 @@ import { TableHoverCard } from '../TableHoverCard';
export default function BuildLineTable({ params = {} }: { params?: any }) { export default function BuildLineTable({ params = {} }: { params?: any }) {
const table = useTable('buildline'); const table = useTable('buildline');
const user = useUserState(); const user = useUserState();
const navigate = useNavigate();
const tableFilters: TableFilter[] = useMemo(() => { const tableFilters: TableFilter[] = useMemo(() => {
return [ return [
@ -47,6 +44,11 @@ export default function BuildLineTable({ params = {} }: { params?: any }) {
name: 'optional', name: 'optional',
label: t`Optional`, label: t`Optional`,
description: t`Show optional lines` description: t`Show optional lines`
},
{
name: 'tracked',
label: t`Tracked`,
description: t`Show tracked lines`
} }
]; ];
}, []); }, []);
@ -122,18 +124,28 @@ export default function BuildLineTable({ params = {} }: { params?: any }) {
return [ return [
{ {
accessor: 'bom_item', accessor: 'bom_item',
ordering: 'part',
sortable: true, sortable: true,
switchable: false, switchable: false,
render: (record: any) => <PartHoverCard part={record.part_detail} /> render: (record: any) => <PartHoverCard part={record.part_detail} />
}, },
{ {
accessor: 'bom_item_detail.reference' accessor: 'bom_item_detail.reference',
ordering: 'reference',
sortable: true,
title: t`Reference`
}, },
BooleanColumn({ BooleanColumn({
accessor: 'bom_item_detail.consumable' accessor: 'bom_item_detail.consumable',
ordering: 'consumable'
}), }),
BooleanColumn({ BooleanColumn({
accessor: 'bom_item_detail.optional' accessor: 'bom_item_detail.optional',
ordering: 'optional'
}),
BooleanColumn({
accessor: 'part_detail.trackable',
ordering: 'trackable'
}), }),
{ {
accessor: 'bom_item_detail.quantity', accessor: 'bom_item_detail.quantity',
@ -198,6 +210,11 @@ export default function BuildLineTable({ params = {} }: { params?: any }) {
return []; return [];
} }
// Tracked items must be allocated to a particular output
if (record?.part_detail?.trackable) {
return [];
}
return [ return [
{ {
icon: <IconArrowRight />, icon: <IconArrowRight />,
@ -234,11 +251,8 @@ export default function BuildLineTable({ params = {} }: { params?: any }) {
}, },
tableFilters: tableFilters, tableFilters: tableFilters,
rowActions: rowActions, rowActions: rowActions,
onRowClick: (row: any) => { modelType: ModelType.part,
if (row?.part_detail?.pk) { modelField: 'part_detail.pk'
navigate(getDetailUrl(ModelType.part, row.part_detail.pk));
}
}
}} }}
/> />
); );