mirror of
https://github.com/inventree/InvenTree.git
synced 2025-07-02 19:50:59 +00:00
[Feature] External build order (#9312)
* Add BuildOrder reference to PurchaseOrderLineItem * Add setting to enable / disable external build orders * Fix for supplier part detail * Update forms * Filter build list by "external" status * Add "external" attribute to BuildOrder * Filter by external build when selecting against purchase order line item * Add frontend elements * Prevent creation of build outputs * Tweak related model field - Send filters when fetching initial data * Fix migrations * Fix some existing typos * Add build info when receiving line items * Logic fix * Bump API version * Updated relationship * Add external orders tab for order * Display table of external purchase orders against a build order * Fix permissions * Tweak field definition * Add unit tests * Tweak api_version.py * Playwright testing * Fix discrepancy in 'building' filter * Add basic documentation ( more work required ) * Tweak docs macros * Migration fix * Adjust build page tabs * Fix imports * Fix broken import * Update playywright tests * Bump API version * Handle DB issues * Improve filter * Cleaner code * Fix column ordering bug * Add filters to build output table * Documentation * Tweak unit test * Add "scheduled_for_production" field * Add helper function to part model * Cleanup
This commit is contained in:
@ -410,6 +410,7 @@ function ProgressBarValue(props: Readonly<FieldProps>) {
|
||||
|
||||
return (
|
||||
<ProgressBar
|
||||
size='lg'
|
||||
value={props.field_data.progress}
|
||||
maximum={props.field_data.total}
|
||||
progressLabel
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { forwardRef, useImperativeHandle, useState } from 'react';
|
||||
|
||||
import { ApiEndpoints, apiUrl } from '@lib/index';
|
||||
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
|
||||
import { apiUrl } from '@lib/functions/Api';
|
||||
import { api } from '../../../../App';
|
||||
import type { PreviewAreaComponent } from '../TemplateEditor';
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { ApiEndpoints, apiUrl } from '@lib/index';
|
||||
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
|
||||
import { apiUrl } from '@lib/functions/Api';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import {
|
||||
|
@ -71,24 +71,30 @@ export function RelatedModelField({
|
||||
return;
|
||||
}
|
||||
|
||||
api.get(url).then((response) => {
|
||||
const pk_field = definition.pk_field ?? 'pk';
|
||||
if (response.data?.[pk_field]) {
|
||||
const value = {
|
||||
value: response.data[pk_field],
|
||||
data: response.data
|
||||
};
|
||||
const params = definition?.filters ?? {};
|
||||
|
||||
// Run custom callback for this field (if provided)
|
||||
if (definition.onValueChange) {
|
||||
definition.onValueChange(response.data[pk_field], response.data);
|
||||
api
|
||||
.get(url, {
|
||||
params: params
|
||||
})
|
||||
.then((response) => {
|
||||
const pk_field = definition.pk_field ?? 'pk';
|
||||
if (response.data?.[pk_field]) {
|
||||
const value = {
|
||||
value: response.data[pk_field],
|
||||
data: response.data
|
||||
};
|
||||
|
||||
// Run custom callback for this field (if provided)
|
||||
if (definition.onValueChange) {
|
||||
definition.onValueChange(response.data[pk_field], response.data);
|
||||
}
|
||||
|
||||
setInitialData(value);
|
||||
dataRef.current = [value];
|
||||
setPk(response.data[pk_field]);
|
||||
}
|
||||
|
||||
setInitialData(value);
|
||||
dataRef.current = [value];
|
||||
setPk(response.data[pk_field]);
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
setPk(null);
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
|
||||
import { UserRoles } from '@lib/enums/Roles';
|
||||
import { apiUrl } from '@lib/functions/Api';
|
||||
import { UserRoles } from '@lib/index';
|
||||
import type { ApiFormFieldSet } from '@lib/types/Forms';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { Table } from '@mantine/core';
|
||||
|
@ -126,7 +126,8 @@ export function useBuildOrderFields({
|
||||
filters: {
|
||||
is_active: true
|
||||
}
|
||||
}
|
||||
},
|
||||
external: {}
|
||||
};
|
||||
|
||||
if (create) {
|
||||
@ -137,6 +138,10 @@ export function useBuildOrderFields({
|
||||
delete fields.project_code;
|
||||
}
|
||||
|
||||
if (!globalSettings.isSet('BUILDORDER_EXTERNAL_BUILDS', true)) {
|
||||
delete fields.external;
|
||||
}
|
||||
|
||||
return fields;
|
||||
}, [create, destination, batchCode, globalSettings]);
|
||||
}
|
||||
|
@ -64,9 +64,14 @@ export function usePurchaseOrderLineItemFields({
|
||||
orderId?: number;
|
||||
create?: boolean;
|
||||
}) {
|
||||
const globalSettings = useGlobalSettingsState();
|
||||
|
||||
const [purchasePrice, setPurchasePrice] = useState<string>('');
|
||||
const [autoPricing, setAutoPricing] = useState(true);
|
||||
|
||||
// Internal part information
|
||||
const [part, setPart] = useState<any>({});
|
||||
|
||||
useEffect(() => {
|
||||
if (autoPricing) {
|
||||
setPurchasePrice('');
|
||||
@ -92,6 +97,9 @@ export function usePurchaseOrderLineItemFields({
|
||||
active: true,
|
||||
part_active: true
|
||||
},
|
||||
onValueChange: (value, record) => {
|
||||
setPart(record?.part_detail ?? {});
|
||||
},
|
||||
adjustFilters: (adjust: ApiFormAdjustFilterType) => {
|
||||
return {
|
||||
...adjust.filters,
|
||||
@ -119,6 +127,14 @@ export function usePurchaseOrderLineItemFields({
|
||||
destination: {
|
||||
icon: <IconSitemap />
|
||||
},
|
||||
build_order: {
|
||||
disabled: !part?.assembly,
|
||||
filters: {
|
||||
external: true,
|
||||
outstanding: true,
|
||||
part: part?.pk
|
||||
}
|
||||
},
|
||||
notes: {
|
||||
icon: <IconNotes />
|
||||
},
|
||||
@ -127,12 +143,24 @@ export function usePurchaseOrderLineItemFields({
|
||||
}
|
||||
};
|
||||
|
||||
if (!globalSettings.isSet('BUILDORDER_EXTERNAL_BUILDS', false)) {
|
||||
delete fields.build_order;
|
||||
}
|
||||
|
||||
if (create) {
|
||||
fields['merge_items'] = {};
|
||||
}
|
||||
|
||||
return fields;
|
||||
}, [create, orderId, supplierId, autoPricing, purchasePrice]);
|
||||
}, [
|
||||
create,
|
||||
orderId,
|
||||
part,
|
||||
globalSettings,
|
||||
supplierId,
|
||||
autoPricing,
|
||||
purchasePrice
|
||||
]);
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
@ -32,7 +32,7 @@ import RemoveRowButton from '../components/buttons/RemoveRowButton';
|
||||
import { StandaloneField } from '../components/forms/StandaloneField';
|
||||
|
||||
import { apiUrl } from '@lib/functions/Api';
|
||||
import { getDetailUrl } from '@lib/index';
|
||||
import { getDetailUrl } from '@lib/functions/Navigation';
|
||||
import type {
|
||||
ApiFormAdjustFilterType,
|
||||
ApiFormFieldChoice,
|
||||
|
@ -248,6 +248,7 @@ export default function SystemSettings() {
|
||||
<GlobalSettingList
|
||||
keys={[
|
||||
'BUILDORDER_REFERENCE_PATTERN',
|
||||
'BUILDORDER_EXTERNAL_BUILDS',
|
||||
'BUILDORDER_REQUIRE_RESPONSIBLE',
|
||||
'BUILDORDER_REQUIRE_ACTIVE_PART',
|
||||
'BUILDORDER_REQUIRE_LOCKED_PART',
|
||||
|
@ -8,6 +8,7 @@ import {
|
||||
IconList,
|
||||
IconListCheck,
|
||||
IconListNumbers,
|
||||
IconShoppingCart,
|
||||
IconSitemap
|
||||
} from '@tabler/icons-react';
|
||||
import { useMemo } from 'react';
|
||||
@ -25,6 +26,7 @@ import {
|
||||
type DetailsField,
|
||||
DetailsTable
|
||||
} from '../../components/details/Details';
|
||||
import DetailsBadge from '../../components/details/DetailsBadge';
|
||||
import { DetailsImage } from '../../components/details/DetailsImage';
|
||||
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
|
||||
import {
|
||||
@ -49,12 +51,14 @@ import {
|
||||
} from '../../hooks/UseForm';
|
||||
import { useInstance } from '../../hooks/UseInstance';
|
||||
import useStatusCodes from '../../hooks/UseStatusCodes';
|
||||
import { useGlobalSettingsState } from '../../states/SettingsState';
|
||||
import { useUserState } from '../../states/UserState';
|
||||
import BuildAllocatedStockTable from '../../tables/build/BuildAllocatedStockTable';
|
||||
import BuildLineTable from '../../tables/build/BuildLineTable';
|
||||
import { BuildOrderTable } from '../../tables/build/BuildOrderTable';
|
||||
import BuildOrderTestTable from '../../tables/build/BuildOrderTestTable';
|
||||
import BuildOutputTable from '../../tables/build/BuildOutputTable';
|
||||
import { PurchaseOrderTable } from '../../tables/purchasing/PurchaseOrderTable';
|
||||
import { StockItemTable } from '../../tables/stock/StockItemTable';
|
||||
|
||||
/**
|
||||
@ -64,6 +68,7 @@ export default function BuildDetail() {
|
||||
const { id } = useParams();
|
||||
|
||||
const user = useUserState();
|
||||
const globalSettings = useGlobalSettingsState();
|
||||
|
||||
const buildStatus = useStatusCodes({ modelType: ModelType.build });
|
||||
|
||||
@ -124,6 +129,24 @@ export default function BuildDetail() {
|
||||
hidden:
|
||||
!build.status_custom_key || build.status_custom_key == build.status
|
||||
},
|
||||
{
|
||||
type: 'boolean',
|
||||
name: 'external',
|
||||
label: t`External`,
|
||||
icon: 'manufacturers',
|
||||
hidden: !build.external
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'purchase_order',
|
||||
label: t`Purchase Order`,
|
||||
icon: 'purchase_orders',
|
||||
copy: true,
|
||||
hidden: !build.external,
|
||||
value_formatter: () => {
|
||||
return 'TODO: external PO';
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'reference',
|
||||
@ -287,10 +310,38 @@ export default function BuildDetail() {
|
||||
},
|
||||
{
|
||||
name: 'line-items',
|
||||
label: t`Line Items`,
|
||||
label: t`Required Stock`,
|
||||
icon: <IconListNumbers />,
|
||||
content: build?.pk ? <BuildLineTable build={build} /> : <Skeleton />
|
||||
},
|
||||
{
|
||||
name: 'allocated-stock',
|
||||
label: t`Allocated Stock`,
|
||||
icon: <IconList />,
|
||||
hidden:
|
||||
build.status == buildStatus.COMPLETE ||
|
||||
build.status == buildStatus.CANCELLED,
|
||||
content: build.pk ? (
|
||||
<BuildAllocatedStockTable buildId={build.pk} showPartInfo allowEdit />
|
||||
) : (
|
||||
<Skeleton />
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'consumed-stock',
|
||||
label: t`Consumed Stock`,
|
||||
icon: <IconListCheck />,
|
||||
content: (
|
||||
<StockItemTable
|
||||
allowAdd={false}
|
||||
tableName='build-consumed'
|
||||
showLocation={false}
|
||||
params={{
|
||||
consumed_by: id
|
||||
}}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'incomplete-outputs',
|
||||
label: t`Incomplete Outputs`,
|
||||
@ -320,32 +371,18 @@ export default function BuildDetail() {
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'allocated-stock',
|
||||
label: t`Allocated Stock`,
|
||||
icon: <IconList />,
|
||||
hidden:
|
||||
build.status == buildStatus.COMPLETE ||
|
||||
build.status == buildStatus.CANCELLED,
|
||||
name: 'external-purchase-orders',
|
||||
label: t`External Orders`,
|
||||
icon: <IconShoppingCart />,
|
||||
content: build.pk ? (
|
||||
<BuildAllocatedStockTable buildId={build.pk} showPartInfo allowEdit />
|
||||
<PurchaseOrderTable externalBuildId={build.pk} />
|
||||
) : (
|
||||
<Skeleton />
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'consumed-stock',
|
||||
label: t`Consumed Stock`,
|
||||
icon: <IconListCheck />,
|
||||
content: (
|
||||
<StockItemTable
|
||||
allowAdd={false}
|
||||
tableName='build-consumed'
|
||||
showLocation={false}
|
||||
params={{
|
||||
consumed_by: id
|
||||
}}
|
||||
/>
|
||||
)
|
||||
),
|
||||
hidden:
|
||||
!user.hasViewRole(UserRoles.purchase_order) ||
|
||||
!build.external ||
|
||||
!globalSettings.isSet('BUILDORDER_EXTERNAL_BUILDS')
|
||||
},
|
||||
{
|
||||
name: 'child-orders',
|
||||
@ -377,7 +414,7 @@ export default function BuildDetail() {
|
||||
model_id: build.pk
|
||||
})
|
||||
];
|
||||
}, [build, id, user, buildStatus]);
|
||||
}, [build, id, user, buildStatus, globalSettings]);
|
||||
|
||||
const buildOrderFields = useBuildOrderFields({ create: false });
|
||||
|
||||
@ -531,6 +568,12 @@ export default function BuildDetail() {
|
||||
status={build.status_custom_key}
|
||||
type={ModelType.build}
|
||||
options={{ size: 'lg' }}
|
||||
/>,
|
||||
<DetailsBadge
|
||||
label={t`External`}
|
||||
color='blue'
|
||||
key='external'
|
||||
visible={build.external}
|
||||
/>
|
||||
];
|
||||
}, [build, instanceQuery]);
|
||||
|
@ -13,14 +13,25 @@ import PermissionDenied from '../../components/errors/PermissionDenied';
|
||||
import { PageDetail } from '../../components/nav/PageDetail';
|
||||
import type { PanelType } from '../../components/panels/Panel';
|
||||
import { PanelGroup } from '../../components/panels/PanelGroup';
|
||||
import { useGlobalSettingsState } from '../../states/SettingsState';
|
||||
import { useUserState } from '../../states/UserState';
|
||||
import { PartCategoryFilter } from '../../tables/Filter';
|
||||
import { BuildOrderTable } from '../../tables/build/BuildOrderTable';
|
||||
|
||||
function BuildOrderCalendar() {
|
||||
const globalSettings = useGlobalSettingsState();
|
||||
|
||||
const calendarFilters: TableFilter[] = useMemo(() => {
|
||||
return [PartCategoryFilter()];
|
||||
}, []);
|
||||
return [
|
||||
{
|
||||
name: 'external',
|
||||
label: t`External`,
|
||||
description: t`Show external build orders`,
|
||||
active: globalSettings.isSet('BUILDORDER_EXTERNAL_BUILDS')
|
||||
},
|
||||
PartCategoryFilter()
|
||||
];
|
||||
}, [globalSettings]);
|
||||
|
||||
return (
|
||||
<OrderCalendar
|
||||
|
@ -266,7 +266,10 @@ export default function SupplierPartDetail() {
|
||||
label: t`Purchase Orders`,
|
||||
icon: <IconShoppingCart />,
|
||||
content: supplierPart?.pk ? (
|
||||
<PurchaseOrderTable supplierPartId={supplierPart.pk} />
|
||||
<PurchaseOrderTable
|
||||
supplierId={supplierPart.supplier}
|
||||
supplierPartId={supplierPart.pk}
|
||||
/>
|
||||
) : (
|
||||
<Skeleton />
|
||||
)
|
||||
|
@ -288,11 +288,12 @@ export default function PartDetail() {
|
||||
name: 'required',
|
||||
label: t`Required for Orders`,
|
||||
hidden: part.required <= 0,
|
||||
icon: 'tick_off'
|
||||
icon: 'stocktake'
|
||||
},
|
||||
{
|
||||
type: 'progressbar',
|
||||
name: 'allocated_to_build_orders',
|
||||
icon: 'tick_off',
|
||||
total: part.required_for_build_orders,
|
||||
progress: part.allocated_to_build_orders,
|
||||
label: t`Allocated to Build Orders`,
|
||||
@ -303,6 +304,7 @@ export default function PartDetail() {
|
||||
},
|
||||
{
|
||||
type: 'progressbar',
|
||||
icon: 'tick_off',
|
||||
name: 'allocated_to_sales_orders',
|
||||
total: part.required_for_sales_orders,
|
||||
progress: part.allocated_to_sales_orders,
|
||||
@ -320,11 +322,12 @@ export default function PartDetail() {
|
||||
hidden: true // TODO: Expose "can_build" to the API
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
type: 'progressbar',
|
||||
name: 'building',
|
||||
unit: true,
|
||||
label: t`In Production`,
|
||||
hidden: !part.assembly || !part.building
|
||||
progress: part.building,
|
||||
total: part.scheduled_to_build,
|
||||
hidden: !part.assembly || (!part.building && !part.scheduled_to_build)
|
||||
}
|
||||
];
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
|
||||
import { ModelType } from '@lib/enums/ModelType';
|
||||
import { UserRoles } from '@lib/enums/Roles';
|
||||
import { apiUrl } from '@lib/functions/Api';
|
||||
import { getDetailUrl } from '@lib/functions/Navigation';
|
||||
import { apiUrl } from '@lib/index';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { Group, Skeleton, Stack, Text } from '@mantine/core';
|
||||
import { IconInfoCircle, IconPackages, IconSitemap } from '@tabler/icons-react';
|
||||
|
@ -34,6 +34,7 @@ import type { TableColumn } from './Column';
|
||||
import InvenTreeTableHeader from './InvenTreeTableHeader';
|
||||
import { type RowAction, RowActions } from './RowActions';
|
||||
|
||||
const ACTIONS_COLUMN_ACCESSOR: string = '--actions--';
|
||||
const defaultPageSize: number = 25;
|
||||
const PAGE_SIZES = [10, 15, 20, 25, 50, 100, 500];
|
||||
|
||||
@ -313,7 +314,7 @@ export function InvenTreeTable<T extends Record<string, any>>({
|
||||
// If row actions are available, add a column for them
|
||||
if (tableProps.rowActions) {
|
||||
cols.push({
|
||||
accessor: '--actions--',
|
||||
accessor: ACTIONS_COLUMN_ACCESSOR,
|
||||
title: ' ',
|
||||
hidden: false,
|
||||
resizable: false,
|
||||
@ -359,6 +360,23 @@ export function InvenTreeTable<T extends Record<string, any>>({
|
||||
columns: dataColumns
|
||||
});
|
||||
|
||||
// Ensure that the "actions" column is always at the end of the list
|
||||
// This effect is necessary as sometimes the underlying mantine-datatable columns change
|
||||
useEffect(() => {
|
||||
const idx: number = tableColumns.columnsOrder.indexOf(
|
||||
ACTIONS_COLUMN_ACCESSOR
|
||||
);
|
||||
|
||||
if (idx >= 0 && idx < tableColumns.columnsOrder.length - 1) {
|
||||
// Actions column is not at the end of the list - move it there
|
||||
const newOrder = tableColumns.columnsOrder.filter(
|
||||
(col) => col != ACTIONS_COLUMN_ACCESSOR
|
||||
);
|
||||
newOrder.push(ACTIONS_COLUMN_ACCESSOR);
|
||||
tableColumns.setColumnsOrder(newOrder);
|
||||
}
|
||||
}, [tableColumns.columnsOrder]);
|
||||
|
||||
// Reset the pagination state when the search term changes
|
||||
useEffect(() => {
|
||||
tableState.setPage(1);
|
||||
|
@ -12,8 +12,10 @@ import { RenderUser } from '../../components/render/User';
|
||||
import { useBuildOrderFields } from '../../forms/BuildForms';
|
||||
import { useCreateApiFormModal } from '../../hooks/UseForm';
|
||||
import { useTable } from '../../hooks/UseTable';
|
||||
import { useGlobalSettingsState } from '../../states/SettingsState';
|
||||
import { useUserState } from '../../states/UserState';
|
||||
import {
|
||||
BooleanColumn,
|
||||
CreationDateColumn,
|
||||
DateColumn,
|
||||
PartColumn,
|
||||
@ -59,6 +61,7 @@ export function BuildOrderTable({
|
||||
parentBuildId?: number;
|
||||
salesOrderId?: number;
|
||||
}>) {
|
||||
const globalSettings = useGlobalSettingsState();
|
||||
const table = useTable(!!partId ? 'buildorder-part' : 'buildorder-index');
|
||||
|
||||
const tableColumns = useMemo(() => {
|
||||
@ -109,6 +112,13 @@ export function BuildOrderTable({
|
||||
accessor: 'priority',
|
||||
sortable: true
|
||||
},
|
||||
BooleanColumn({
|
||||
accessor: 'external',
|
||||
title: t`External`,
|
||||
sortable: true,
|
||||
switchable: true,
|
||||
hidden: !globalSettings.isSet('BUILDORDER_EXTERNAL_BUILDS')
|
||||
}),
|
||||
CreationDateColumn({}),
|
||||
StartDateColumn({}),
|
||||
TargetDateColumn({}),
|
||||
@ -126,7 +136,7 @@ export function BuildOrderTable({
|
||||
},
|
||||
ResponsibleColumn({})
|
||||
];
|
||||
}, [parentBuildId]);
|
||||
}, [parentBuildId, globalSettings]);
|
||||
|
||||
const tableFilters: TableFilter[] = useMemo(() => {
|
||||
const filters: TableFilter[] = [
|
||||
@ -160,6 +170,12 @@ export function BuildOrderTable({
|
||||
HasProjectCodeFilter(),
|
||||
IssuedByFilter(),
|
||||
ResponsibleFilter(),
|
||||
{
|
||||
name: 'external',
|
||||
label: t`External`,
|
||||
description: t`Show external build orders`,
|
||||
active: globalSettings.isSet('BUILDORDER_EXTERNAL_BUILDS')
|
||||
},
|
||||
PartCategoryFilter()
|
||||
];
|
||||
|
||||
@ -174,7 +190,7 @@ export function BuildOrderTable({
|
||||
}
|
||||
|
||||
return filters;
|
||||
}, [partId]);
|
||||
}, [partId, globalSettings]);
|
||||
|
||||
const user = useUserState();
|
||||
|
||||
|
@ -2,17 +2,11 @@ import { t } from '@lingui/core/macro';
|
||||
import { ActionIcon, Badge, Group, Text, Tooltip } from '@mantine/core';
|
||||
import { IconCirclePlus } from '@tabler/icons-react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import {
|
||||
type ReactNode,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState
|
||||
} from 'react';
|
||||
import { type ReactNode, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
|
||||
import { ModelType } from '@lib/enums/ModelType';
|
||||
import { apiUrl } from '@lib/functions/Api';
|
||||
import { ModelType } from '@lib/index';
|
||||
import type { TableFilter } from '@lib/types/Filters';
|
||||
import type { ApiFormFieldSet } from '@lib/types/Forms';
|
||||
import { PassFailButton } from '../../components/buttons/YesNoButton';
|
||||
@ -26,7 +20,6 @@ import { useUserState } from '../../states/UserState';
|
||||
import type { TableColumn } from '../Column';
|
||||
import { LocationColumn } from '../ColumnRenderers';
|
||||
import { InvenTreeTable } from '../InvenTreeTable';
|
||||
import type { RowAction } from '../RowActions';
|
||||
import { TableHoverCard } from '../TableHoverCard';
|
||||
|
||||
/**
|
||||
@ -235,13 +228,6 @@ export default function BuildOrderTestTable({
|
||||
return [];
|
||||
}, []);
|
||||
|
||||
const rowActions = useCallback(
|
||||
(record: any): RowAction[] => {
|
||||
return [];
|
||||
},
|
||||
[user]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{createTestResult.modal}
|
||||
@ -256,7 +242,6 @@ export default function BuildOrderTestTable({
|
||||
tests: true,
|
||||
build: buildId
|
||||
},
|
||||
rowActions: rowActions,
|
||||
tableFilters: tableFilters,
|
||||
tableActions: tableActions,
|
||||
modelType: ModelType.stockitem
|
||||
|
@ -6,10 +6,12 @@ import {
|
||||
Group,
|
||||
Paper,
|
||||
Space,
|
||||
Stack,
|
||||
Text
|
||||
} from '@mantine/core';
|
||||
import { useDisclosure } from '@mantine/hooks';
|
||||
import {
|
||||
IconBuildingFactory2,
|
||||
IconCircleCheck,
|
||||
IconCircleX,
|
||||
IconExclamationCircle
|
||||
@ -22,6 +24,7 @@ import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
|
||||
import { ModelType } from '@lib/enums/ModelType';
|
||||
import { UserRoles } from '@lib/enums/Roles';
|
||||
import { apiUrl } from '@lib/functions/Api';
|
||||
import type { TableFilter } from '@lib/types/Filters';
|
||||
import { ActionButton } from '../../components/buttons/ActionButton';
|
||||
import { AddItemButton } from '../../components/buttons/AddItemButton';
|
||||
import { ProgressBar } from '../../components/items/ProgressBar';
|
||||
@ -43,6 +46,7 @@ import { useTable } from '../../hooks/UseTable';
|
||||
import { useUserState } from '../../states/UserState';
|
||||
import type { TableColumn } from '../Column';
|
||||
import { LocationColumn, PartColumn, StatusColumn } from '../ColumnRenderers';
|
||||
import { StatusFilterOptions } from '../Filter';
|
||||
import { InvenTreeTable } from '../InvenTreeTable';
|
||||
import { type RowAction, RowEditAction, RowViewAction } from '../RowActions';
|
||||
import { TableHoverCard } from '../TableHoverCard';
|
||||
@ -335,6 +339,17 @@ export default function BuildOutputTable({
|
||||
}
|
||||
});
|
||||
|
||||
const tableFilters: TableFilter[] = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
name: 'status',
|
||||
label: t`Status`,
|
||||
description: t`Filter by stock status`,
|
||||
choiceFunction: StatusFilterOptions(ModelType.stockitem)
|
||||
}
|
||||
];
|
||||
}, []);
|
||||
|
||||
const tableActions = useMemo(() => {
|
||||
return [
|
||||
<ActionButton
|
||||
@ -373,11 +388,11 @@ export default function BuildOutputTable({
|
||||
<AddItemButton
|
||||
key='add-build-output'
|
||||
tooltip={t`Add Build Output`}
|
||||
hidden={!user.hasAddRole(UserRoles.build)}
|
||||
hidden={build.external || !user.hasAddRole(UserRoles.build)}
|
||||
onClick={addBuildOutput.open}
|
||||
/>
|
||||
];
|
||||
}, [user, table.selectedRecords, table.hasSelectedRecords]);
|
||||
}, [build, user, table.selectedRecords, table.hasSelectedRecords]);
|
||||
|
||||
const rowActions = useCallback(
|
||||
(record: any): RowAction[] => {
|
||||
@ -568,32 +583,44 @@ export default function BuildOutputTable({
|
||||
opened={drawerOpen}
|
||||
close={closeDrawer}
|
||||
/>
|
||||
<InvenTreeTable
|
||||
tableState={table}
|
||||
url={apiUrl(ApiEndpoints.stock_item_list)}
|
||||
columns={tableColumns}
|
||||
props={{
|
||||
params: {
|
||||
part_detail: true,
|
||||
location_detail: true,
|
||||
tests: true,
|
||||
is_building: true,
|
||||
build: buildId
|
||||
},
|
||||
enableLabels: true,
|
||||
enableReports: true,
|
||||
dataFormatter: formatRecords,
|
||||
tableActions: tableActions,
|
||||
rowActions: rowActions,
|
||||
enableSelection: true,
|
||||
onRowClick: (record: any) => {
|
||||
if (hasTrackedItems && !!record.serial) {
|
||||
setSelectedOutputs([record]);
|
||||
openDrawer();
|
||||
<Stack gap='xs'>
|
||||
{build.external && (
|
||||
<Alert
|
||||
color='blue'
|
||||
icon={<IconBuildingFactory2 />}
|
||||
title={t`External Build`}
|
||||
>
|
||||
{t`This build order is fulfilled by an external purchase order`}
|
||||
</Alert>
|
||||
)}
|
||||
<InvenTreeTable
|
||||
tableState={table}
|
||||
url={apiUrl(ApiEndpoints.stock_item_list)}
|
||||
columns={tableColumns}
|
||||
props={{
|
||||
params: {
|
||||
part_detail: true,
|
||||
location_detail: true,
|
||||
tests: true,
|
||||
is_building: true,
|
||||
build: buildId
|
||||
},
|
||||
enableLabels: true,
|
||||
enableReports: true,
|
||||
dataFormatter: formatRecords,
|
||||
tableFilters: tableFilters,
|
||||
tableActions: tableActions,
|
||||
rowActions: rowActions,
|
||||
enableSelection: true,
|
||||
onRowClick: (record: any) => {
|
||||
if (hasTrackedItems && !!record.serial) {
|
||||
setSelectedOutputs([record]);
|
||||
openDrawer();
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -8,10 +8,12 @@ import { ModelType } from '@lib/enums/ModelType';
|
||||
import { UserRoles } from '@lib/enums/Roles';
|
||||
import { apiUrl } from '@lib/functions/Api';
|
||||
import type { TableFilter } from '@lib/types/Filters';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { ActionButton } from '../../components/buttons/ActionButton';
|
||||
import { AddItemButton } from '../../components/buttons/AddItemButton';
|
||||
import ImporterDrawer from '../../components/importer/ImporterDrawer';
|
||||
import { ProgressBar } from '../../components/items/ProgressBar';
|
||||
import { RenderInstance } from '../../components/render/Instance';
|
||||
import { RenderStockLocation } from '../../components/render/Stock';
|
||||
import { dataImporterSessionFields } from '../../forms/ImporterForms';
|
||||
import {
|
||||
@ -40,7 +42,8 @@ import {
|
||||
type RowAction,
|
||||
RowDeleteAction,
|
||||
RowDuplicateAction,
|
||||
RowEditAction
|
||||
RowEditAction,
|
||||
RowViewAction
|
||||
} from '../RowActions';
|
||||
import { TableHoverCard } from '../TableHoverCard';
|
||||
|
||||
@ -62,6 +65,7 @@ export function PurchaseOrderLineItemTable({
|
||||
}>) {
|
||||
const table = useTable('purchase-order-line-item');
|
||||
|
||||
const navigate = useNavigate();
|
||||
const user = useUserState();
|
||||
|
||||
// Data import
|
||||
@ -142,6 +146,23 @@ export function PurchaseOrderLineItemTable({
|
||||
sortable: false
|
||||
},
|
||||
ReferenceColumn({}),
|
||||
{
|
||||
accessor: 'build_order',
|
||||
title: t`Build Order`,
|
||||
sortable: true,
|
||||
render: (record: any) => {
|
||||
if (record.build_order_detail) {
|
||||
return (
|
||||
<RenderInstance
|
||||
instance={record.build_order_detail}
|
||||
model={ModelType.build}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return '-';
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
accessor: 'quantity',
|
||||
title: t`Quantity`,
|
||||
@ -276,7 +297,7 @@ export function PurchaseOrderLineItemTable({
|
||||
|
||||
const [selectedLine, setSelectedLine] = useState<number>(0);
|
||||
|
||||
const editPurchaseOrderFields = usePurchaseOrderLineItemFields({
|
||||
const editLineItemFields = usePurchaseOrderLineItemFields({
|
||||
create: false,
|
||||
orderId: orderId,
|
||||
supplierId: supplierId
|
||||
@ -286,7 +307,7 @@ export function PurchaseOrderLineItemTable({
|
||||
url: ApiEndpoints.purchase_order_line_list,
|
||||
pk: selectedLine,
|
||||
title: t`Edit Line Item`,
|
||||
fields: editPurchaseOrderFields,
|
||||
fields: editLineItemFields,
|
||||
table: table
|
||||
});
|
||||
|
||||
@ -326,6 +347,13 @@ export function PurchaseOrderLineItemTable({
|
||||
receiveLineItems.open();
|
||||
}
|
||||
},
|
||||
RowViewAction({
|
||||
hidden: !record.build_order,
|
||||
title: t`View Build Order`,
|
||||
modelType: ModelType.build,
|
||||
modelId: record.build_order,
|
||||
navigate: navigate
|
||||
}),
|
||||
RowEditAction({
|
||||
hidden: !user.hasChangeRole(UserRoles.purchase_order),
|
||||
onClick: () => {
|
||||
|
@ -53,10 +53,12 @@ import { InvenTreeTable } from '../InvenTreeTable';
|
||||
*/
|
||||
export function PurchaseOrderTable({
|
||||
supplierId,
|
||||
supplierPartId
|
||||
supplierPartId,
|
||||
externalBuildId
|
||||
}: Readonly<{
|
||||
supplierId?: number;
|
||||
supplierPartId?: number;
|
||||
externalBuildId?: number;
|
||||
}>) {
|
||||
const table = useTable('purchase-order');
|
||||
const user = useUserState();
|
||||
@ -178,7 +180,8 @@ export function PurchaseOrderTable({
|
||||
params: {
|
||||
supplier_detail: true,
|
||||
supplier: supplierId,
|
||||
supplier_part: supplierPartId
|
||||
supplier_part: supplierPartId,
|
||||
external_build: externalBuildId
|
||||
},
|
||||
tableFilters: tableFilters,
|
||||
tableActions: tableActions,
|
||||
|
@ -2,8 +2,8 @@ import { t } from '@lingui/core/macro';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
|
||||
import { UserRoles } from '@lib/enums/Roles';
|
||||
import { apiUrl } from '@lib/functions/Api';
|
||||
import { UserRoles } from '@lib/index';
|
||||
import { notifications, showNotification } from '@mantine/notifications';
|
||||
import { IconTrashXFilled, IconX } from '@tabler/icons-react';
|
||||
import { api } from '../../App';
|
||||
|
@ -3,6 +3,7 @@ import { test } from '../baseFixtures.ts';
|
||||
import {
|
||||
activateCalendarView,
|
||||
clearTableFilters,
|
||||
clickOnRowMenu,
|
||||
getRowFromCell,
|
||||
loadTab,
|
||||
navigate,
|
||||
@ -65,7 +66,7 @@ test('Build Order - Basic Tests', async ({ browser }) => {
|
||||
await loadTab(page, 'Attachments');
|
||||
await loadTab(page, 'Notes');
|
||||
await loadTab(page, 'Incomplete Outputs');
|
||||
await loadTab(page, 'Line Items');
|
||||
await loadTab(page, 'Required Stock');
|
||||
await loadTab(page, 'Allocated Stock');
|
||||
|
||||
// Check for expected text in the table
|
||||
@ -373,3 +374,53 @@ test('Build Order - Duplicate', async ({ browser }) => {
|
||||
|
||||
await page.getByText('Pending').first().waitFor();
|
||||
});
|
||||
|
||||
// Tests for external build orders
|
||||
test('Build Order - External', async ({ browser }) => {
|
||||
const page = await doCachedLogin(browser, { url: 'manufacturing/index/' });
|
||||
await loadTab(page, 'Build Orders');
|
||||
|
||||
// Filter to show only external builds
|
||||
await clearTableFilters(page);
|
||||
await setTableChoiceFilter(page, 'External', 'Yes');
|
||||
await page.getByRole('cell', { name: 'BO0026' }).waitFor();
|
||||
await page.getByRole('cell', { name: 'BO0025' }).click();
|
||||
await page
|
||||
.locator('span')
|
||||
.filter({ hasText: /^External$/ })
|
||||
.waitFor();
|
||||
|
||||
await loadTab(page, 'Allocated Stock');
|
||||
await loadTab(page, 'Incomplete Outputs');
|
||||
await page
|
||||
.getByText('This build order is fulfilled by an external purchase order')
|
||||
.waitFor();
|
||||
|
||||
await loadTab(page, 'External Orders');
|
||||
await page.getByRole('cell', { name: 'PO0016' }).click();
|
||||
|
||||
await loadTab(page, 'Attachments');
|
||||
await loadTab(page, 'Received Stock');
|
||||
await loadTab(page, 'Line Items');
|
||||
|
||||
const cell = await page.getByRole('cell', {
|
||||
name: '002.01-PCBA',
|
||||
exact: true
|
||||
});
|
||||
await clickOnRowMenu(cell);
|
||||
|
||||
await page.getByRole('menuitem', { name: 'Receive line item' }).waitFor();
|
||||
await page.getByRole('menuitem', { name: 'Duplicate' }).waitFor();
|
||||
await page.getByRole('menuitem', { name: 'Edit' }).waitFor();
|
||||
await page.getByRole('menuitem', { name: 'View Build Order' }).click();
|
||||
|
||||
// Wait for navigation back to build order detail page
|
||||
await page.getByText('Build Order: BO0025', { exact: true }).waitFor();
|
||||
|
||||
// Let's look at BO0026 too
|
||||
await navigate(page, 'manufacturing/build-order/26/details');
|
||||
await loadTab(page, 'External Orders');
|
||||
|
||||
await page.getByRole('cell', { name: 'PO0017' }).waitFor();
|
||||
await page.getByRole('cell', { name: 'PO0018' }).waitFor();
|
||||
});
|
||||
|
Reference in New Issue
Block a user