[React] Order tables (#5860)

* Factor out custom component for displaying project code information in a table

* Bump API version

* Update order serializers

- Annotate 'completed_lines' to each order type

* Build out columns for ReturnOrderTable

* Improvements to PurchaseOrderTable

* Building out SalesOrderTable

* Column tweaks

* Factor out project code column

* Factor out status column

* Factor out description column

* Factor out more columns

* More refactoring

* Center status labels

* Fix for PurchaseOrderLineItemTable

* Improve rendering

* Remove unused imports

* Refactor TotalPriceColumn

* Add generic currency column for rendering currency / money values
This commit is contained in:
Oliver 2023-11-05 12:57:38 +11:00 committed by GitHub
parent 93f642c790
commit dbf1baf0ed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 292 additions and 238 deletions

View File

@ -2,11 +2,15 @@
# InvenTree API version
INVENTREE_API_VERSION = 145
INVENTREE_API_VERSION = 147
"""
Increment this API version number whenever there is a significant change to the API that any clients need to know about
v147 -> 2023-11-04: https://github.com/inventree/InvenTree/pull/5860
- Adds "completed_lines" field to SalesOrder API endpoint
- Adds "completed_lines" field to PurchaseOrder API endpoint
v146 -> 2023-11-02: https://github.com/inventree/InvenTree/pull/5822
- Extended SSO Provider endpoint to contain if a provider is configured
- Adds API endpoints for Email Address model

View File

@ -29,8 +29,8 @@ from InvenTree.serializers import (InvenTreeAttachmentSerializer,
InvenTreeModelSerializer,
InvenTreeMoneySerializer)
from InvenTree.status_codes import (PurchaseOrderStatusGroups,
ReturnOrderStatus, SalesOrderStatusGroups,
StockStatus)
ReturnOrderLineStatus, ReturnOrderStatus,
SalesOrderStatusGroups, StockStatus)
from part.serializers import PartBriefSerializer
from users.serializers import OwnerSerializer
@ -58,6 +58,9 @@ class AbstractOrderSerializer(serializers.Serializer):
# Number of line items in this order
line_items = serializers.IntegerField(read_only=True)
# Number of completed line items (this is an annotated field)
completed_lines = serializers.IntegerField(read_only=True)
# Human-readable status text (read-only)
status_text = serializers.CharField(source='get_status_display', read_only=True)
@ -107,6 +110,7 @@ class AbstractOrderSerializer(serializers.Serializer):
'target_date',
'description',
'line_items',
'completed_lines',
'link',
'project_code',
'project_code_detail',
@ -211,6 +215,10 @@ class PurchaseOrderSerializer(TotalPriceMixin, AbstractOrderSerializer, InvenTre
"""
queryset = AbstractOrderSerializer.annotate_queryset(queryset)
queryset = queryset.annotate(
completed_lines=SubqueryCount('lines', filter=Q(quantity__lte=F('received')))
)
queryset = queryset.annotate(
overdue=Case(
When(
@ -743,10 +751,15 @@ class SalesOrderSerializer(TotalPriceMixin, AbstractOrderSerializer, InvenTreeMo
"""Add extra information to the queryset.
- Number of line items in the SalesOrder
- Number of completed line items in the SalesOrder
- Overdue status of the SalesOrder
"""
queryset = AbstractOrderSerializer.annotate_queryset(queryset)
queryset = queryset.annotate(
completed_lines=SubqueryCount('lines', filter=Q(quantity__lte=F('shipped')))
)
queryset = queryset.annotate(
overdue=Case(
When(
@ -1503,6 +1516,10 @@ class ReturnOrderSerializer(AbstractOrderSerializer, TotalPriceMixin, InvenTreeM
"""Custom annotation for the serializer queryset"""
queryset = AbstractOrderSerializer.annotate_queryset(queryset)
queryset = queryset.annotate(
completed_lines=SubqueryCount('lines', filter=~Q(outcome=ReturnOrderLineStatus.PENDING.value))
)
queryset = queryset.annotate(
overdue=Case(
When(

View File

@ -1,4 +1,4 @@
import { Badge, MantineSize } from '@mantine/core';
import { Badge, Center, MantineSize } from '@mantine/core';
import { colorMap } from '../../defaults/backendMappings';
import { useServerApiState } from '../../states/ApiState';
@ -99,5 +99,8 @@ export const StatusRenderer = ({
export function TableStatusRenderer(
type: ModelType
): ((record: any) => any) | undefined {
return (record: any) => StatusRenderer({ status: record.status, type: type });
return (record: any) =>
record.status && (
<Center>{StatusRenderer({ status: record.status, type: type })}</Center>
);
}

View File

@ -0,0 +1,134 @@
/**
* Common rendering functions for table column data.
*/
import { t } from '@lingui/macro';
import { ProgressBar } from '../items/ProgressBar';
import { ModelType } from '../render/ModelType';
import { RenderOwner } from '../render/User';
import { TableStatusRenderer } from '../renderers/StatusRenderer';
import { TableColumn } from './Column';
import { ProjectCodeHoverCard } from './TableHoverCard';
export function DescriptionColumn(): TableColumn {
return {
accessor: 'description',
title: t`Description`,
sortable: false,
switchable: true
};
}
export function LinkColumn(): TableColumn {
return {
accessor: 'link',
title: t`Link`,
sortable: false
// TODO: Custom URL hyperlink renderer?
};
}
export function LineItemsProgressColumn(): TableColumn {
return {
accessor: 'line_items',
title: t`Line Items`,
sortable: true,
render: (record: any) => (
<ProgressBar
progressLabel={true}
value={record.completed_lines}
maximum={record.line_items}
/>
)
};
}
export function ProjectCodeColumn(): TableColumn {
return {
accessor: 'project_code',
title: t`Project Code`,
sortable: true,
render: (record: any) => (
<ProjectCodeHoverCard projectCode={record.project_code_detail} />
)
};
}
export function StatusColumn(model: ModelType) {
return {
accessor: 'status',
sortable: true,
title: t`Status`,
render: TableStatusRenderer(model)
};
}
export function ResponsibleColumn(): TableColumn {
return {
accessor: 'responsible',
title: t`Responsible`,
sortable: true,
render: (record: any) =>
record.responsible && RenderOwner({ instance: record.responsible_detail })
};
}
export function TargetDateColumn(): TableColumn {
return {
accessor: 'target_date',
title: t`Target Date`,
sortable: true
// TODO: custom renderer which alerts user if target date is overdue
};
}
export function CreationDateColumn(): TableColumn {
return {
accessor: 'creation_date',
title: t`Creation Date`,
sortable: true
};
}
export function ShipmentDateColumn(): TableColumn {
return {
accessor: 'shipment_date',
title: t`Shipment Date`,
sortable: true
};
}
export function CurrencyColumn({
accessor,
title,
currency,
currency_accessor,
sortable
}: {
accessor: string;
title?: string;
currency?: string;
currency_accessor?: string;
sortable?: boolean;
}): TableColumn {
return {
accessor: accessor,
title: title ?? t`Currency`,
sortable: sortable ?? true,
render: (record: any) => {
let value = record[accessor];
let currency_key = currency_accessor ?? `${accessor}_currency`;
currency = currency ?? record[currency_key];
// TODO: A better render which correctly formats money values
return `${value} ${currency}`;
}
};
}
export function TotalPriceColumn(): TableColumn {
return CurrencyColumn({
accessor: 'total_price',
title: t`Total Price`
});
}

View File

@ -1,3 +1,4 @@
import { t } from '@lingui/macro';
import { Divider, Group, HoverCard, Stack, Text } from '@mantine/core';
import { IconInfoCircle } from '@tabler/icons-react';
import { ReactNode } from 'react';
@ -21,6 +22,10 @@ export function TableHoverCard({
return value;
}
if (Array.isArray(extra) && extra.length == 0) {
return value;
}
return (
<HoverCard>
<HoverCard.Target>
@ -42,3 +47,18 @@ export function TableHoverCard({
</HoverCard>
);
}
/**
* Custom hovercard for displaying projectcode detail in a table
*/
export function ProjectCodeHoverCard({ projectCode }: { projectCode: any }) {
return projectCode ? (
<TableHoverCard
value={projectCode?.code}
title={t`Project Code`}
extra={projectCode?.description}
/>
) : (
'-'
);
}

View File

@ -1,22 +1,22 @@
import { t } from '@lingui/macro';
import { Text } from '@mantine/core';
import { useCallback, useMemo } from 'react';
import { useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { buildOrderFields } from '../../../forms/BuildForms';
import { openCreateApiForm } from '../../../functions/forms';
import { useTableRefresh } from '../../../hooks/TableRefresh';
import { ApiPaths, apiUrl } from '../../../states/ApiState';
import { AddItemButton } from '../../buttons/AddItemButton';
import { ThumbnailHoverCard } from '../../images/Thumbnail';
import { ProgressBar } from '../../items/ProgressBar';
import { ModelType } from '../../render/ModelType';
import { RenderOwner, RenderUser } from '../../render/User';
import { TableStatusRenderer } from '../../renderers/StatusRenderer';
import { RenderUser } from '../../render/User';
import { TableColumn } from '../Column';
import { TableFilter } from '../Filter';
import {
CreationDateColumn,
ProjectCodeColumn,
ResponsibleColumn,
StatusColumn,
TargetDateColumn
} from '../ColumnRenderers';
import { InvenTreeTable } from '../InvenTreeTable';
import { TableHoverCard } from '../TableHoverCard';
/**
* Construct a list of columns for the build order table
@ -66,47 +66,15 @@ function buildOrderTableColumns(): TableColumn[] {
/>
)
},
{
accessor: 'status',
sortable: true,
title: t`Status`,
render: TableStatusRenderer(ModelType.build)
},
{
accessor: 'project_code',
title: t`Project Code`,
sortable: true,
// TODO: Hide this if project code is not enabled
render: (record: any) => {
let project = record.project_code_detail;
return project ? (
<TableHoverCard
value={project.code}
title={t`Project Code`}
extra={<Text>{project.description}</Text>}
/>
) : (
'-'
);
}
},
StatusColumn(ModelType.build),
ProjectCodeColumn(),
{
accessor: 'priority',
title: t`Priority`,
sortable: true
},
{
accessor: 'creation_date',
sortable: true,
title: t`Created`
},
{
accessor: 'target_date',
sortable: true,
title: t`Target Date`
},
CreationDateColumn(),
TargetDateColumn(),
{
accessor: 'completion_date',
sortable: true,
@ -120,14 +88,7 @@ function buildOrderTableColumns(): TableColumn[] {
<RenderUser instance={record?.issued_by_detail} />
)
},
{
accessor: 'responsible',
sortable: true,
title: t`Responsible`,
render: (record: any) => (
<RenderOwner instance={record?.responsible_detail} />
)
}
ResponsibleColumn()
];
}

View File

@ -6,6 +6,7 @@ import { useNavigate } from 'react-router-dom';
import { useTableRefresh } from '../../../hooks/TableRefresh';
import { ApiPaths, apiUrl } from '../../../states/ApiState';
import { Thumbnail } from '../../images/Thumbnail';
import { DescriptionColumn } from '../ColumnRenderers';
import { InvenTreeTable } from '../InvenTreeTable';
/**
@ -42,11 +43,7 @@ export function CompanyTable({
);
}
},
{
accessor: 'description',
title: t`Description`,
sortable: false
},
DescriptionColumn(),
{
accessor: 'website',
title: t`Website`,

View File

@ -5,6 +5,7 @@ import { useNavigate } from 'react-router-dom';
import { useTableRefresh } from '../../../hooks/TableRefresh';
import { ApiPaths, apiUrl } from '../../../states/ApiState';
import { TableColumn } from '../Column';
import { DescriptionColumn } from '../ColumnRenderers';
import { InvenTreeTable } from '../InvenTreeTable';
/**
@ -23,11 +24,7 @@ export function PartCategoryTable({ params = {} }: { params?: any }) {
sortable: true,
switchable: false
},
{
accessor: 'description',
title: t`Description`,
sortable: false
},
DescriptionColumn(),
{
accessor: 'pathstring',
title: t`Path`,

View File

@ -8,6 +8,7 @@ import { useTableRefresh } from '../../../hooks/TableRefresh';
import { ApiPaths, apiUrl } from '../../../states/ApiState';
import { Thumbnail } from '../../images/Thumbnail';
import { TableColumn } from '../Column';
import { DescriptionColumn, LinkColumn } from '../ColumnRenderers';
import { TableFilter } from '../Filter';
import { InvenTreeTable, InvenTreeTableProps } from '../InvenTreeTable';
import { TableHoverCard } from '../TableHoverCard';
@ -42,11 +43,7 @@ function partTableColumns(): TableColumn[] {
sortable: true,
title: t`Units`
},
{
accessor: 'description',
title: t`Description`,
sortable: true
},
DescriptionColumn(),
{
accessor: 'category',
title: t`Category`,
@ -155,10 +152,7 @@ function partTableColumns(): TableColumn[] {
return '-- price --';
}
},
{
accessor: 'link',
title: t`Link`
}
LinkColumn()
];
}

View File

@ -12,6 +12,13 @@ import { useUserState } from '../../../states/UserState';
import { ActionButton } from '../../buttons/ActionButton';
import { AddItemButton } from '../../buttons/AddItemButton';
import { Thumbnail } from '../../images/Thumbnail';
import { RenderStockLocation } from '../../render/Stock';
import {
CurrencyColumn,
LinkColumn,
TargetDateColumn,
TotalPriceColumn
} from '../ColumnRenderers';
import { InvenTreeTable } from '../InvenTreeTable';
import {
RowDeleteAction,
@ -112,8 +119,8 @@ export function PurchaseOrderLineItemTable({
sortable: true,
switchable: false,
render: (record: any) => {
let part = record?.part_detail;
let supplier_part = record?.supplier_part_detail ?? {};
let part = record?.part_detail ?? supplier_part?.part_detail ?? {};
let extra = [];
if (supplier_part.pack_quantity_native != 1) {
@ -127,8 +134,7 @@ export function PurchaseOrderLineItemTable({
extra.push(
<Text key="total-quantity">
{t`Total Quantity`}: {total}
{part.units}
{t`Total Quantity`}: {total} {part?.units}
</Text>
);
}
@ -158,7 +164,6 @@ export function PurchaseOrderLineItemTable({
{
accessor: 'pack_quantity',
sortable: false,
title: t`Pack Quantity`,
render: (record: any) => record?.supplier_part_detail?.pack_quantity
},
@ -166,7 +171,8 @@ export function PurchaseOrderLineItemTable({
accessor: 'SKU',
title: t`Supplier Code`,
switchable: false,
sortable: true
sortable: true,
render: (record: any) => record?.supplier_part_detail?.SKU
},
{
accessor: 'supplier_link',
@ -183,43 +189,26 @@ export function PurchaseOrderLineItemTable({
render: (record: any) =>
record?.supplier_part_detail?.manufacturer_part_detail?.MPN
},
{
CurrencyColumn({
accessor: 'purchase_price',
title: t`Unit Price`,
sortable: true
// TODO: custom renderer
},
{
accessor: 'total_price',
title: t`Total Price`,
sortable: true
// TODO: custom renderer
},
{
accessor: 'target_date',
title: t`Target Date`,
sortable: true
},
title: t`Unit Price`
}),
TotalPriceColumn(),
TargetDateColumn(),
{
accessor: 'destination',
title: t`Destination`,
sortable: false
// TODO: Custom renderer
sortable: false,
render: (record: any) =>
record.destination
? RenderStockLocation({ instance: record.destination_detail })
: '-'
},
{
accessor: 'notes',
title: t`Notes`
},
{
accessor: 'link',
title: t`Link`
// TODO: custom renderer
}
LinkColumn()
];
}, [orderId, user]);

View File

@ -6,7 +6,16 @@ import { useTableRefresh } from '../../../hooks/TableRefresh';
import { ApiPaths, apiUrl } from '../../../states/ApiState';
import { Thumbnail } from '../../images/Thumbnail';
import { ModelType } from '../../render/ModelType';
import { StatusRenderer } from '../../renderers/StatusRenderer';
import {
CreationDateColumn,
DescriptionColumn,
LineItemsProgressColumn,
ProjectCodeColumn,
ResponsibleColumn,
StatusColumn,
TargetDateColumn,
TotalPriceColumn
} from '../ColumnRenderers';
import { InvenTreeTable } from '../InvenTreeTable';
/**
@ -30,11 +39,9 @@ export function PurchaseOrderTable({ params }: { params?: any }) {
title: t`Reference`,
sortable: true,
switchable: false
// TODO: Display extra information if order is overdue
},
{
accessor: 'description',
title: t`Description`
},
DescriptionColumn(),
{
accessor: 'supplier__name',
title: t`Supplier`,
@ -55,54 +62,13 @@ export function PurchaseOrderTable({ params }: { params?: any }) {
accessor: 'supplier_reference',
title: t`Supplier Reference`
},
{
accessor: 'project_code',
title: t`Project Code`
// TODO: Custom project code formatter
},
{
accessor: 'status',
title: t`Status`,
sortable: true,
render: (record: any) =>
StatusRenderer({
status: record.status,
type: ModelType.purchaseorder
})
},
{
accessor: 'creation_date',
title: t`Created`
// TODO: Custom date formatter
},
{
accessor: 'target_date',
title: t`Target Date`
// TODO: Custom date formatter
},
{
accessor: 'line_items',
title: t`Line Items`,
sortable: true
},
{
accessor: 'total_price',
title: t`Total Price`,
sortable: true
// TODO: Custom money formatter
},
{
accessor: 'responsible',
title: t`Responsible`,
sortable: true
// TODO: custom 'owner' formatter
}
LineItemsProgressColumn(),
StatusColumn(ModelType.purchaseorder),
ProjectCodeColumn(),
CreationDateColumn(),
TargetDateColumn(),
TotalPriceColumn(),
ResponsibleColumn()
];
}, []);

View File

@ -14,6 +14,7 @@ import { useUserState } from '../../../states/UserState';
import { AddItemButton } from '../../buttons/AddItemButton';
import { Thumbnail } from '../../images/Thumbnail';
import { TableColumn } from '../Column';
import { DescriptionColumn, LinkColumn } from '../ColumnRenderers';
import { InvenTreeTable } from '../InvenTreeTable';
import { RowDeleteAction, RowEditAction } from '../RowActions';
import { TableHoverCard } from '../TableHoverCard';
@ -63,11 +64,7 @@ export function SupplierPartTable({ params }: { params: any }): ReactNode {
title: t`Supplier Part`,
sortable: true
},
{
accessor: 'description',
title: t`Description`,
sortable: false
},
DescriptionColumn(),
{
accessor: 'manufacturer',
@ -128,13 +125,7 @@ export function SupplierPartTable({ params }: { params: any }): ReactNode {
);
}
},
{
accessor: 'link',
title: t`Link`,
sortable: false
// TODO: custom link renderer?
},
LinkColumn(),
{
accessor: 'note',
title: t`Notes`,

View File

@ -6,7 +6,15 @@ import { useTableRefresh } from '../../../hooks/TableRefresh';
import { ApiPaths, apiUrl } from '../../../states/ApiState';
import { Thumbnail } from '../../images/Thumbnail';
import { ModelType } from '../../render/ModelType';
import { TableStatusRenderer } from '../../renderers/StatusRenderer';
import {
CreationDateColumn,
DescriptionColumn,
LineItemsProgressColumn,
ProjectCodeColumn,
ResponsibleColumn,
StatusColumn,
TargetDateColumn
} from '../ColumnRenderers';
import { InvenTreeTable } from '../InvenTreeTable';
export function ReturnOrderTable({ params }: { params?: any }) {
@ -26,10 +34,7 @@ export function ReturnOrderTable({ params }: { params?: any }) {
accessor: 'reference',
title: t`Return Order`,
sortable: true
},
{
accessor: 'description',
title: t`Description`
// TODO: Display extra information if order is overdue
},
{
accessor: 'customer__name',
@ -51,24 +56,17 @@ export function ReturnOrderTable({ params }: { params?: any }) {
accessor: 'customer_reference',
title: t`Customer Reference`
},
DescriptionColumn(),
LineItemsProgressColumn(),
StatusColumn(ModelType.returnorder),
ProjectCodeColumn(),
CreationDateColumn(),
TargetDateColumn(),
ResponsibleColumn(),
{
accessor: 'project_code',
title: t`Project Code`
// TODO: Custom formatter
},
{
accessor: 'status',
title: t`Status`,
sortable: true,
render: TableStatusRenderer(ModelType.returnorder)
accessor: 'total_cost',
title: t`Total Cost`
}
// TODO: Creation date
// TODO: Target date
// TODO: Line items
// TODO: Responsible
// TODO: Total cost
];
}, []);

View File

@ -6,7 +6,16 @@ import { useTableRefresh } from '../../../hooks/TableRefresh';
import { ApiPaths, apiUrl } from '../../../states/ApiState';
import { Thumbnail } from '../../images/Thumbnail';
import { ModelType } from '../../render/ModelType';
import { TableStatusRenderer } from '../../renderers/StatusRenderer';
import {
CreationDateColumn,
DescriptionColumn,
LineItemsProgressColumn,
ProjectCodeColumn,
ShipmentDateColumn,
StatusColumn,
TargetDateColumn,
TotalPriceColumn
} from '../ColumnRenderers';
import { InvenTreeTable } from '../InvenTreeTable';
export function SalesOrderTable({ params }: { params?: any }) {
@ -27,10 +36,7 @@ export function SalesOrderTable({ params }: { params?: any }) {
title: t`Sales Order`,
sortable: true,
switchable: false
},
{
accessor: 'description',
title: t`Description`
// TODO: Display extra information if order is overdue
},
{
accessor: 'customer__name',
@ -52,25 +58,14 @@ export function SalesOrderTable({ params }: { params?: any }) {
accessor: 'customer_reference',
title: t`Customer Reference`
},
{
accessor: 'project_code',
title: t`Project Code`
// TODO: Custom formatter
},
{
accessor: 'status',
title: t`Status`,
sortable: true,
render: TableStatusRenderer(ModelType.salesorder)
}
// TODO: Creation date
// TODO: Target date
// TODO: Shipment date
// TODO: Line items
// TODO: Total price
DescriptionColumn(),
LineItemsProgressColumn(),
StatusColumn(ModelType.salesorder),
ProjectCodeColumn(),
CreationDateColumn(),
TargetDateColumn(),
ShipmentDateColumn(),
TotalPriceColumn()
];
}, []);

View File

@ -11,6 +11,7 @@ import { useTableRefresh } from '../../../hooks/TableRefresh';
import { ApiPaths, apiUrl } from '../../../states/ApiState';
import { AddItemButton } from '../../buttons/AddItemButton';
import { TableColumn } from '../Column';
import { DescriptionColumn } from '../ColumnRenderers';
import { InvenTreeTable } from '../InvenTreeTable';
import { RowAction, RowDeleteAction, RowEditAction } from '../RowActions';
@ -27,11 +28,7 @@ export function ProjectCodeTable() {
sortable: true,
title: t`Project Code`
},
{
accessor: 'description',
sortable: false,
title: t`Description`
}
DescriptionColumn()
];
}, []);

View File

@ -7,8 +7,8 @@ import { useTableRefresh } from '../../../hooks/TableRefresh';
import { ApiPaths, apiUrl } from '../../../states/ApiState';
import { Thumbnail } from '../../images/Thumbnail';
import { ModelType } from '../../render/ModelType';
import { TableStatusRenderer } from '../../renderers/StatusRenderer';
import { TableColumn } from '../Column';
import { StatusColumn } from '../ColumnRenderers';
import { TableFilter } from '../Filter';
import { RowAction } from '../RowActions';
import { TableHoverCard } from '../TableHoverCard';
@ -150,14 +150,7 @@ function stockItemTableColumns(): TableColumn[] {
);
}
},
{
accessor: 'status',
sortable: true,
filter: true,
title: t`Status`,
render: TableStatusRenderer(ModelType.stockitem)
},
StatusColumn(ModelType.stockitem),
{
accessor: 'batch',
sortable: true,

View File

@ -6,6 +6,7 @@ import { useTableRefresh } from '../../../hooks/TableRefresh';
import { ApiPaths, apiUrl } from '../../../states/ApiState';
import { YesNoButton } from '../../items/YesNoButton';
import { TableColumn } from '../Column';
import { DescriptionColumn } from '../ColumnRenderers';
import { InvenTreeTable } from '../InvenTreeTable';
/**
@ -23,10 +24,7 @@ export function StockLocationTable({ params = {} }: { params?: any }) {
title: t`Name`,
switchable: false
},
{
accessor: 'description',
title: t`Description`
},
DescriptionColumn(),
{
accessor: 'pathstring',
title: t`Path`,