From abe9b19ead7370e5d2830545d88a938020f5e30c Mon Sep 17 00:00:00 2001 From: Oliver Date: Sat, 3 Aug 2024 18:49:09 +1000 Subject: [PATCH] [PUI] Sales order tables (#7793) * Add placeholder for more sales order actions * Add * Allow filtering by date fields * Add - Add label rendering for ReturnOrderLineItem * Add placeholder actions * Edit / delete / add line items for return order * Fix for duplicate action * Cleanup unused code * Bump API version * Update playwright tests --- .../InvenTree/InvenTree/api_version.py | 6 +- src/backend/InvenTree/order/api.py | 4 +- .../src/components/render/Instance.tsx | 2 + .../src/components/render/ModelType.tsx | 5 + src/frontend/src/components/render/Order.tsx | 17 ++ .../src/components/render/StatusRenderer.tsx | 15 +- src/frontend/src/defaults/backendMappings.tsx | 2 +- src/frontend/src/enums/ApiEndpoints.tsx | 1 + src/frontend/src/enums/ModelType.tsx | 1 + src/frontend/src/forms/ReturnOrderForms.tsx | 38 ++++ src/frontend/src/forms/SalesOrderForms.tsx | 17 ++ .../src/pages/sales/ReturnOrderDetail.tsx | 9 +- .../src/pages/sales/SalesOrderDetail.tsx | 17 +- src/frontend/src/tables/ColumnRenderers.tsx | 7 +- .../tables/sales/ReturnOrderLineItemTable.tsx | 199 ++++++++++++++++++ .../tables/sales/SalesOrderLineItemTable.tsx | 26 ++- .../tables/sales/SalesOrderShipmentTable.tsx | 175 +++++++++++++++ src/frontend/tests/pui_general.spec.ts | 3 +- 18 files changed, 517 insertions(+), 27 deletions(-) create mode 100644 src/frontend/src/forms/ReturnOrderForms.tsx create mode 100644 src/frontend/src/tables/sales/ReturnOrderLineItemTable.tsx create mode 100644 src/frontend/src/tables/sales/SalesOrderShipmentTable.tsx diff --git a/src/backend/InvenTree/InvenTree/api_version.py b/src/backend/InvenTree/InvenTree/api_version.py index adf6b0c037..5aa93531ad 100644 --- a/src/backend/InvenTree/InvenTree/api_version.py +++ b/src/backend/InvenTree/InvenTree/api_version.py @@ -1,12 +1,16 @@ """InvenTree API version information.""" # InvenTree API version -INVENTREE_API_VERSION = 231 +INVENTREE_API_VERSION = 232 """Increment this API version number whenever there is a significant change to the API that any clients need to know about.""" INVENTREE_API_TEXT = """ + +v232 - 2024-08-03 : https://github.com/inventree/InvenTree/pull/7793 + - Allow ordering of SalesOrderShipment API by 'shipment_date' and 'delivery_date' + v231 - 2024-08-03 : https://github.com/inventree/InvenTree/pull/7794 - Optimize BuildItem and BuildLine serializers to improve API efficiency diff --git a/src/backend/InvenTree/order/api.py b/src/backend/InvenTree/order/api.py index 1850439602..92603fadfb 100644 --- a/src/backend/InvenTree/order/api.py +++ b/src/backend/InvenTree/order/api.py @@ -1042,7 +1042,9 @@ class SalesOrderShipmentList(ListCreateAPI): serializer_class = serializers.SalesOrderShipmentSerializer filterset_class = SalesOrderShipmentFilter - filter_backends = [rest_filters.DjangoFilterBackend] + filter_backends = SEARCH_ORDER_FILTER_ALIAS + + ordering_fields = ['delivery_date', 'shipment_date'] class SalesOrderShipmentDetail(RetrieveUpdateDestroyAPI): diff --git a/src/frontend/src/components/render/Instance.tsx b/src/frontend/src/components/render/Instance.tsx index 97c9c80c03..5938eb34ef 100644 --- a/src/frontend/src/components/render/Instance.tsx +++ b/src/frontend/src/components/render/Instance.tsx @@ -21,6 +21,7 @@ import { ModelInformationDict } from './ModelType'; import { RenderPurchaseOrder, RenderReturnOrder, + RenderReturnOrderLineItem, RenderSalesOrder, RenderSalesOrderShipment } from './Order'; @@ -73,6 +74,7 @@ const RendererLookup: EnumDictionary< [ModelType.purchaseorder]: RenderPurchaseOrder, [ModelType.purchaseorderlineitem]: RenderPurchaseOrder, [ModelType.returnorder]: RenderReturnOrder, + [ModelType.returnorderlineitem]: RenderReturnOrderLineItem, [ModelType.salesorder]: RenderSalesOrder, [ModelType.salesordershipment]: RenderSalesOrderShipment, [ModelType.stocklocation]: RenderStockLocation, diff --git a/src/frontend/src/components/render/ModelType.tsx b/src/frontend/src/components/render/ModelType.tsx index 5b4c1133cd..ef30cfa0c2 100644 --- a/src/frontend/src/components/render/ModelType.tsx +++ b/src/frontend/src/components/render/ModelType.tsx @@ -173,6 +173,11 @@ export const ModelInformationDict: ModelDict = { api_endpoint: ApiEndpoints.return_order_list, admin_url: '/order/returnorder/' }, + returnorderlineitem: { + label: t`Return Order Line Item`, + label_multiple: t`Return Order Line Items`, + api_endpoint: ApiEndpoints.return_order_line_list + }, address: { label: t`Address`, label_multiple: t`Addresses`, diff --git a/src/frontend/src/components/render/Order.tsx b/src/frontend/src/components/render/Order.tsx index d36277dfe4..416f45f1fd 100644 --- a/src/frontend/src/components/render/Order.tsx +++ b/src/frontend/src/components/render/Order.tsx @@ -62,6 +62,23 @@ export function RenderReturnOrder( ); } +export function RenderReturnOrderLineItem( + props: Readonly +): ReactNode { + const { instance } = props; + + return ( + + ); +} + /** * Inline rendering of a single SalesOrder instance */ diff --git a/src/frontend/src/components/render/StatusRenderer.tsx b/src/frontend/src/components/render/StatusRenderer.tsx index 42185690ee..680e9c37e1 100644 --- a/src/frontend/src/components/render/StatusRenderer.tsx +++ b/src/frontend/src/components/render/StatusRenderer.tsx @@ -2,6 +2,7 @@ import { Badge, Center, MantineSize } from '@mantine/core'; import { colorMap } from '../../defaults/backendMappings'; import { ModelType } from '../../enums/ModelType'; +import { resolveItem } from '../../functions/conversion'; import { useGlobalStatusState } from '../../states/StatusState'; interface StatusCodeInterface { @@ -132,10 +133,16 @@ export const StatusRenderer = ({ * Render the status badge in a table */ export function TableStatusRenderer( - type: ModelType + type: ModelType, + accessor?: string ): ((record: any) => any) | undefined { - return (record: any) => - record.status && ( -
{StatusRenderer({ status: record.status, type: type })}
+ return (record: any) => { + const status = resolveItem(record, accessor ?? 'status'); + + return ( + status && ( +
{StatusRenderer({ status: status, type: type })}
+ ) ); + }; } diff --git a/src/frontend/src/defaults/backendMappings.tsx b/src/frontend/src/defaults/backendMappings.tsx index 3339b27a8e..7ec0a776c2 100644 --- a/src/frontend/src/defaults/backendMappings.tsx +++ b/src/frontend/src/defaults/backendMappings.tsx @@ -9,8 +9,8 @@ import { ModelType } from '../enums/ModelType'; export const statusCodeList: Record = { BuildStatus: ModelType.build, PurchaseOrderStatus: ModelType.purchaseorder, - ReturnOrderLineStatus: ModelType.purchaseorderlineitem, ReturnOrderStatus: ModelType.returnorder, + ReturnOrderLineStatus: ModelType.returnorderlineitem, SalesOrderStatus: ModelType.salesorder, StockHistoryCode: ModelType.stockhistory, StockStatus: ModelType.stockitem, diff --git a/src/frontend/src/enums/ApiEndpoints.tsx b/src/frontend/src/enums/ApiEndpoints.tsx index 62b54cdc27..14d40cd9c9 100644 --- a/src/frontend/src/enums/ApiEndpoints.tsx +++ b/src/frontend/src/enums/ApiEndpoints.tsx @@ -132,6 +132,7 @@ export enum ApiEndpoints { sales_order_shipment_list = 'order/so/shipment/', return_order_list = 'order/ro/', + return_order_line_list = 'order/ro-line/', // Template API endpoints label_list = 'label/template/', diff --git a/src/frontend/src/enums/ModelType.tsx b/src/frontend/src/enums/ModelType.tsx index 6a1a1bb212..f20e3f1ec6 100644 --- a/src/frontend/src/enums/ModelType.tsx +++ b/src/frontend/src/enums/ModelType.tsx @@ -22,6 +22,7 @@ export enum ModelType { salesorder = 'salesorder', salesordershipment = 'salesordershipment', returnorder = 'returnorder', + returnorderlineitem = 'returnorderlineitem', importsession = 'importsession', address = 'address', contact = 'contact', diff --git a/src/frontend/src/forms/ReturnOrderForms.tsx b/src/frontend/src/forms/ReturnOrderForms.tsx new file mode 100644 index 0000000000..c582089ffb --- /dev/null +++ b/src/frontend/src/forms/ReturnOrderForms.tsx @@ -0,0 +1,38 @@ +import { useMemo } from 'react'; + +export function useReturnOrderLineItemFields({ + orderId, + customerId, + create +}: { + orderId: number; + customerId: number; + create?: boolean; +}) { + return useMemo(() => { + return { + order: { + disabled: true, + filters: { + customer_detail: true + } + }, + item: { + filters: { + customer: customerId, + part_detail: true, + serialized: true + } + }, + reference: {}, + outcome: { + hidden: create == true + }, + price: {}, + price_currency: {}, + target_date: {}, + notes: {}, + link: {} + }; + }, [create, orderId, customerId]); +} diff --git a/src/frontend/src/forms/SalesOrderForms.tsx b/src/frontend/src/forms/SalesOrderForms.tsx index 9c11eb5c50..6b137a367a 100644 --- a/src/frontend/src/forms/SalesOrderForms.tsx +++ b/src/frontend/src/forms/SalesOrderForms.tsx @@ -84,6 +84,23 @@ export function useSalesOrderLineItemFields({ return fields; } +export function useSalesOrderShipmentFields(): ApiFormFieldSet { + return useMemo(() => { + return { + order: { + disabled: true + }, + reference: {}, + shipment_date: {}, + delivery_date: {}, + tracking_number: {}, + invoice_number: {}, + link: {}, + notes: {} + }; + }, []); +} + export function useReturnOrderFields(): ApiFormFieldSet { return useMemo(() => { return { diff --git a/src/frontend/src/pages/sales/ReturnOrderDetail.tsx b/src/frontend/src/pages/sales/ReturnOrderDetail.tsx index d9a128b55a..10f8fe4b2e 100644 --- a/src/frontend/src/pages/sales/ReturnOrderDetail.tsx +++ b/src/frontend/src/pages/sales/ReturnOrderDetail.tsx @@ -26,7 +26,6 @@ import { UnlinkBarcodeAction, ViewBarcodeAction } from '../../components/items/ActionDropdown'; -import { PlaceholderPanel } from '../../components/items/Placeholder'; import InstanceDetail from '../../components/nav/InstanceDetail'; import { PageDetail } from '../../components/nav/PageDetail'; import { PanelGroup, PanelType } from '../../components/nav/PanelGroup'; @@ -43,6 +42,7 @@ import { import { useInstance } from '../../hooks/UseInstance'; import { useUserState } from '../../states/UserState'; import { AttachmentTable } from '../../tables/general/AttachmentTable'; +import ReturnOrderLineItemTable from '../../tables/sales/ReturnOrderLineItemTable'; /** * Detail page for a single ReturnOrder @@ -227,7 +227,12 @@ export default function ReturnOrderDetail() { name: 'line-items', label: t`Line Items`, icon: , - content: + content: ( + + ) }, { name: 'attachments', diff --git a/src/frontend/src/pages/sales/SalesOrderDetail.tsx b/src/frontend/src/pages/sales/SalesOrderDetail.tsx index 5fb6c861c6..6d4f35b1e3 100644 --- a/src/frontend/src/pages/sales/SalesOrderDetail.tsx +++ b/src/frontend/src/pages/sales/SalesOrderDetail.tsx @@ -7,8 +7,7 @@ import { IconNotes, IconPaperclip, IconTools, - IconTruckDelivery, - IconTruckLoading + IconTruckDelivery } from '@tabler/icons-react'; import { ReactNode, useMemo } from 'react'; import { useParams } from 'react-router-dom'; @@ -29,7 +28,6 @@ import { UnlinkBarcodeAction, ViewBarcodeAction } from '../../components/items/ActionDropdown'; -import { PlaceholderPanel } from '../../components/items/Placeholder'; import InstanceDetail from '../../components/nav/InstanceDetail'; import { PageDetail } from '../../components/nav/PageDetail'; import { PanelGroup, PanelType } from '../../components/nav/PanelGroup'; @@ -48,6 +46,7 @@ import { useUserState } from '../../states/UserState'; import { BuildOrderTable } from '../../tables/build/BuildOrderTable'; import { AttachmentTable } from '../../tables/general/AttachmentTable'; import SalesOrderLineItemTable from '../../tables/sales/SalesOrderLineItemTable'; +import SalesOrderShipmentTable from '../../tables/sales/SalesOrderShipmentTable'; /** * Detail page for a single SalesOrder @@ -258,16 +257,10 @@ export default function SalesOrderDetail() { ) }, { - name: 'pending-shipments', - label: t`Pending Shipments`, - icon: , - content: - }, - { - name: 'completed-shipments', - label: t`Completed Shipments`, + name: 'shipments', + label: t`Shipments`, icon: , - content: + content: }, { name: 'build-orders', diff --git a/src/frontend/src/tables/ColumnRenderers.tsx b/src/frontend/src/tables/ColumnRenderers.tsx index be7fe7a759..8fbcecfbd5 100644 --- a/src/frontend/src/tables/ColumnRenderers.tsx +++ b/src/frontend/src/tables/ColumnRenderers.tsx @@ -163,16 +163,19 @@ export function ProjectCodeColumn(props: TableColumnProps): TableColumn { export function StatusColumn({ model, sortable, - accessor + accessor, + title }: { model: ModelType; sortable?: boolean; accessor?: string; + title?: string; }) { return { accessor: accessor ?? 'status', sortable: sortable ?? true, - render: TableStatusRenderer(model) + title: title, + render: TableStatusRenderer(model, accessor ?? 'status') }; } diff --git a/src/frontend/src/tables/sales/ReturnOrderLineItemTable.tsx b/src/frontend/src/tables/sales/ReturnOrderLineItemTable.tsx new file mode 100644 index 0000000000..de93a2945f --- /dev/null +++ b/src/frontend/src/tables/sales/ReturnOrderLineItemTable.tsx @@ -0,0 +1,199 @@ +import { t } from '@lingui/macro'; +import { IconSquareArrowRight } from '@tabler/icons-react'; +import { useCallback, useMemo, useState } from 'react'; + +import { AddItemButton } from '../../components/buttons/AddItemButton'; +import { formatCurrency } from '../../defaults/formatters'; +import { ApiEndpoints } from '../../enums/ApiEndpoints'; +import { ModelType } from '../../enums/ModelType'; +import { UserRoles } from '../../enums/Roles'; +import { useReturnOrderLineItemFields } from '../../forms/ReturnOrderForms'; +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, + NoteColumn, + PartColumn, + ReferenceColumn, + StatusColumn +} from '../ColumnRenderers'; +import { StatusFilterOptions, TableFilter } from '../Filter'; +import { InvenTreeTable } from '../InvenTreeTable'; +import { RowDeleteAction, RowEditAction } from '../RowActions'; + +export default function ReturnOrderLineItemTable({ + orderId, + customerId +}: { + orderId: number; + customerId: number; +}) { + const table = useTable('return-order-line-item'); + const user = useUserState(); + + const [selectedLine, setSelectedLine] = useState(0); + + const newLineFields = useReturnOrderLineItemFields({ + orderId: orderId, + customerId: customerId, + create: true + }); + + const editLineFields = useReturnOrderLineItemFields({ + orderId: orderId, + customerId: customerId + }); + + const newLine = useCreateApiFormModal({ + url: ApiEndpoints.return_order_line_list, + title: t`Add Line Item`, + fields: newLineFields, + initialData: { + order: orderId + }, + table: table + }); + + const editLine = useEditApiFormModal({ + url: ApiEndpoints.return_order_line_list, + pk: selectedLine, + title: t`Edit Line Item`, + fields: editLineFields, + table: table + }); + + const deleteLine = useDeleteApiFormModal({ + url: ApiEndpoints.return_order_line_list, + pk: selectedLine, + title: t`Delete Line Item`, + table: table + }); + + const tableColumns: TableColumn[] = useMemo(() => { + return [ + { + accessor: 'part', + title: t`Part`, + switchable: false, + render: (record: any) => PartColumn(record?.part_detail) + }, + { + accessor: 'item', + title: t`Stock Item`, + switchable: false + }, + ReferenceColumn({}), + StatusColumn({ + model: ModelType.returnorderlineitem, + sortable: true, + accessor: 'outcome' + }), + { + accessor: 'price', + render: (record: any) => + formatCurrency(record.price, { currency: record.price_currency }) + }, + DateColumn({ + accessor: 'target_date', + title: t`Target Date` + }), + DateColumn({ + accessor: 'received_date', + title: t`Received Date` + }), + NoteColumn({ + accessor: 'notes' + }), + LinkColumn({}) + ]; + }, []); + + const tableFilters: TableFilter[] = useMemo(() => { + return [ + { + name: 'received', + label: t`Received`, + description: t`Show items which have been received` + }, + { + name: 'status', + label: t`Status`, + description: t`Filter by line item status`, + choiceFunction: StatusFilterOptions(ModelType.returnorderlineitem) + } + ]; + }, []); + + const tableActions = useMemo(() => { + return [ +