Files
InvenTree/src/frontend/src/tables/part/PartTable.tsx
Oliver 767b76314e Revision Improvements (#7585)
* Bump djangorestframework from 3.14.0 to 3.15.2 in /src/backend

Bumps [djangorestframework](https://github.com/encode/django-rest-framework) from 3.14.0 to 3.15.2.
- [Release notes](https://github.com/encode/django-rest-framework/releases)
- [Commits](https://github.com/encode/django-rest-framework/compare/3.14.0...3.15.2)

---
updated-dependencies:
- dependency-name: djangorestframework
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

* fix req

* fix deps again

* patch serializer

* bump api version

* Fix "min_value" for DRF decimal fields

* Add default serializer values for 'IPN' and 'revision'

* Add specific serializer for email field

* Fix API version

* Add 'revision_of' field to Part model

* Add validation checks for new revision_of field

* Update migration

* Add unit test for 'revision' rules

* Add API filters for revision control

* Add table filters for PUI

* Add "revision_of" field to PUI form

* Update part forms for PUI

* Render part revision selection dropdown in PUI

* Prevent refetch on focus

* Ensure select renders above other items

* Disable searching

* Cleanup <PartDetail/>

* UI tweak

* Add setting to control revisions for assemblies

* Hide revision selection drop-down if revisions are not enabled

* Query updates

* Validate entire BOM table from PUI

* Sort revisions

* Fix requirements files

* Fix api_version.py

* Reintroduce previous check for IPN / revision uniqueness

* Set default value for refetchOnWindowFocus (false)

* Revert serializer change

* Further CI fixes

* Further unit test updates

* Fix defaults for query client

* Add docs

* Add link to "revision_of" in CUI

* Add playwright test for revisions

* Ignore notification errors for playwright

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Matthias Mair <code@mjmair.com>
2024-07-12 14:37:32 +10:00

357 lines
9.0 KiB
TypeScript

import { t } from '@lingui/macro';
import { Group, Text } from '@mantine/core';
import { ReactNode, useMemo } from 'react';
import { AddItemButton } from '../../components/buttons/AddItemButton';
import { formatPriceRange } from '../../defaults/formatters';
import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType';
import { UserRoles } from '../../enums/Roles';
import { usePartFields } from '../../forms/PartForms';
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 { DescriptionColumn, LinkColumn, PartColumn } from '../ColumnRenderers';
import { TableFilter } from '../Filter';
import { InvenTreeTable, InvenTreeTableProps } from '../InvenTreeTable';
import { TableHoverCard } from '../TableHoverCard';
/**
* Construct a list of columns for the part table
*/
function partTableColumns(): TableColumn[] {
return [
{
accessor: 'name',
title: t`Part`,
sortable: true,
noWrap: true,
render: (record: any) => PartColumn(record)
},
{
accessor: 'IPN',
sortable: true
},
{
accessor: 'revision',
sortable: true
},
{
accessor: 'units',
sortable: true
},
DescriptionColumn({}),
{
accessor: 'category',
sortable: true,
render: (record: any) => record.category_detail?.pathstring
},
{
accessor: 'default_location',
sortable: true,
render: (record: any) => record.default_location_detail?.pathstring
},
{
accessor: 'total_in_stock',
sortable: true,
render: (record) => {
let extra: ReactNode[] = [];
let stock = record?.total_in_stock ?? 0;
let allocated =
(record?.allocated_to_build_orders ?? 0) +
(record?.allocated_to_sales_orders ?? 0);
let available = Math.max(0, stock - allocated);
let min_stock = record?.minimum_stock ?? 0;
let text = String(stock);
let color: string | undefined = undefined;
if (min_stock > stock) {
extra.push(
<Text key="min-stock" color="orange">
{t`Minimum stock` + `: ${min_stock}`}
</Text>
);
color = 'orange';
}
if (record.ordering > 0) {
extra.push(
<Text key="on-order">{t`On Order` + `: ${record.ordering}`}</Text>
);
}
if (record.building) {
extra.push(
<Text key="building">{t`Building` + `: ${record.building}`}</Text>
);
}
if (record.allocated_to_build_orders > 0) {
extra.push(
<Text key="bo-allocations">
{t`Build Order Allocations` +
`: ${record.allocated_to_build_orders}`}
</Text>
);
}
if (record.allocated_to_sales_orders > 0) {
extra.push(
<Text key="so-allocations">
{t`Sales Order Allocations` +
`: ${record.allocated_to_sales_orders}`}
</Text>
);
}
if (available != stock) {
extra.push(
<Text key="available">
{t`Available`}: {available}
</Text>
);
}
if (record.external_stock > 0) {
extra.push(
<Text key="external">
{t`External stock`}: {record.external_stock}
</Text>
);
}
// TODO: Add extra information on stock "demand"
if (stock <= 0) {
color = 'red';
text = t`No stock`;
} else if (available <= 0) {
color = 'orange';
} else if (available < min_stock) {
color = 'yellow';
}
return (
<TableHoverCard
value={
<Group gap="xs" justify="left" wrap="nowrap">
<Text c={color}>{text}</Text>
{record.units && (
<Text size="xs" color={color}>
[{record.units}]
</Text>
)}
</Group>
}
title={t`Stock Information`}
extra={extra}
/>
);
}
},
{
accessor: 'price_range',
title: t`Price Range`,
sortable: true,
ordering: 'pricing_max',
render: (record: any) =>
formatPriceRange(record.pricing_min, record.pricing_max)
},
LinkColumn({})
];
}
/**
* Construct a set of filters for the part table
*/
function partTableFilters(): TableFilter[] {
return [
{
name: 'active',
label: t`Active`,
description: t`Filter by part active status`,
type: 'boolean'
},
{
name: 'locked',
label: t`Locked`,
description: t`Filter by part locked status`,
type: 'boolean'
},
{
name: 'assembly',
label: t`Assembly`,
description: t`Filter by assembly attribute`,
type: 'boolean'
},
{
name: 'cascade',
label: t`Include Subcategories`,
description: t`Include parts in subcategories`,
type: 'boolean'
},
{
name: 'component',
label: t`Component`,
description: t`Filter by component attribute`,
type: 'boolean'
},
{
name: 'trackable',
label: t`Trackable`,
description: t`Filter by trackable attribute`,
type: 'boolean'
},
{
name: 'has_units',
label: t`Has Units`,
description: t`Filter by parts which have units`,
type: 'boolean'
},
{
name: 'has_ipn',
label: t`Has IPN`,
description: t`Filter by parts which have an internal part number`,
type: 'boolean'
},
{
name: 'has_stock',
label: t`Has Stock`,
description: t`Filter by parts which have stock`,
type: 'boolean'
},
{
name: 'low_stock',
label: t`Low Stock`,
description: t`Filter by parts which have low stock`,
type: 'boolean'
},
{
name: 'purchaseable',
label: t`Purchaseable`,
description: t`Filter by parts which are purchaseable`,
type: 'boolean'
},
{
name: 'salable',
label: t`Salable`,
description: t`Filter by parts which are salable`,
type: 'boolean'
},
{
name: 'virtual',
label: t`Virtual`,
description: t`Filter by parts which are virtual`,
type: 'choice',
choices: [
{ value: 'true', label: t`Virtual` },
{ value: 'false', label: t`Not Virtual` }
]
},
{
name: 'is_template',
label: t`Is Template`,
description: t`Filter by parts which are templates`,
type: 'boolean'
},
{
name: 'is_revision',
label: t`Is Revision`,
description: t`Filter by parts which are revisions`
},
{
name: 'has_revisions',
label: t`Has Revisions`,
description: t`Filter by parts which have revisions`
},
{
name: 'has_pricing',
label: t`Has Pricing`,
description: t`Filter by parts which have pricing information`,
type: 'boolean'
},
{
name: 'unallocated_stock',
label: t`Available Stock`,
description: t`Filter by parts which have available stock`,
type: 'boolean'
},
{
name: 'starred',
label: t`Subscribed`,
description: t`Filter by parts to which the user is subscribed`,
type: 'boolean'
},
{
name: 'stocktake',
label: t`Has Stocktake`,
description: t`Filter by parts which have stocktake information`,
type: 'boolean'
}
];
}
/**
* PartListTable - Displays a list of parts, based on the provided parameters
* @param {Object} params - The query parameters to pass to the API
* @returns
*/
export function PartListTable({ props }: { props: InvenTreeTableProps }) {
const tableColumns = useMemo(() => partTableColumns(), []);
const tableFilters = useMemo(() => partTableFilters(), []);
const table = useTable('part-list');
const user = useUserState();
const newPart = useCreateApiFormModal({
url: ApiEndpoints.part_list,
title: t`Add Part`,
fields: usePartFields({ create: true }),
initialData: {
...(props.params ?? {})
},
follow: true,
modelType: ModelType.part
});
const tableActions = useMemo(() => {
return [
<AddItemButton
hidden={!user.hasAddRole(UserRoles.part)}
tooltip={t`Add Part`}
onClick={() => newPart.open()}
/>
];
}, [user]);
return (
<>
{newPart.modal}
<InvenTreeTable
url={apiUrl(ApiEndpoints.part_list)}
tableState={table}
columns={tableColumns}
props={{
...props,
enableDownload: true,
modelType: ModelType.part,
tableFilters: tableFilters,
tableActions: tableActions,
params: {
...props.params,
category_detail: true,
location_detail: true
}
}}
/>
</>
);
}