diff --git a/src/frontend/src/enums/ApiEndpoints.tsx b/src/frontend/src/enums/ApiEndpoints.tsx index 80ae3443ae..3cb3b1b1f0 100644 --- a/src/frontend/src/enums/ApiEndpoints.tsx +++ b/src/frontend/src/enums/ApiEndpoints.tsx @@ -141,6 +141,7 @@ export enum ApiEndpoints { sales_order_ship = 'order/so/:id/ship/', sales_order_complete = 'order/so/:id/complete/', sales_order_line_list = 'order/so-line/', + sales_order_allocation_list = 'order/so-allocation/', sales_order_shipment_list = 'order/so/shipment/', return_order_list = 'order/ro/', diff --git a/src/frontend/src/pages/build/BuildDetail.tsx b/src/frontend/src/pages/build/BuildDetail.tsx index 0896a39d70..1c6a9c9e3f 100644 --- a/src/frontend/src/pages/build/BuildDetail.tsx +++ b/src/frontend/src/pages/build/BuildDetail.tsx @@ -283,7 +283,7 @@ export default function BuildDetail() { label: t`Allocated Stock`, icon: , content: build.pk ? ( - + ) : ( ) diff --git a/src/frontend/src/pages/part/PartDetail.tsx b/src/frontend/src/pages/part/PartDetail.tsx index 9a59d2a72b..70448e26d4 100644 --- a/src/frontend/src/pages/part/PartDetail.tsx +++ b/src/frontend/src/pages/part/PartDetail.tsx @@ -1,5 +1,13 @@ import { t } from '@lingui/macro'; -import { Alert, Grid, Skeleton, Space, Stack, Text } from '@mantine/core'; +import { + Accordion, + Alert, + Grid, + Skeleton, + Space, + Stack, + Text +} from '@mantine/core'; import { IconBookmarks, IconBuilding, @@ -48,6 +56,7 @@ import { ViewBarcodeAction } from '../../components/items/ActionDropdown'; import { PlaceholderPanel } from '../../components/items/Placeholder'; +import { StylishText } from '../../components/items/StylishText'; import InstanceDetail from '../../components/nav/InstanceDetail'; import NavigationTree from '../../components/nav/NavigationTree'; import { PageDetail } from '../../components/nav/PageDetail'; @@ -76,6 +85,7 @@ import { useGlobalSettingsState } from '../../states/SettingsState'; import { useUserState } from '../../states/UserState'; import { BomTable } from '../../tables/bom/BomTable'; import { UsedInTable } from '../../tables/bom/UsedInTable'; +import BuildAllocatedStockTable from '../../tables/build/BuildAllocatedStockTable'; import { BuildOrderTable } from '../../tables/build/BuildOrderTable'; import { AttachmentTable } from '../../tables/general/AttachmentTable'; import { PartParameterTable } from '../../tables/part/PartParameterTable'; @@ -84,6 +94,7 @@ import { PartVariantTable } from '../../tables/part/PartVariantTable'; import { RelatedPartTable } from '../../tables/part/RelatedPartTable'; import { ManufacturerPartTable } from '../../tables/purchasing/ManufacturerPartTable'; import { SupplierPartTable } from '../../tables/purchasing/SupplierPartTable'; +import SalesOrderAllocationTable from '../../tables/sales/SalesOrderAllocationTable'; import { SalesOrderTable } from '../../tables/sales/SalesOrderTable'; import { StockItemTable } from '../../tables/stock/StockItemTable'; import { TestStatisticsTable } from '../../tables/stock/TestStatisticsTable'; @@ -539,7 +550,43 @@ export default function PartDetail() { label: t`Allocations`, icon: , hidden: !part.component && !part.salable, - content: + content: ( + + {part.component && ( + + + {t`Build Order Allocations`} + + + + + + )} + {part.salable && ( + + + {t`Sales Order Allocations`} + + + + + + )} + + ) }, { name: 'bom', diff --git a/src/frontend/src/pages/sales/SalesOrderDetail.tsx b/src/frontend/src/pages/sales/SalesOrderDetail.tsx index fbafd71c22..e2e38de37e 100644 --- a/src/frontend/src/pages/sales/SalesOrderDetail.tsx +++ b/src/frontend/src/pages/sales/SalesOrderDetail.tsx @@ -1,6 +1,8 @@ import { t } from '@lingui/macro'; import { Grid, Skeleton, Stack } from '@mantine/core'; import { + IconBook, + IconBookmark, IconDots, IconInfoCircle, IconList, @@ -49,6 +51,7 @@ import { apiUrl } from '../../states/ApiState'; import { useUserState } from '../../states/UserState'; import { BuildOrderTable } from '../../tables/build/BuildOrderTable'; import { AttachmentTable } from '../../tables/general/AttachmentTable'; +import SalesOrderAllocationTable from '../../tables/sales/SalesOrderAllocationTable'; import SalesOrderLineItemTable from '../../tables/sales/SalesOrderLineItemTable'; import SalesOrderShipmentTable from '../../tables/sales/SalesOrderShipmentTable'; @@ -272,6 +275,20 @@ export default function SalesOrderDetail() { icon: , content: }, + { + name: 'allocations', + label: t`Allocated Stock`, + icon: , + content: ( + + ) + }, { name: 'build-orders', label: t`Build Orders`, diff --git a/src/frontend/src/pages/stock/StockDetail.tsx b/src/frontend/src/pages/stock/StockDetail.tsx index f92b287a12..247bdd44fe 100644 --- a/src/frontend/src/pages/stock/StockDetail.tsx +++ b/src/frontend/src/pages/stock/StockDetail.tsx @@ -1,5 +1,5 @@ import { t } from '@lingui/macro'; -import { Grid, Skeleton, Stack } from '@mantine/core'; +import { Accordion, Grid, Skeleton, Stack } from '@mantine/core'; import { IconBookmark, IconBoxPadding, @@ -33,6 +33,7 @@ import { ViewBarcodeAction } from '../../components/items/ActionDropdown'; import { PlaceholderPanel } from '../../components/items/Placeholder'; +import { StylishText } from '../../components/items/StylishText'; import InstanceDetail from '../../components/nav/InstanceDetail'; import NavigationTree from '../../components/nav/NavigationTree'; import { PageDetail } from '../../components/nav/PageDetail'; @@ -58,7 +59,9 @@ import { } from '../../hooks/UseForm'; import { useInstance } from '../../hooks/UseInstance'; import { useUserState } from '../../states/UserState'; +import BuildAllocatedStockTable from '../../tables/build/BuildAllocatedStockTable'; import { AttachmentTable } from '../../tables/general/AttachmentTable'; +import SalesOrderAllocationTable from '../../tables/sales/SalesOrderAllocationTable'; import InstalledItemsTable from '../../tables/stock/InstalledItemsTable'; import { StockItemTable } from '../../tables/stock/StockItemTable'; import StockItemTestResultTable from '../../tables/stock/StockItemTestResultTable'; @@ -268,6 +271,19 @@ export default function StockDetail() { ); }, [stockitem, instanceQuery]); + const showBuildAllocations = useMemo(() => { + // Determine if "build allocations" should be shown for this stock item + return ( + stockitem?.part_detail?.component && // Must be a "component" + !stockitem?.sales_order && // Must not be assigned to a sales order + !stockitem?.belongs_to + ); // Must not be installed into another item + }, [stockitem]); + + const showSalesAlloctions = useMemo(() => { + return stockitem?.part_detail?.salable; + }, [stockitem]); + const stockPanels: PanelType[] = useMemo(() => { return [ { @@ -290,10 +306,44 @@ export default function StockDetail() { name: 'allocations', label: t`Allocations`, icon: , - hidden: - !stockitem?.part_detail?.salable && - !stockitem?.part_detail?.component, - content: + hidden: !showSalesAlloctions && !showBuildAllocations, + content: ( + + {showBuildAllocations && ( + + + {t`Build Order Allocations`} + + + + + + )} + {showSalesAlloctions && ( + + + {t`Sales Order Allocations`} + + + + + + )} + + ) }, { name: 'testdata', diff --git a/src/frontend/src/tables/ColumnRenderers.tsx b/src/frontend/src/tables/ColumnRenderers.tsx index 8fbcecfbd5..49ba57e678 100644 --- a/src/frontend/src/tables/ColumnRenderers.tsx +++ b/src/frontend/src/tables/ColumnRenderers.tsx @@ -164,17 +164,20 @@ export function StatusColumn({ model, sortable, accessor, - title + title, + hidden }: { model: ModelType; sortable?: boolean; accessor?: string; + hidden?: boolean; title?: string; }) { return { accessor: accessor ?? 'status', sortable: sortable ?? true, title: title, + hidden: hidden, render: TableStatusRenderer(model, accessor ?? 'status') }; } diff --git a/src/frontend/src/tables/InvenTreeTable.tsx b/src/frontend/src/tables/InvenTreeTable.tsx index 9b113960c4..9419c12777 100644 --- a/src/frontend/src/tables/InvenTreeTable.tsx +++ b/src/frontend/src/tables/InvenTreeTable.tsx @@ -251,7 +251,16 @@ export function InvenTreeTable({ if (props.enableColumnSwitching == false) { return false; } else { - return columns.some((col: TableColumn) => col.switchable ?? true); + return columns.some((col: TableColumn) => { + if (col.hidden == true) { + // Not a switchable column - is hidden + return false; + } else if (col.switchable == false) { + return false; + } else { + return true; + } + }); } }, [columns, props.enableColumnSwitching]); @@ -264,19 +273,21 @@ export function InvenTreeTable({ // Update column visibility when hiddenColumns change const dataColumns: any = useMemo(() => { - let cols = columns.map((col) => { - let hidden: boolean = col.hidden ?? false; + let cols = columns + .filter((col) => col?.hidden != true) + .map((col) => { + let hidden: boolean = col.hidden ?? false; - if (col.switchable ?? true) { - hidden = tableState.hiddenColumns.includes(col.accessor); - } + if (col.switchable ?? true) { + hidden = tableState.hiddenColumns.includes(col.accessor); + } - return { - ...col, - hidden: hidden, - title: col.title ?? fieldNames[col.accessor] ?? `${col.accessor}` - }; - }); + return { + ...col, + hidden: hidden, + title: col.title ?? fieldNames[col.accessor] ?? `${col.accessor}` + }; + }); // If row actions are available, add a column for them if (tableProps.rowActions) { diff --git a/src/frontend/src/tables/build/BuildAllocatedStockTable.tsx b/src/frontend/src/tables/build/BuildAllocatedStockTable.tsx index 5452590fa9..b2b9df3ee1 100644 --- a/src/frontend/src/tables/build/BuildAllocatedStockTable.tsx +++ b/src/frontend/src/tables/build/BuildAllocatedStockTable.tsx @@ -12,7 +12,12 @@ import { useTable } from '../../hooks/UseTable'; import { apiUrl } from '../../states/ApiState'; import { useUserState } from '../../states/UserState'; import { TableColumn } from '../Column'; -import { LocationColumn, PartColumn } from '../ColumnRenderers'; +import { + LocationColumn, + PartColumn, + ReferenceColumn, + StatusColumn +} from '../ColumnRenderers'; import { TableFilter } from '../Filter'; import { InvenTreeTable } from '../InvenTreeTable'; import { RowDeleteAction, RowEditAction } from '../RowActions'; @@ -21,12 +26,26 @@ import { RowDeleteAction, RowEditAction } from '../RowActions'; * Render a table of allocated stock for a build. */ export default function BuildAllocatedStockTable({ - buildId + buildId, + stockId, + partId, + showBuildInfo, + showPartInfo, + allowEdit, + modelTarget, + modelField }: { - buildId: number; + buildId?: number; + stockId?: number; + partId?: number; + showPartInfo?: boolean; + showBuildInfo?: boolean; + allowEdit?: boolean; + modelTarget?: ModelType; + modelField?: string; }) { const user = useUserState(); - const table = useTable('build-allocated-stock'); + const table = useTable('buildallocatedstock'); const tableFilters: TableFilter[] = useMemo(() => { return [ @@ -40,14 +59,33 @@ export default function BuildAllocatedStockTable({ const tableColumns: TableColumn[] = useMemo(() => { return [ + ReferenceColumn({ + accessor: 'build_detail.reference', + title: t`Build Order`, + switchable: false, + hidden: showBuildInfo != true + }), + { + accessor: 'build_detail.title', + title: t`Description`, + hidden: showBuildInfo != true + }, + StatusColumn({ + accessor: 'build_detail.status', + model: ModelType.build, + title: t`Order Status`, + hidden: showBuildInfo != true + }), { accessor: 'part', + hidden: !showPartInfo, title: t`Part`, sortable: true, switchable: false, render: (record: any) => PartColumn(record.part_detail) }, { + hidden: !showPartInfo, accessor: 'bom_reference', title: t`Reference`, sortable: true, @@ -149,18 +187,21 @@ export default function BuildAllocatedStockTable({ props={{ params: { build: buildId, - part_detail: true, + part: partId, + stock_item: stockId, + build_detail: showBuildInfo ?? false, + part_detail: showPartInfo ?? false, location_detail: true, stock_detail: true, supplier_detail: true }, - enableBulkDelete: user.hasDeleteRole(UserRoles.build), + enableBulkDelete: allowEdit && user.hasDeleteRole(UserRoles.build), enableDownload: true, - enableSelection: true, + enableSelection: allowEdit && user.hasDeleteRole(UserRoles.build), rowActions: rowActions, tableFilters: tableFilters, - modelField: 'stock_item', - modelType: ModelType.stockitem + modelField: modelField ?? 'stock_item', + modelType: modelTarget ?? ModelType.stockitem }} /> diff --git a/src/frontend/src/tables/sales/SalesOrderAllocationTable.tsx b/src/frontend/src/tables/sales/SalesOrderAllocationTable.tsx new file mode 100644 index 0000000000..8d2a2907c3 --- /dev/null +++ b/src/frontend/src/tables/sales/SalesOrderAllocationTable.tsx @@ -0,0 +1,135 @@ +import { t } from '@lingui/macro'; +import { useCallback, useMemo } from 'react'; + +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 { + LocationColumn, + PartColumn, + ReferenceColumn, + StatusColumn +} from '../ColumnRenderers'; +import { TableFilter } from '../Filter'; +import { InvenTreeTable } from '../InvenTreeTable'; + +export default function SalesOrderAllocationTable({ + partId, + stockId, + orderId, + showPartInfo, + showOrderInfo, + allowEdit, + modelTarget, + modelField +}: { + partId?: number; + stockId?: number; + orderId?: number; + showPartInfo?: boolean; + showOrderInfo?: boolean; + allowEdit?: boolean; + modelTarget?: ModelType; + modelField?: string; +}) { + const user = useUserState(); + const table = useTable('salesorderallocations'); + + const tableFilters: TableFilter[] = useMemo(() => { + return []; + }, []); + + const tableColumns: TableColumn[] = useMemo(() => { + return [ + ReferenceColumn({ + accessor: 'order_detail.reference', + title: t`Sales Order`, + switchable: false, + hidden: showOrderInfo != true + }), + { + accessor: 'order_detail.description', + title: t`Description`, + hidden: showOrderInfo != true + }, + StatusColumn({ + accessor: 'order_detail.status', + model: ModelType.salesorder, + title: t`Order Status`, + hidden: showOrderInfo != true + }), + { + accessor: 'part', + hidden: showPartInfo != true, + title: t`Part`, + sortable: true, + switchable: false, + render: (record: any) => PartColumn(record.part_detail) + }, + { + accessor: 'quantity', + title: t`Allocated Quantity`, + sortable: true + }, + { + accessor: 'serial', + title: t`Serial Number`, + sortable: false, + switchable: true, + render: (record: any) => record?.item_detail?.serial + }, + { + accessor: 'batch', + title: t`Batch Code`, + sortable: false, + switchable: true, + render: (record: any) => record?.item_detail?.batch + }, + { + accessor: 'available', + title: t`Available Quantity`, + render: (record: any) => record?.item_detail?.quantity + }, + LocationColumn({ + accessor: 'location_detail', + switchable: true, + sortable: true + }) + ]; + }, []); + + const rowActions = useCallback( + (record: any) => { + return []; + }, + [user] + ); + + return ( + <> + + + ); +}