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 (
+ <>
+
+ >
+ );
+}