From 556a3161e88658c49903dcb993dcdf1474732911 Mon Sep 17 00:00:00 2001 From: Oliver Date: Sat, 10 Aug 2024 09:19:32 +1000 Subject: [PATCH] [PUI] Part po table (#7844) * Implement table for part purchase orders * Add filters * Improve table * Adjust PO actions based on status * Bump API version --- .../InvenTree/InvenTree/api_version.py | 5 +- src/backend/InvenTree/order/serializers.py | 5 + src/frontend/src/pages/part/PartDetail.tsx | 3 +- .../tables/part/PartPurchaseOrdersTable.tsx | 152 ++++++++++++++++++ .../purchasing/PurchaseOrderLineItemTable.tsx | 24 ++- 5 files changed, 181 insertions(+), 8 deletions(-) create mode 100644 src/frontend/src/tables/part/PartPurchaseOrdersTable.tsx diff --git a/src/backend/InvenTree/InvenTree/api_version.py b/src/backend/InvenTree/InvenTree/api_version.py index a0f0d02acd..0f0a6a6bde 100644 --- a/src/backend/InvenTree/InvenTree/api_version.py +++ b/src/backend/InvenTree/InvenTree/api_version.py @@ -1,13 +1,16 @@ """InvenTree API version information.""" # InvenTree API version -INVENTREE_API_VERSION = 235 +INVENTREE_API_VERSION = 236 """Increment this API version number whenever there is a significant change to the API that any clients need to know about.""" INVENTREE_API_TEXT = """ +v236 - 2024-08-10 : https://github.com/inventree/InvenTree/pull/7844 + - Adds "supplier_name" to the PurchaseOrder API serializer + v235 - 2024-08-08 : https://github.com/inventree/InvenTree/pull/7837 - Adds "on_order" quantity to SalesOrderLineItem serializer - Adds "building" quantity to SalesOrderLineItem serializer diff --git a/src/backend/InvenTree/order/serializers.py b/src/backend/InvenTree/order/serializers.py index f24b009e38..f94ed0c293 100644 --- a/src/backend/InvenTree/order/serializers.py +++ b/src/backend/InvenTree/order/serializers.py @@ -231,6 +231,7 @@ class PurchaseOrderSerializer( 'supplier', 'supplier_detail', 'supplier_reference', + 'supplier_name', 'total_price', 'order_currency', ]) @@ -278,6 +279,10 @@ class PurchaseOrderSerializer( return queryset + supplier_name = serializers.CharField( + source='supplier.name', read_only=True, label=_('Supplier Name') + ) + supplier_detail = CompanyBriefSerializer( source='supplier', many=False, read_only=True ) diff --git a/src/frontend/src/pages/part/PartDetail.tsx b/src/frontend/src/pages/part/PartDetail.tsx index 821766af5f..638f156e5d 100644 --- a/src/frontend/src/pages/part/PartDetail.tsx +++ b/src/frontend/src/pages/part/PartDetail.tsx @@ -89,6 +89,7 @@ import BuildAllocatedStockTable from '../../tables/build/BuildAllocatedStockTabl import { BuildOrderTable } from '../../tables/build/BuildOrderTable'; import { AttachmentTable } from '../../tables/general/AttachmentTable'; import { PartParameterTable } from '../../tables/part/PartParameterTable'; +import PartPurchaseOrdersTable from '../../tables/part/PartPurchaseOrdersTable'; import PartTestTemplateTable from '../../tables/part/PartTestTemplateTable'; import { PartVariantTable } from '../../tables/part/PartVariantTable'; import { RelatedPartTable } from '../../tables/part/RelatedPartTable'; @@ -648,7 +649,7 @@ export default function PartDetail() { label: t`Purchase Orders`, icon: , hidden: !part.purchaseable, - content: + content: }, { name: 'sales_orders', diff --git a/src/frontend/src/tables/part/PartPurchaseOrdersTable.tsx b/src/frontend/src/tables/part/PartPurchaseOrdersTable.tsx new file mode 100644 index 0000000000..4857f75dcf --- /dev/null +++ b/src/frontend/src/tables/part/PartPurchaseOrdersTable.tsx @@ -0,0 +1,152 @@ +import { t } from '@lingui/macro'; +import { Text } from '@mantine/core'; +import { useMemo } from 'react'; + +import { ProgressBar } from '../../components/items/ProgressBar'; +import { formatCurrency } from '../../defaults/formatters'; +import { ApiEndpoints } from '../../enums/ApiEndpoints'; +import { ModelType } from '../../enums/ModelType'; +import { useTable } from '../../hooks/UseTable'; +import { apiUrl } from '../../states/ApiState'; +import { useUserState } from '../../states/UserState'; +import { TableColumn } from '../Column'; +import { DateColumn, ReferenceColumn, StatusColumn } from '../ColumnRenderers'; +import { StatusFilterOptions, TableFilter } from '../Filter'; +import { InvenTreeTable } from '../InvenTreeTable'; +import { TableHoverCard } from '../TableHoverCard'; + +export default function PartPurchaseOrdersTable({ + partId +}: { + partId: number; +}) { + const table = useTable('partpurchaseorders'); + const user = useUserState(); + + const tableColumns: TableColumn[] = useMemo(() => { + return [ + ReferenceColumn({ + accessor: 'order_detail.reference', + sortable: true, + switchable: false, + title: t`Purchase Order` + }), + StatusColumn({ + accessor: 'order_detail.status', + sortable: true, + title: t`Status`, + model: ModelType.purchaseorder + }), + { + accessor: 'order_detail.supplier_name', + title: t`Supplier`, + sortable: false, + switchable: true + }, + { + accessor: 'supplier_part_detail.SKU', + ordering: 'sku', + title: t`Supplier Part`, + sortable: true + }, + { + accessor: 'supplier_part_detail.manufacturer_part_detail.MPN', + ordering: 'mpn', + title: t`Manufacturer Part`, + sortable: true + }, + { + accessor: 'quantity', + switchable: false, + render: (record: any) => { + 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) { + let total = record.quantity * supplier_part.pack_quantity_native; + + extra.push( + + {t`Pack Quantity`}: {supplier_part.pack_quantity} + + ); + + extra.push( + + {t`Total Quantity`}: {total} {part?.units} + + ); + } + + return ( + + } + extra={extra} + title={t`Quantity`} + /> + ); + } + }, + DateColumn({ + accessor: 'target_date', + title: t`Target Date` + }), + { + accessor: 'purchase_price', + render: (record: any) => + formatCurrency(record.purchase_price, { + currency: record.purchase_price_currency + }) + } + ]; + }, []); + + const tableFilters: TableFilter[] = useMemo(() => { + return [ + { + name: 'pending', + label: t`Pending`, + description: t`Show pending orders` + }, + { + name: 'received', + label: t`Received`, + description: t`Show received items` + }, + { + name: 'order_status', + label: t`Order Status`, + description: t`Filter by order status`, + choiceFunction: StatusFilterOptions(ModelType.purchaseorder) + } + ]; + }, []); + + return ( + <> + + + ); +} diff --git a/src/frontend/src/tables/purchasing/PurchaseOrderLineItemTable.tsx b/src/frontend/src/tables/purchasing/PurchaseOrderLineItemTable.tsx index 5d10c1484c..895a40beff 100644 --- a/src/frontend/src/tables/purchasing/PurchaseOrderLineItemTable.tsx +++ b/src/frontend/src/tables/purchasing/PurchaseOrderLineItemTable.tsx @@ -21,6 +21,7 @@ import { useDeleteApiFormModal, useEditApiFormModal } from '../../hooks/UseForm'; +import useStatusCodes from '../../hooks/UseStatusCodes'; import { useTable } from '../../hooks/UseTable'; import { apiUrl } from '../../states/ApiState'; import { useUserState } from '../../states/UserState'; @@ -273,13 +274,23 @@ export function PurchaseOrderLineItemTable({ table: table }); + const poStatus = useStatusCodes({ modelType: ModelType.purchaseorder }); + + const orderOpen: boolean = useMemo(() => { + return ( + order.status == poStatus.PENDING || + order.status == poStatus.PLACED || + order.status == poStatus.ON_HOLD + ); + }, [order, poStatus]); + const rowActions = useCallback( (record: any) => { let received = (record?.received ?? 0) >= (record?.quantity ?? 0); return [ { - hidden: received, + hidden: received || !orderOpen, title: t`Receive line item`, icon: , color: 'green', @@ -296,7 +307,7 @@ export function PurchaseOrderLineItemTable({ } }), RowDuplicateAction({ - hidden: !user.hasAddRole(UserRoles.purchase_order), + hidden: !orderOpen || !user.hasAddRole(UserRoles.purchase_order), onClick: () => { setInitialData({ ...record }); newLine.open(); @@ -311,14 +322,14 @@ export function PurchaseOrderLineItemTable({ }) ]; }, - [orderId, user] + [orderId, user, orderOpen] ); // Custom table actions const tableActions = useMemo(() => { return [