mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
[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:
parent
dce6cf6b01
commit
90a918e6d2
@ -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/',
|
||||
|
@ -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 />
|
||||
)
|
||||
|
@ -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',
|
||||
|
@ -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`,
|
||||
|
@ -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',
|
||||
|
@ -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')
|
||||
};
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
|
135
src/frontend/src/tables/sales/SalesOrderAllocationTable.tsx
Normal file
135
src/frontend/src/tables/sales/SalesOrderAllocationTable.tsx
Normal 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
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
Loading…
Reference in New Issue
Block a user