2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-06-16 03:55:41 +00:00

Build order improvements (#6343)

* Auto-fill project code

When creating a new child build order, copy project code from parent build

* Auto-fill project code for sales orders

* Annotate "building" quantity to BuildLine serializer

- So we know how many units are in production

* Display building quantity in build line table

* Update API version info

* Skeleton for BuildLineTable

- No content yet (needs work)

* Refactor part hovercard

* Navigate to part

* Add actions for build line table

* Display more information for "available stock" column

* More updates

* Fix "building" filter

- Rename to "in_production"

* Add filters

* Remove unused imports
This commit is contained in:
Oliver
2024-01-29 10:56:34 +11:00
committed by GitHub
parent 1272b89839
commit f6ba180cc4
14 changed files with 334 additions and 49 deletions

View File

@ -1,5 +1,5 @@
import { t } from '@lingui/macro';
import { Anchor } from '@mantine/core';
import { Anchor, Skeleton } from '@mantine/core';
import { Group } from '@mantine/core';
import { Text } from '@mantine/core';
import { ReactNode, useMemo } from 'react';
@ -74,3 +74,16 @@ export function ThumbnailHoverCard({
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 />
);
}

View File

@ -5,7 +5,7 @@ import { useNavigate } from 'react-router-dom';
import { ApiPaths } from '../../../enums/ApiEndpoints';
import { useTable } from '../../../hooks/UseTable';
import { apiUrl } from '../../../states/ApiState';
import { ThumbnailHoverCard } from '../../images/Thumbnail';
import { PartHoverCard } from '../../images/Thumbnail';
import { TableColumn } from '../Column';
import { TableFilter } from '../Filter';
import { InvenTreeTable } from '../InvenTreeTable';
@ -31,37 +31,13 @@ export function UsedInTable({
title: t`Assembled Part`,
switchable: false,
sortable: true,
render: (record: any) => {
let part = record.part_detail;
return (
part && (
<ThumbnailHoverCard
src={part.thumbnail || part.image}
text={part.full_name}
alt={part.description}
link=""
/>
)
);
}
render: (record: any) => <PartHoverCard part={record.part_detail} />
},
{
accessor: 'sub_part',
title: t`Required Part`,
sortable: true,
render: (record: any) => {
let part = record.sub_part_detail;
return (
part && (
<ThumbnailHoverCard
src={part.thumbnail || part.image}
text={part.full_name}
alt={part.description}
link=""
/>
)
);
}
render: (record: any) => <PartHoverCard part={record.sub_part_detail} />
},
{
accessor: 'quantity',

View File

@ -0,0 +1,242 @@
import { t } from '@lingui/macro';
import { Group, Text } from '@mantine/core';
import {
IconArrowRight,
IconShoppingCart,
IconTool
} from '@tabler/icons-react';
import { useCallback, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { ApiPaths } from '../../../enums/ApiEndpoints';
import { useTable } from '../../../hooks/UseTable';
import { apiUrl } from '../../../states/ApiState';
import { useUserState } from '../../../states/UserState';
import { PartHoverCard } from '../../images/Thumbnail';
import { ProgressBar } from '../../items/ProgressBar';
import { TableColumn } from '../Column';
import { BooleanColumn } from '../ColumnRenderers';
import { TableFilter } from '../Filter';
import { InvenTreeTable } from '../InvenTreeTable';
import { TableHoverCard } from '../TableHoverCard';
export default function BuildLineTable({ params = {} }: { params?: any }) {
const table = useTable('buildline');
const user = useUserState();
const navigate = useNavigate();
const tableFilters: TableFilter[] = useMemo(() => {
return [
{
name: 'allocated',
label: t`Allocated`,
description: t`Show allocated lines`
},
{
name: 'available',
label: t`Available`,
description: t`Show lines with available stock`
},
{
name: 'consumable',
label: t`Consumable`,
description: t`Show consumable lines`
},
{
name: 'optional',
label: t`Optional`,
description: t`Show optional lines`
}
];
}, []);
const renderAvailableColumn = useCallback((record: any) => {
let bom_item = record?.bom_item_detail ?? {};
let extra: any[] = [];
let available = record?.available_stock;
// Account for substitute stock
if (record.available_substitute_stock > 0) {
available += record.available_substitute_stock;
extra.push(
<Text key="substitite" size="sm">
{t`Includes substitute stock`}
</Text>
);
}
// Account for variant stock
if (bom_item.allow_variants && record.available_variant_stock > 0) {
available += record.available_variant_stock;
extra.push(
<Text key="variant" size="sm">
{t`Includes variant stock`}
</Text>
);
}
// Account for in-production stock
if (record.in_production > 0) {
extra.push(
<Text key="production" size="sm">
{t`In production`}: {record.in_production}
</Text>
);
}
// Account for stock on order
if (record.on_order > 0) {
extra.push(
<Text key="on-order" size="sm">
{t`On order`}: {record.on_order}
</Text>
);
}
return (
<TableHoverCard
value={
available > 0 ? (
available
) : (
<Text color="red" italic>{t`No stock available`}</Text>
)
}
title={t`Available Stock`}
extra={extra}
/>
);
}, []);
const tableColumns: TableColumn[] = useMemo(() => {
return [
{
accessor: 'bom_item',
title: t`Part`,
sortable: true,
switchable: false,
render: (record: any) => <PartHoverCard part={record.part_detail} />
},
{
accessor: 'reference',
title: t`Reference`,
render: (record: any) => record.bom_item_detail.reference
},
BooleanColumn({
accessor: 'bom_item_detail.consumable',
title: t`Consumable`
}),
BooleanColumn({
accessor: 'bom_item_detail.optional',
title: t`Optional`
}),
{
accessor: 'unit_quantity',
title: t`Unit Quantity`,
sortable: true,
render: (record: any) => {
return (
<Group position="apart">
<Text>{record.bom_item_detail?.quantity}</Text>
{record?.part_detail?.units && (
<Text size="xs">[{record.part_detail.units}]</Text>
)}
</Group>
);
}
},
{
accessor: 'quantity',
title: t`Required Quantity`,
sortable: true,
render: (record: any) => {
return (
<Group position="apart">
<Text>{record.quantity}</Text>
{record?.part_detail?.units && (
<Text size="xs">[{record.part_detail.units}]</Text>
)}
</Group>
);
}
},
{
accessor: 'available_stock',
title: t`Available`,
sortable: true,
switchable: false,
render: renderAvailableColumn
},
{
accessor: 'allocated',
title: t`Allocated`,
switchable: false,
render: (record: any) => {
return record?.bom_item_detail?.consumable ? (
<Text italic>{t`Consumable item`}</Text>
) : (
<ProgressBar
progressLabel={true}
value={record.allocated}
maximum={record.quantity}
/>
);
}
}
];
}, []);
const rowActions = useCallback(
(record: any) => {
let part = record.part_detail;
// Consumable items have no appropriate actions
if (record?.bom_item_detail?.consumable) {
return [];
}
return [
{
icon: <IconArrowRight />,
title: t`Allocate Stock`,
hidden: record.allocated >= record.quantity,
color: 'green'
},
{
icon: <IconShoppingCart />,
title: t`Order Stock`,
hidden: !part?.purchaseable,
color: 'blue'
},
{
icon: <IconTool />,
title: t`Build Stock`,
hidden: !part?.assembly,
color: 'blue'
}
];
},
[user]
);
return (
<InvenTreeTable
url={apiUrl(ApiPaths.build_line_list)}
tableState={table}
columns={tableColumns}
props={{
params: {
...params,
part_detail: true
},
tableFilters: tableFilters,
rowActions: rowActions,
onRowClick: (row: any) => {
if (row?.part_detail?.pk) {
navigate(`/part/${row.part_detail.pk}`);
}
}
}}
/>
);
}

View File

@ -7,7 +7,7 @@ import { ApiPaths } from '../../../enums/ApiEndpoints';
import { ModelType } from '../../../enums/ModelType';
import { useTable } from '../../../hooks/UseTable';
import { apiUrl } from '../../../states/ApiState';
import { ThumbnailHoverCard } from '../../images/Thumbnail';
import { PartHoverCard } from '../../images/Thumbnail';
import { ProgressBar } from '../../items/ProgressBar';
import { RenderUser } from '../../render/User';
import { TableColumn } from '../Column';
@ -37,19 +37,7 @@ function buildOrderTableColumns(): TableColumn[] {
sortable: true,
switchable: false,
title: t`Part`,
render: (record: any) => {
let part = record.part_detail;
return (
part && (
<ThumbnailHoverCard
src={part.thumbnail || part.image}
text={part.full_name}
alt={part.description}
link=""
/>
)
);
}
render: (record: any) => <PartHoverCard part={record.part_detail} />
},
{
accessor: 'title',

View File

@ -47,6 +47,7 @@ export enum ApiPaths {
// Build order URLs
build_order_list = 'api-build-list',
build_order_attachment_list = 'api-build-attachment-list',
build_line_list = 'api-build-line-list',
// BOM URLs
bom_list = 'api-bom-list',

View File

@ -29,6 +29,7 @@ import {
import { PageDetail } from '../../components/nav/PageDetail';
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
import { StatusRenderer } from '../../components/render/StatusRenderer';
import BuildLineTable from '../../components/tables/build/BuildLineTable';
import { BuildOrderTable } from '../../components/tables/build/BuildOrderTable';
import { AttachmentTable } from '../../components/tables/general/AttachmentTable';
import { StockItemTable } from '../../components/tables/stock/StockItemTable';
@ -104,8 +105,17 @@ export default function BuildDetail() {
{
name: 'allocate-stock',
label: t`Allocate Stock`,
icon: <IconListCheck />
// TODO: Hide if build is complete
icon: <IconListCheck />,
content: build?.pk ? (
<BuildLineTable
params={{
build: id,
tracked: false
}}
/>
) : (
<Skeleton />
)
},
{
name: 'incomplete-outputs',

View File

@ -151,6 +151,8 @@ export function apiEndpoint(path: ApiPaths): string {
return 'build/';
case ApiPaths.build_order_attachment_list:
return 'build/attachment/';
case ApiPaths.build_line_list:
return 'build/line/';
case ApiPaths.bom_list:
return 'bom/';
case ApiPaths.part_list: