mirror of
https://github.com/inventree/InvenTree.git
synced 2026-09-02 10:18:42 +00:00
Merge commit '749c4715ee022bdb39b500f5c3776c1ee18411e3' into block-notes
This commit is contained in:
@@ -385,7 +385,7 @@ export function useCompleteBuildOutputsForm({
|
||||
title: t`Complete Build Outputs`,
|
||||
fields: buildOutputCompleteFields,
|
||||
onFormSuccess: onFormSuccess,
|
||||
successMessage: t`Build outputs have been completed`,
|
||||
successMessage: null,
|
||||
size: '80%'
|
||||
});
|
||||
}
|
||||
@@ -466,7 +466,7 @@ export function useScrapBuildOutputsForm({
|
||||
),
|
||||
fields: buildOutputScrapFields,
|
||||
onFormSuccess: onFormSuccess,
|
||||
successMessage: t`Build outputs have been scrapped`,
|
||||
successMessage: null,
|
||||
size: '80%'
|
||||
});
|
||||
}
|
||||
@@ -527,7 +527,7 @@ export function useCancelBuildOutputsForm({
|
||||
),
|
||||
fields: buildOutputCancelFields,
|
||||
onFormSuccess: onFormSuccess,
|
||||
successMessage: t`Build outputs have been cancelled`,
|
||||
successMessage: null,
|
||||
size: '80%'
|
||||
});
|
||||
}
|
||||
|
||||
@@ -188,6 +188,11 @@ export function usePartFields({
|
||||
delete fields['default_expiry'];
|
||||
}
|
||||
|
||||
// Remove "locked" field if locking not enabled
|
||||
if (!globalSettings.isSet('PART_ENABLE_LOCKING')) {
|
||||
delete fields['locked'];
|
||||
}
|
||||
|
||||
if (create) {
|
||||
delete fields['starred'];
|
||||
}
|
||||
|
||||
@@ -212,6 +212,7 @@ export default function SystemSettings() {
|
||||
'PART_ALLOW_DUPLICATE_IPN',
|
||||
'PART_ALLOW_EDIT_IPN',
|
||||
'PART_ALLOW_DELETE_FROM_ASSEMBLY',
|
||||
'PART_ENABLE_LOCKING',
|
||||
'PART_ENABLE_REVISION',
|
||||
'PART_REVISION_ASSEMBLY_ONLY',
|
||||
'PART_SHOW_RELATED',
|
||||
|
||||
@@ -202,6 +202,11 @@ export default function PartDetail() {
|
||||
refetchOnMount: true
|
||||
});
|
||||
|
||||
const lockingEnabled = useMemo(
|
||||
() => globalSettings.isSet('PART_ENABLE_LOCKING'),
|
||||
[globalSettings]
|
||||
);
|
||||
|
||||
const revisionsEnabled = useMemo(
|
||||
() => globalSettings.isSet('PART_ENABLE_REVISION'),
|
||||
[globalSettings]
|
||||
@@ -808,7 +813,12 @@ export default function PartDetail() {
|
||||
icon: <IconTestPipe />,
|
||||
hidden: !part.testable,
|
||||
content: part?.pk ? (
|
||||
<PartTestTemplateTable partId={part?.pk} partLocked={part.locked} />
|
||||
<PartTestTemplateTable
|
||||
partId={part?.pk}
|
||||
partLocked={
|
||||
globalSettings.isSet('PART_ENABLE_LOCKING') && part?.locked
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<Skeleton />
|
||||
)
|
||||
@@ -836,7 +846,7 @@ export default function PartDetail() {
|
||||
icon: <IconListDetails />,
|
||||
content: (
|
||||
<>
|
||||
{part.locked && (
|
||||
{lockingEnabled && part.locked && (
|
||||
<Alert
|
||||
title={t`Part is Locked`}
|
||||
color='orange'
|
||||
@@ -849,7 +859,7 @@ export default function PartDetail() {
|
||||
<ParameterTable
|
||||
modelType={ModelType.part}
|
||||
modelId={part?.pk}
|
||||
allowEdit={part?.locked != true}
|
||||
allowEdit={!lockingEnabled || part?.locked != true}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
@@ -1149,20 +1159,22 @@ export default function PartDetail() {
|
||||
<PageDetail
|
||||
title={`${t`Part`}: ${part.full_name}`}
|
||||
icon={
|
||||
<ActionIcon
|
||||
aria-label='part-lock-icon'
|
||||
variant='transparent'
|
||||
disabled={!user.hasChangeRole(UserRoles.part)}
|
||||
onClick={() => {
|
||||
api
|
||||
.patch(apiUrl(ApiEndpoints.part_list, part.pk), {
|
||||
locked: !part.locked
|
||||
})
|
||||
.then(refreshInstance);
|
||||
}}
|
||||
>
|
||||
{part?.locked ? <IconLock /> : <IconLockOpen />}
|
||||
</ActionIcon>
|
||||
lockingEnabled ? (
|
||||
<ActionIcon
|
||||
aria-label='part-lock-icon'
|
||||
variant='transparent'
|
||||
disabled={!user.hasChangeRole(UserRoles.part)}
|
||||
onClick={() => {
|
||||
api
|
||||
.patch(apiUrl(ApiEndpoints.part_list, part.pk), {
|
||||
locked: !part.locked
|
||||
})
|
||||
.then(refreshInstance);
|
||||
}}
|
||||
>
|
||||
{part?.locked ? <IconLock /> : <IconLockOpen />}
|
||||
</ActionIcon>
|
||||
) : undefined
|
||||
}
|
||||
subtitle={part.description}
|
||||
imageUrl={part.image}
|
||||
|
||||
@@ -52,6 +52,8 @@ export function RenderPartColumn({
|
||||
part: any;
|
||||
full_name?: boolean;
|
||||
}) {
|
||||
const globalSettings = useGlobalSettingsState.getState();
|
||||
|
||||
if (!part) {
|
||||
return <Skeleton />;
|
||||
}
|
||||
@@ -69,7 +71,7 @@ export function RenderPartColumn({
|
||||
<IconExclamationCircle color='red' size={16} />
|
||||
</Tooltip>
|
||||
)}
|
||||
{part?.locked && (
|
||||
{globalSettings.isSet('PART_ENABLE_LOCKING') && part?.locked && (
|
||||
<Tooltip label={t`Part is Locked`}>
|
||||
<IconLock size={16} />
|
||||
</Tooltip>
|
||||
|
||||
@@ -40,7 +40,10 @@ import {
|
||||
useEditApiFormModal
|
||||
} from '../../hooks/UseForm';
|
||||
import { useImporterState } from '../../states/ImporterState';
|
||||
import { useUserSettingsState } from '../../states/SettingsStates';
|
||||
import {
|
||||
useGlobalSettingsState,
|
||||
useUserSettingsState
|
||||
} from '../../states/SettingsStates';
|
||||
import { useUserState } from '../../states/UserState';
|
||||
import {
|
||||
BooleanColumn,
|
||||
@@ -91,6 +94,13 @@ export function BomTable({
|
||||
|
||||
const [isEditing, setIsEditing] = useState<boolean>(false);
|
||||
|
||||
const globalSettings = useGlobalSettingsState();
|
||||
|
||||
const isLocked = useMemo(
|
||||
() => globalSettings.isSet('PART_ENABLE_LOCKING') && (partLocked ?? false),
|
||||
[globalSettings, partLocked]
|
||||
);
|
||||
|
||||
const userSettings = useUserSettingsState();
|
||||
|
||||
const tableColumns: TableColumn[] = useMemo(() => {
|
||||
@@ -605,16 +615,14 @@ export function BomTable({
|
||||
title: t`Validate BOM Line`,
|
||||
color: 'green',
|
||||
hidden:
|
||||
partLocked ||
|
||||
record.validated ||
|
||||
!user.hasChangeRole(UserRoles.bom),
|
||||
isLocked || record.validated || !user.hasChangeRole(UserRoles.bom),
|
||||
icon: <IconCircleCheck />,
|
||||
onClick: () => {
|
||||
validateBomItem(record);
|
||||
}
|
||||
},
|
||||
RowEditAction({
|
||||
hidden: partLocked || !user.hasChangeRole(UserRoles.bom),
|
||||
hidden: isLocked || !user.hasChangeRole(UserRoles.bom),
|
||||
onClick: () => {
|
||||
setSelectedBomItem(record);
|
||||
editBomItem.open();
|
||||
@@ -623,7 +631,7 @@ export function BomTable({
|
||||
{
|
||||
title: t`Edit Substitutes`,
|
||||
color: 'blue',
|
||||
hidden: partLocked || !user.hasAddRole(UserRoles.bom),
|
||||
hidden: isLocked || !user.hasAddRole(UserRoles.bom),
|
||||
icon: <IconSwitch3 />,
|
||||
onClick: () => {
|
||||
setSelectedBomItem(record);
|
||||
@@ -631,7 +639,7 @@ export function BomTable({
|
||||
}
|
||||
},
|
||||
RowDeleteAction({
|
||||
hidden: partLocked || !user.hasDeleteRole(UserRoles.bom),
|
||||
hidden: isLocked || !user.hasDeleteRole(UserRoles.bom),
|
||||
onClick: () => {
|
||||
setSelectedBomItem(record);
|
||||
deleteBomItem.open();
|
||||
@@ -639,7 +647,7 @@ export function BomTable({
|
||||
})
|
||||
];
|
||||
},
|
||||
[isEditing, partId, partLocked, user]
|
||||
[isEditing, partId, isLocked, user]
|
||||
);
|
||||
|
||||
const tableActions = useMemo(() => {
|
||||
@@ -649,7 +657,7 @@ export function BomTable({
|
||||
tooltip={t`Add BOM Items`}
|
||||
position='bottom-start'
|
||||
icon={<IconPlus />}
|
||||
hidden={!isEditing || partLocked || !user.hasAddRole(UserRoles.bom)}
|
||||
hidden={!isEditing || isLocked || !user.hasAddRole(UserRoles.bom)}
|
||||
actions={[
|
||||
{
|
||||
name: t`Add BOM Item`,
|
||||
@@ -667,7 +675,7 @@ export function BomTable({
|
||||
/>,
|
||||
<ActionButton
|
||||
key='edit-bom'
|
||||
hidden={partLocked || !user.hasChangeRole(UserRoles.bom) || isEditing}
|
||||
hidden={isLocked || !user.hasChangeRole(UserRoles.bom) || isEditing}
|
||||
tooltip={t`Edit BOM`}
|
||||
icon={<IconEdit />}
|
||||
onClick={() => {
|
||||
@@ -686,7 +694,7 @@ export function BomTable({
|
||||
}}
|
||||
/>
|
||||
];
|
||||
}, [isEditing, partLocked, user]);
|
||||
}, [isEditing, isLocked, user]);
|
||||
|
||||
// Row expansion (for displaying subassemblies)
|
||||
const rowExpansion = subassemblyRowExpansion({ table: table });
|
||||
@@ -699,7 +707,7 @@ export function BomTable({
|
||||
{deleteBomItem.modal}
|
||||
{editSubstitutes.modal}
|
||||
<Stack gap='xs'>
|
||||
{partLocked && (
|
||||
{isLocked && (
|
||||
<Alert
|
||||
title={t`Part is Locked`}
|
||||
color='orange'
|
||||
@@ -730,9 +738,9 @@ export function BomTable({
|
||||
modelField: 'sub_part',
|
||||
onCellClick: () => {},
|
||||
rowActions: isEditing ? rowActions : undefined,
|
||||
enableSelection: isEditing && !partLocked,
|
||||
enableSelection: isEditing && !isLocked,
|
||||
enableBulkDelete:
|
||||
isEditing && !partLocked && user.hasDeleteRole(UserRoles.bom),
|
||||
isEditing && !isLocked && user.hasDeleteRole(UserRoles.bom),
|
||||
enableDownload: true,
|
||||
rowExpansion: isEditing ? undefined : rowExpansion
|
||||
}}
|
||||
|
||||
@@ -346,31 +346,80 @@ export default function BuildOutputTable({
|
||||
|
||||
const [selectedOutputs, setSelectedOutputs] = useState<any[]>([]);
|
||||
|
||||
const [completeTaskId, setCompleteTaskId] = useState<string>('');
|
||||
const [scrapTaskId, setScrapTaskId] = useState<string>('');
|
||||
const [deleteTaskId, setDeleteTaskId] = useState<string>('');
|
||||
|
||||
useBackgroundTask({
|
||||
taskId: completeTaskId,
|
||||
message: t`Completing build outputs`,
|
||||
successMessage: t`Build outputs have been completed`,
|
||||
onSuccess: () => {
|
||||
table.refreshTable(true);
|
||||
refreshBuild();
|
||||
}
|
||||
});
|
||||
|
||||
useBackgroundTask({
|
||||
taskId: scrapTaskId,
|
||||
message: t`Scrapping build outputs`,
|
||||
successMessage: t`Build outputs have been scrapped`,
|
||||
onSuccess: () => {
|
||||
table.refreshTable(true);
|
||||
refreshBuild();
|
||||
}
|
||||
});
|
||||
|
||||
useBackgroundTask({
|
||||
taskId: deleteTaskId,
|
||||
message: t`Cancelling build outputs`,
|
||||
successMessage: t`Build outputs have been cancelled`,
|
||||
onSuccess: () => {
|
||||
table.refreshTable(true);
|
||||
refreshBuild();
|
||||
}
|
||||
});
|
||||
|
||||
const completeBuildOutputsForm = useCompleteBuildOutputsForm({
|
||||
build: build,
|
||||
outputs: selectedOutputs,
|
||||
hasTrackedItems: hasTrackedItems,
|
||||
onFormSuccess: () => {
|
||||
table.refreshTable(true);
|
||||
refreshBuild();
|
||||
onFormSuccess: (response: any) => {
|
||||
if (response.task_id) {
|
||||
setCompleteTaskId(response.task_id);
|
||||
} else {
|
||||
// If no task ID is returned, immediately refresh the table and build data
|
||||
table.refreshTable(true);
|
||||
refreshBuild();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const scrapBuildOutputsForm = useScrapBuildOutputsForm({
|
||||
build: build,
|
||||
outputs: selectedOutputs,
|
||||
onFormSuccess: () => {
|
||||
table.refreshTable(true);
|
||||
refreshBuild();
|
||||
onFormSuccess: (response: any) => {
|
||||
if (response.task_id) {
|
||||
setScrapTaskId(response.task_id);
|
||||
} else {
|
||||
// If no task ID is returned, immediately refresh the table and build data
|
||||
table.refreshTable(true);
|
||||
refreshBuild();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const cancelBuildOutputsForm = useCancelBuildOutputsForm({
|
||||
build: build,
|
||||
outputs: selectedOutputs,
|
||||
onFormSuccess: () => {
|
||||
table.refreshTable(true);
|
||||
refreshBuild();
|
||||
onFormSuccess: (response: any) => {
|
||||
if (response.task_id) {
|
||||
setDeleteTaskId(response.task_id);
|
||||
} else {
|
||||
// If no task ID is returned, immediately refresh the table and build data
|
||||
table.refreshTable(true);
|
||||
refreshBuild();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -207,8 +207,7 @@ export function PurchaseOrderLineItemTable({
|
||||
{
|
||||
accessor: 'received',
|
||||
title: t`Received`,
|
||||
sortable: false,
|
||||
|
||||
sortable: true,
|
||||
render: (record: any) => (
|
||||
<ProgressBar
|
||||
progressLabel={true}
|
||||
|
||||
@@ -38,6 +38,7 @@ test('Build Order - Basic Tests', async ({ browser }) => {
|
||||
await clearTableFilters(page);
|
||||
|
||||
// We have now loaded the "Build Order" table. Check for some expected texts
|
||||
await page.getByPlaceholder('Search').fill('7');
|
||||
await page.getByText('On Hold').first().waitFor();
|
||||
await page.getByText('Pending').first().waitFor();
|
||||
|
||||
@@ -60,6 +61,7 @@ test('Build Order - Basic Tests', async ({ browser }) => {
|
||||
await page.getByLabel('breadcrumb-0-manufacturing').click();
|
||||
|
||||
// Load a different build order
|
||||
await page.getByPlaceholder('Search').fill('11');
|
||||
await page.getByRole('cell', { name: 'BO0011' }).click();
|
||||
|
||||
// This build order should be "in production"
|
||||
@@ -654,6 +656,7 @@ test('Build Order - Filters', async ({ browser }) => {
|
||||
// Check for expected pagination text i.e. (1 - 24 / 24)
|
||||
// Note: Due to other concurrent tests, the number of build orders may vary
|
||||
await page.getByText(/1 - \d+ \/ \d+/).waitFor();
|
||||
await page.getByPlaceholder('Search').fill('23');
|
||||
await page.getByRole('cell', { name: 'BO0023' }).waitFor();
|
||||
|
||||
// Toggle 'Outstanding' filter
|
||||
@@ -665,7 +668,7 @@ test('Build Order - Filters', async ({ browser }) => {
|
||||
await page.getByRole('textbox', { name: 'table-search-input' }).fill('');
|
||||
await setTableChoiceFilter(page, 'Outstanding', 'No');
|
||||
|
||||
await page.getByText('1 - 6 / 6').waitFor();
|
||||
await page.getByText(/1 - \d+ \/ \d+/).waitFor();
|
||||
|
||||
await clearTableFilters(page);
|
||||
|
||||
@@ -699,6 +702,40 @@ test('Build Order - Duplicate', async ({ browser }) => {
|
||||
await page.getByRole('tab', { name: 'Build Details' }).click();
|
||||
|
||||
await page.getByText('Pending').first().waitFor();
|
||||
|
||||
// Create a build output
|
||||
await loadTab(page, 'Incomplete Outputs');
|
||||
await page
|
||||
.getByRole('button', { name: 'action-button-add-build-output' })
|
||||
.click();
|
||||
await page
|
||||
.getByRole('textbox', { name: 'text-field-batch_code' })
|
||||
.fill('BATCH-001');
|
||||
await page.getByRole('button', { name: 'Submit' }).click();
|
||||
|
||||
// Cancel (delete) the build output
|
||||
const cell = await page.getByRole('cell', { name: 'BATCH-001' }).first();
|
||||
await clickOnRowMenu(cell);
|
||||
await page.getByRole('menuitem', { name: 'Cancel' }).click();
|
||||
await page.getByRole('button', { name: 'Submit' }).click();
|
||||
|
||||
// no more build outputs
|
||||
await page.getByText('No records found').waitFor();
|
||||
|
||||
// Cancel the build
|
||||
await page.getByRole('button', { name: 'action-menu-build-order-' }).click();
|
||||
await page
|
||||
.getByRole('menuitem', { name: 'action-menu-build-order-actions-cancel' })
|
||||
.click();
|
||||
|
||||
await page
|
||||
.getByRole('switch', { name: 'boolean-field-remove_allocated_stock' })
|
||||
.click();
|
||||
await page
|
||||
.getByRole('switch', { name: 'boolean-field-remove_incomplete_outputs' })
|
||||
.click();
|
||||
await page.getByRole('button', { name: 'Submit' }).click();
|
||||
await page.getByText('Cancelled').first().waitFor();
|
||||
});
|
||||
|
||||
// Tests for external build orders
|
||||
|
||||
Reference in New Issue
Block a user