mirror of
https://github.com/inventree/InvenTree.git
synced 2025-06-18 13:05:42 +00:00
* Add basic model for handling generic attachments * Refactor migration * Data migration to convert old files across * Admin updates * Increase comment field max_length * Adjust field name * Remove legacy serializer classes / endpoints * Expose new model to API * Admin site list filters * Remove legacy attachment models - Add new mixin class to designate which models can have attachments * Update data migration - Ensure other apps are at the correct migration state beforehand * Add migrations to remove legacy attachment tables * Fix for "rename_attachment" callback * Refactor model_type field - ContentType does not allow easy API serialization * Set allowed options for admin * Update model verbose names * Fix logic for file upload * Add choices for serializer * Add API filtering * Fix for API filter * Fix for attachment tables in PUI - Still not solved permission issues * Bump API version * Record user when uploading attachment via API * Refactor <AttachmentTable /> for PUI * Display 'file_size' in PUI attachment table * Fix company migrations * Include permission informtion in roles API endpoint * Read user permissions in PUI * Simplify permission checks for <AttachmentTable /> * Automatically clean up old content types * Cleanup PUI * Fix typo in data migration * Add reverse data migration * Update unit tests * Use InMemoryStorage for media files in test mode * Data migration unit test * Fix "model_type" field - It is a required field after all * Add permission check for serializer * Fix permission check for CUI * Fix PUI import * Test python lib against specific branch - Will be reverted once code is merged * Revert STORAGES setting - Might be worth looking into again * Fix part unit test * Fix unit test for sales order * Use 'get_global_setting' * Use 'get_global_setting' * Update setting getter * Unit tests * Tweaks * Revert change to settings.py * More updates for get_global_setting * Relax API query count requirement * remove illegal chars and add unit tests * Fix unit tests * Fix frontend unit tests * settings management updates * Prevent db write under more conditions * Simplify settings code * Pop values before creating filters * Prevent settings write under certain conditions * Add debug msg * Clear db on record import * Refactor permissions checks - Allows extension / customization of permission checks at a later date * Unit test updates * Prevent delete of attachment without correct permissions * Adjust odcker.yaml * Cleanup data migrations * Tweak migration tests for build app * Update data migration - Handle case with missing data * Prevent debug shell in TESTING mode * Update migration dependencies - Ensure all apps are "up to date" before removing legacy tables * add file size test * Update migration tests * Revert some settings caching changes * Fix incorrect logic in migration * Update unit tests * Prevent create on CURRENCY_CODES - Seems to play havoc with bootup sequence * Fix unit test * Some refactoring - Use get_global_setting * Fix typo * Revert change * Add "tags" and "metadata" * Include "tags" field in API serializer * add "metadata" endpoint for attachments
454 lines
12 KiB
TypeScript
454 lines
12 KiB
TypeScript
import { t } from '@lingui/macro';
|
|
import { Badge, Group, Text, Tooltip } from '@mantine/core';
|
|
import { showNotification } from '@mantine/notifications';
|
|
import {
|
|
IconCircleCheck,
|
|
IconCirclePlus,
|
|
IconInfoCircle
|
|
} from '@tabler/icons-react';
|
|
import { useQuery } from '@tanstack/react-query';
|
|
import { DataTable } from 'mantine-datatable';
|
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
|
|
import { api } from '../../App';
|
|
import { AddItemButton } from '../../components/buttons/AddItemButton';
|
|
import { PassFailButton } from '../../components/buttons/YesNoButton';
|
|
import { ApiFormFieldSet } from '../../components/forms/fields/ApiFormField';
|
|
import { AttachmentLink } from '../../components/items/AttachmentLink';
|
|
import { RenderUser } from '../../components/render/User';
|
|
import { formatDate } from '../../defaults/formatters';
|
|
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
|
import { UserRoles } from '../../enums/Roles';
|
|
import { useTestResultFields } from '../../forms/StockForms';
|
|
import {
|
|
useCreateApiFormModal,
|
|
useDeleteApiFormModal,
|
|
useEditApiFormModal
|
|
} from '../../hooks/UseForm';
|
|
import { useTable } from '../../hooks/UseTable';
|
|
import { apiUrl } from '../../states/ApiState';
|
|
import { useUserState } from '../../states/UserState';
|
|
import { TableColumn } from '../Column';
|
|
import { DateColumn, DescriptionColumn, NoteColumn } from '../ColumnRenderers';
|
|
import { TableFilter } from '../Filter';
|
|
import { InvenTreeTable } from '../InvenTreeTable';
|
|
import { RowActions, RowDeleteAction, RowEditAction } from '../RowActions';
|
|
|
|
export default function StockItemTestResultTable({
|
|
partId,
|
|
itemId
|
|
}: {
|
|
partId: number;
|
|
itemId: number;
|
|
}) {
|
|
const user = useUserState();
|
|
const table = useTable('stocktests');
|
|
|
|
// Fetch the test templates required for this stock item
|
|
const { data: testTemplates } = useQuery({
|
|
queryKey: ['stocktesttemplates', partId, itemId],
|
|
queryFn: async () => {
|
|
if (!partId) {
|
|
return [];
|
|
}
|
|
|
|
return api
|
|
.get(apiUrl(ApiEndpoints.part_test_template_list), {
|
|
params: {
|
|
part: partId,
|
|
include_inherited: true,
|
|
enabled: true
|
|
}
|
|
})
|
|
.then((response) => response.data)
|
|
.catch((_error) => []);
|
|
}
|
|
});
|
|
|
|
useEffect(() => {
|
|
table.refreshTable();
|
|
}, [testTemplates]);
|
|
|
|
// Format the test results based on the returned data
|
|
const formatRecords = useCallback(
|
|
(records: any[]): any[] => {
|
|
// Construct a list of test templates
|
|
let results = testTemplates.map((template: any) => {
|
|
return {
|
|
...template,
|
|
templateId: template.pk,
|
|
results: []
|
|
};
|
|
});
|
|
|
|
// If any of the tests results point to templates which we do not have, add them in
|
|
records.forEach((record) => {
|
|
if (!results.find((r: any) => r.templateId == record.template)) {
|
|
results.push({
|
|
...record.template_detail,
|
|
templateId: record.template,
|
|
results: []
|
|
});
|
|
}
|
|
});
|
|
|
|
// Iterate through the returned records
|
|
// Note that the results are sorted by oldest first,
|
|
// to ensure that the most recent result is displayed "on top"
|
|
records
|
|
.sort((a: any, b: any) => {
|
|
return a.pk > b.pk ? 1 : -1;
|
|
})
|
|
.forEach((record) => {
|
|
// Find matching template
|
|
let idx = results.findIndex(
|
|
(r: any) => r.templateId == record.template
|
|
);
|
|
if (idx >= 0) {
|
|
results[idx] = {
|
|
...results[idx],
|
|
...record
|
|
};
|
|
|
|
results[idx].results.push(record);
|
|
}
|
|
});
|
|
|
|
return results;
|
|
},
|
|
[partId, itemId, testTemplates]
|
|
);
|
|
|
|
const tableColumns: TableColumn[] = useMemo(() => {
|
|
return [
|
|
{
|
|
accessor: 'test',
|
|
title: t`Test`,
|
|
switchable: false,
|
|
sortable: true,
|
|
render: (record: any) => {
|
|
const enabled = record.enabled ?? record.template_detail?.enabled;
|
|
const installed =
|
|
record.stock_item != undefined && record.stock_item != itemId;
|
|
|
|
return (
|
|
<Group justify="space-between" wrap="nowrap">
|
|
<Text
|
|
style={{ fontStyle: installed ? 'italic' : undefined }}
|
|
c={enabled ? undefined : 'red'}
|
|
>
|
|
{!record.templateId && '- '}
|
|
{record.test_name ?? record.template_detail?.test_name}
|
|
</Text>
|
|
<Group justify="right">
|
|
{record.results && record.results.length > 1 && (
|
|
<Tooltip label={t`Test Results`}>
|
|
<Badge color="lightblue" variant="filled">
|
|
{record.results.length}
|
|
</Badge>
|
|
</Tooltip>
|
|
)}
|
|
{installed && (
|
|
<Tooltip label={t`Test result for installed stock item`}>
|
|
<IconInfoCircle size={16} color="blue" />
|
|
</Tooltip>
|
|
)}
|
|
</Group>
|
|
</Group>
|
|
);
|
|
}
|
|
},
|
|
{
|
|
accessor: 'result',
|
|
title: t`Result`,
|
|
switchable: false,
|
|
sortable: true,
|
|
render: (record: any) => {
|
|
if (record.result === undefined) {
|
|
return (
|
|
<Badge color="lightblue" variant="filled">{t`No Result`}</Badge>
|
|
);
|
|
} else {
|
|
return <PassFailButton value={record.result} />;
|
|
}
|
|
}
|
|
},
|
|
DescriptionColumn({
|
|
accessor: 'description'
|
|
}),
|
|
{
|
|
accessor: 'value',
|
|
title: t`Value`
|
|
},
|
|
{
|
|
accessor: 'attachment',
|
|
title: t`Attachment`,
|
|
render: (record: any) =>
|
|
record.attachment && <AttachmentLink attachment={record.attachment} />
|
|
},
|
|
NoteColumn({}),
|
|
DateColumn({}),
|
|
{
|
|
accessor: 'user',
|
|
title: t`User`,
|
|
sortable: false,
|
|
render: (record: any) =>
|
|
record.user_detail && <RenderUser instance={record.user_detail} />
|
|
},
|
|
{
|
|
accessor: 'test_station',
|
|
sortable: true,
|
|
title: t`Test station`
|
|
},
|
|
{
|
|
accessor: 'started_datetime',
|
|
sortable: true,
|
|
title: t`Started`,
|
|
render: (record: any) => {
|
|
return (
|
|
<Group justify="space-between">
|
|
{formatDate(record.started_datetime, {
|
|
showTime: true,
|
|
showSeconds: true
|
|
})}
|
|
</Group>
|
|
);
|
|
}
|
|
},
|
|
{
|
|
accessor: 'finished_datetime',
|
|
sortable: true,
|
|
title: t`Finished`,
|
|
render: (record: any) => {
|
|
return (
|
|
<Group justify="space-between">
|
|
{formatDate(record.finished_datetime, {
|
|
showTime: true,
|
|
showSeconds: true
|
|
})}
|
|
</Group>
|
|
);
|
|
}
|
|
}
|
|
];
|
|
}, [itemId]);
|
|
|
|
const resultFields: ApiFormFieldSet = useTestResultFields({
|
|
partId: partId,
|
|
itemId: itemId
|
|
});
|
|
|
|
const [selectedTemplate, setSelectedTemplate] = useState<number | undefined>(
|
|
undefined
|
|
);
|
|
|
|
const newTestModal = useCreateApiFormModal({
|
|
url: ApiEndpoints.stock_test_result_list,
|
|
fields: useMemo(() => ({ ...resultFields }), [resultFields]),
|
|
initialData: {
|
|
template: selectedTemplate,
|
|
result: true
|
|
},
|
|
title: t`Add Test Result`,
|
|
table: table,
|
|
successMessage: t`Test result added`
|
|
});
|
|
|
|
const [selectedTest, setSelectedTest] = useState<number>(0);
|
|
|
|
const editTestModal = useEditApiFormModal({
|
|
url: ApiEndpoints.stock_test_result_list,
|
|
pk: selectedTest,
|
|
fields: useMemo(() => ({ ...resultFields }), [resultFields]),
|
|
title: t`Edit Test Result`,
|
|
table: table,
|
|
successMessage: t`Test result updated`
|
|
});
|
|
|
|
const deleteTestModal = useDeleteApiFormModal({
|
|
url: ApiEndpoints.stock_test_result_list,
|
|
pk: selectedTest,
|
|
title: t`Delete Test Result`,
|
|
table: table,
|
|
successMessage: t`Test result deleted`
|
|
});
|
|
|
|
const passTest = useCallback(
|
|
(templateId: number) => {
|
|
api
|
|
.post(apiUrl(ApiEndpoints.stock_test_result_list), {
|
|
template: templateId,
|
|
stock_item: itemId,
|
|
result: true
|
|
})
|
|
.then(() => {
|
|
table.refreshTable();
|
|
showNotification({
|
|
title: t`Test Passed`,
|
|
message: t`Test result has been recorded`,
|
|
color: 'green'
|
|
});
|
|
})
|
|
.catch(() => {
|
|
showNotification({
|
|
title: t`Error`,
|
|
message: t`Failed to record test result`,
|
|
color: 'red'
|
|
});
|
|
});
|
|
},
|
|
[itemId]
|
|
);
|
|
|
|
const rowActions = useCallback(
|
|
(record: any) => {
|
|
if (record.stock_item != undefined && record.stock_item != itemId) {
|
|
// Test results for other stock items cannot be edited
|
|
return [];
|
|
}
|
|
|
|
return [
|
|
{
|
|
title: t`Pass Test`,
|
|
color: 'green',
|
|
icon: <IconCircleCheck />,
|
|
hidden:
|
|
!record.templateId ||
|
|
record?.requires_attachment ||
|
|
record?.requires_value ||
|
|
record.result,
|
|
onClick: () => passTest(record.templateId)
|
|
},
|
|
{
|
|
title: t`Add`,
|
|
tooltip: t`Add Test Result`,
|
|
color: 'green',
|
|
icon: <IconCirclePlus />,
|
|
hidden: !user.hasAddRole(UserRoles.stock) || !record.templateId,
|
|
onClick: () => {
|
|
setSelectedTemplate(record.templateId);
|
|
newTestModal.open();
|
|
}
|
|
},
|
|
RowEditAction({
|
|
tooltip: t`Edit Test Result`,
|
|
hidden:
|
|
!user.hasChangeRole(UserRoles.stock) || !record.template_detail,
|
|
onClick: () => {
|
|
setSelectedTest(record.pk);
|
|
editTestModal.open();
|
|
}
|
|
}),
|
|
RowDeleteAction({
|
|
tooltip: t`Delete Test Result`,
|
|
hidden:
|
|
!user.hasDeleteRole(UserRoles.stock) || !record.template_detail,
|
|
onClick: () => {
|
|
setSelectedTest(record.pk);
|
|
deleteTestModal.open();
|
|
}
|
|
})
|
|
];
|
|
},
|
|
[user, itemId]
|
|
);
|
|
|
|
const tableFilters: TableFilter[] = useMemo(() => {
|
|
return [
|
|
{
|
|
name: 'required',
|
|
label: t`Required`,
|
|
description: t`Show results for required tests`
|
|
},
|
|
{
|
|
name: 'include_installed',
|
|
label: t`Include Installed`,
|
|
description: t`Show results for installed stock items`
|
|
},
|
|
{
|
|
name: 'result',
|
|
label: t`Passed`,
|
|
description: t`Show only passed tests`
|
|
}
|
|
];
|
|
}, []);
|
|
|
|
const tableActions = useMemo(() => {
|
|
return [
|
|
<AddItemButton
|
|
tooltip={t`Add Test Result`}
|
|
onClick={() => {
|
|
setSelectedTemplate(undefined);
|
|
newTestModal.open();
|
|
}}
|
|
hidden={!user.hasAddRole(UserRoles.stock)}
|
|
/>
|
|
];
|
|
}, [user]);
|
|
|
|
// Row expansion controller
|
|
const rowExpansion: any = useMemo(() => {
|
|
const cols: any = [
|
|
...tableColumns,
|
|
{
|
|
accessor: 'actions',
|
|
title: ' ',
|
|
hidden: false,
|
|
switchable: false,
|
|
width: 50,
|
|
render: (record: any) => (
|
|
<RowActions actions={rowActions(record) ?? []} />
|
|
)
|
|
}
|
|
];
|
|
|
|
return {
|
|
allowMultiple: true,
|
|
content: ({ record }: { record: any }) => {
|
|
if (!record || !record.results || record.results.length < 2) {
|
|
return null;
|
|
}
|
|
|
|
const results = record?.results ?? [];
|
|
|
|
return (
|
|
<DataTable
|
|
key={record.pk}
|
|
noHeader
|
|
columns={cols}
|
|
records={results.slice(0, -1)}
|
|
/>
|
|
);
|
|
}
|
|
};
|
|
}, []);
|
|
|
|
return (
|
|
<>
|
|
{newTestModal.modal}
|
|
{editTestModal.modal}
|
|
{deleteTestModal.modal}
|
|
<InvenTreeTable
|
|
url={apiUrl(ApiEndpoints.stock_test_result_list)}
|
|
tableState={table}
|
|
columns={tableColumns}
|
|
props={{
|
|
dataFormatter: formatRecords,
|
|
enablePagination: false,
|
|
tableActions: tableActions,
|
|
tableFilters: tableFilters,
|
|
rowActions: rowActions,
|
|
rowExpansion: rowExpansion,
|
|
params: {
|
|
stock_item: itemId,
|
|
user_detail: true,
|
|
attachment_detail: true,
|
|
template_detail: true,
|
|
enabled: true
|
|
}
|
|
}}
|
|
/>
|
|
</>
|
|
);
|
|
}
|