mirror of
https://github.com/inventree/InvenTree.git
synced 2026-02-11 01:19:15 -06:00
* 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>
357 lines
9.0 KiB
TypeScript
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
|
|
}
|
|
}}
|
|
/>
|
|
</>
|
|
);
|
|
}
|