mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
[PUI] Table Updates (#7783)
* Refactor part display in tables - Remove legacy code * Remove unused function * Refactoring for PurchaseOrderLineItemTable * Implement sales order line item table * Add placeholders for row actions * Implement table actions * Add placeholder action to allocate stock
This commit is contained in:
parent
21511c74ff
commit
e5fabc6788
@ -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 {
|
||||
|
@ -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',
|
||||
|
@ -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',
|
||||
|
@ -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'
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
Loading…
Reference in New Issue
Block a user