[PUI] Allocation Tables (#7836)

* Skeleton panel and placeholder tables

* Implement build order allocation table

* Refactor and repurpose existing table

* Add allocations table to stock item page

* Skeleton for <SalesOrderAllocationTable />

* Implement sales order allocation table(s)
This commit is contained in:
Oliver 2024-08-08 15:43:11 +10:00 committed by GitHub
parent dce6cf6b01
commit 90a918e6d2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 335 additions and 30 deletions

View File

@ -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/',

View File

@ -283,7 +283,7 @@ export default function BuildDetail() {
label: t`Allocated Stock`,
icon: <IconList />,
content: build.pk ? (
<BuildAllocatedStockTable buildId={build.pk} />
<BuildAllocatedStockTable buildId={build.pk} showPartInfo allowEdit />
) : (
<Skeleton />
)

View File

@ -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: <IconBookmarks />,
hidden: !part.component && !part.salable,
content: <PlaceholderPanel />
content: (
<Accordion
multiple={true}
defaultValue={['buildallocations', 'salesallocations']}
>
{part.component && (
<Accordion.Item value="buildallocations" key="buildallocations">
<Accordion.Control>
<StylishText size="lg">{t`Build Order Allocations`}</StylishText>
</Accordion.Control>
<Accordion.Panel>
<BuildAllocatedStockTable
partId={part.pk}
modelField="build"
modelTarget={ModelType.build}
showBuildInfo
/>
</Accordion.Panel>
</Accordion.Item>
)}
{part.salable && (
<Accordion.Item value="salesallocations" key="salesallocations">
<Accordion.Control>
<StylishText size="lg">{t`Sales Order Allocations`}</StylishText>
</Accordion.Control>
<Accordion.Panel>
<SalesOrderAllocationTable
partId={part.pk}
modelField="order"
modelTarget={ModelType.salesorder}
showOrderInfo
/>
</Accordion.Panel>
</Accordion.Item>
)}
</Accordion>
)
},
{
name: 'bom',

View File

@ -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: <IconTruckDelivery />,
content: <SalesOrderShipmentTable orderId={order.pk} />
},
{
name: 'allocations',
label: t`Allocated Stock`,
icon: <IconBookmark />,
content: (
<SalesOrderAllocationTable
orderId={order.pk}
showPartInfo
allowEdit
modelField="item"
modelTarget={ModelType.stockitem}
/>
)
},
{
name: 'build-orders',
label: t`Build Orders`,

View File

@ -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: <IconBookmark />,
hidden:
!stockitem?.part_detail?.salable &&
!stockitem?.part_detail?.component,
content: <PlaceholderPanel />
hidden: !showSalesAlloctions && !showBuildAllocations,
content: (
<Accordion
multiple={true}
defaultValue={['buildallocations', 'salesallocations']}
>
{showBuildAllocations && (
<Accordion.Item value="buildallocations" key="buildallocations">
<Accordion.Control>
<StylishText size="lg">{t`Build Order Allocations`}</StylishText>
</Accordion.Control>
<Accordion.Panel>
<BuildAllocatedStockTable
stockId={stockitem.pk}
modelField="build"
modelTarget={ModelType.build}
showBuildInfo
/>
</Accordion.Panel>
</Accordion.Item>
)}
{showSalesAlloctions && (
<Accordion.Item value="salesallocations" key="salesallocations">
<Accordion.Control>
<StylishText size="lg">{t`Sales Order Allocations`}</StylishText>
</Accordion.Control>
<Accordion.Panel>
<SalesOrderAllocationTable
stockId={stockitem.pk}
modelField="order"
modelTarget={ModelType.salesorder}
showOrderInfo
/>
</Accordion.Panel>
</Accordion.Item>
)}
</Accordion>
)
},
{
name: 'testdata',

View File

@ -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')
};
}

View File

@ -251,7 +251,16 @@ export function InvenTreeTable<T = any>({
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<T = any>({
// 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) {

View File

@ -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
}}
/>
</>

View File

@ -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 (
<>
<InvenTreeTable
url={apiUrl(ApiEndpoints.sales_order_allocation_list)}
tableState={table}
columns={tableColumns}
props={{
params: {
part_detail: showPartInfo ?? false,
order_detail: showOrderInfo ?? false,
item_detail: true,
location_detail: true,
part: partId,
order: orderId,
stock_item: stockId
},
rowActions: rowActions,
tableFilters: tableFilters,
modelField: modelField ?? 'order',
modelType: modelTarget ?? ModelType.salesorder
}}
/>
</>
);
}