mirror of
https://github.com/inventree/InvenTree.git
synced 2025-07-09 15:10:54 +00:00
StockItem Serialization Improvements (#9968)
* Add "serialize" form to build output table * Fix dependencies for useGenerator hook * Improve serializing of stock - Copy test results - Ensure fields get copied across * Fix rendering for StockItemTestResultTable * Enhanced playwright test * Fix code * Fix for unit test
This commit is contained in:
@ -26,12 +26,14 @@ from mptt.managers import TreeManager
|
|||||||
from mptt.models import MPTTModel, TreeForeignKey
|
from mptt.models import MPTTModel, TreeForeignKey
|
||||||
from taggit.managers import TaggableManager
|
from taggit.managers import TaggableManager
|
||||||
|
|
||||||
|
import build.models
|
||||||
import common.models
|
import common.models
|
||||||
import InvenTree.exceptions
|
import InvenTree.exceptions
|
||||||
import InvenTree.helpers
|
import InvenTree.helpers
|
||||||
import InvenTree.models
|
import InvenTree.models
|
||||||
import InvenTree.ready
|
import InvenTree.ready
|
||||||
import InvenTree.tasks
|
import InvenTree.tasks
|
||||||
|
import order.models
|
||||||
import report.mixins
|
import report.mixins
|
||||||
import stock.tasks
|
import stock.tasks
|
||||||
from common.icons import validate_icon
|
from common.icons import validate_icon
|
||||||
@ -556,24 +558,51 @@ class StockItem(
|
|||||||
kwargs.pop('id', None)
|
kwargs.pop('id', None)
|
||||||
kwargs.pop('pk', None)
|
kwargs.pop('pk', None)
|
||||||
|
|
||||||
part = kwargs.get('part')
|
|
||||||
|
|
||||||
if not part:
|
|
||||||
raise ValidationError({'part': _('Part must be specified')})
|
|
||||||
|
|
||||||
# Create a list of StockItem objects
|
# Create a list of StockItem objects
|
||||||
items = []
|
items = []
|
||||||
|
|
||||||
# Provide some default field values
|
# Provide some default field values
|
||||||
data = {**kwargs}
|
data = {**kwargs}
|
||||||
|
|
||||||
# Remove some extraneous keys which cause issues
|
# Extract foreign-key fields from the provided data
|
||||||
for key in ['parent_id', 'part_id', 'build_id']:
|
fk_relations = {
|
||||||
data.pop(key, None)
|
'parent': StockItem,
|
||||||
|
'part': PartModels.Part,
|
||||||
|
'build': build.models.Build,
|
||||||
|
'purchase_order': order.models.PurchaseOrder,
|
||||||
|
'supplier_part': CompanyModels.SupplierPart,
|
||||||
|
'location': StockLocation,
|
||||||
|
'belongs_to': StockItem,
|
||||||
|
'customer': CompanyModels.Company,
|
||||||
|
'consumed_by': build.models.Build,
|
||||||
|
'sales_order': order.models.SalesOrder,
|
||||||
|
}
|
||||||
|
|
||||||
|
for field, model in fk_relations.items():
|
||||||
|
if instance_id := data.pop(f'{field}_id', None):
|
||||||
|
try:
|
||||||
|
instance = model.objects.get(pk=instance_id)
|
||||||
|
data[field] = instance
|
||||||
|
except (ValueError, model.DoesNotExist):
|
||||||
|
raise ValidationError({field: _(f'{field} does not exist')})
|
||||||
|
|
||||||
|
# Remove some fields which we do not want copied across
|
||||||
|
for field in [
|
||||||
|
'barcode_data',
|
||||||
|
'barcode_hash',
|
||||||
|
'stocktake_date',
|
||||||
|
'stocktake_user',
|
||||||
|
'stocktake_user_id',
|
||||||
|
]:
|
||||||
|
data.pop(field, None)
|
||||||
|
|
||||||
|
if 'part' not in data:
|
||||||
|
raise ValidationError({'part': _('Part must be specified')})
|
||||||
|
|
||||||
|
part = data['part']
|
||||||
tree_id = kwargs.pop('tree_id', 0)
|
tree_id = kwargs.pop('tree_id', 0)
|
||||||
|
|
||||||
data['parent'] = kwargs.pop('parent', None)
|
data['parent'] = kwargs.pop('parent', None) or data.get('parent')
|
||||||
data['tree_id'] = tree_id
|
data['tree_id'] = tree_id
|
||||||
data['level'] = kwargs.pop('level', 0)
|
data['level'] = kwargs.pop('level', 0)
|
||||||
data['rght'] = kwargs.pop('rght', 0)
|
data['rght'] = kwargs.pop('rght', 0)
|
||||||
@ -586,6 +615,7 @@ class StockItem(
|
|||||||
data['serial'] = serial
|
data['serial'] = serial
|
||||||
data['serial_int'] = StockItem.convert_serial_to_int(serial)
|
data['serial_int'] = StockItem.convert_serial_to_int(serial)
|
||||||
|
|
||||||
|
# Construct a new StockItem from the provided dict
|
||||||
items.append(StockItem(**data))
|
items.append(StockItem(**data))
|
||||||
|
|
||||||
# Create the StockItem objects in bulk
|
# Create the StockItem objects in bulk
|
||||||
@ -1786,7 +1816,7 @@ class StockItem(
|
|||||||
if location:
|
if location:
|
||||||
data['location'] = location
|
data['location'] = location
|
||||||
|
|
||||||
data['part'] = self.part
|
# Set the parent ID correctly
|
||||||
data['parent'] = self
|
data['parent'] = self
|
||||||
data['tree_id'] = self.tree_id
|
data['tree_id'] = self.tree_id
|
||||||
|
|
||||||
@ -1797,6 +1827,7 @@ class StockItem(
|
|||||||
history_items = []
|
history_items = []
|
||||||
|
|
||||||
for item in items:
|
for item in items:
|
||||||
|
# Construct a tracking entry for the new StockItem
|
||||||
if entry := item.add_tracking_entry(
|
if entry := item.add_tracking_entry(
|
||||||
StockHistoryCode.ASSIGNED_SERIAL,
|
StockHistoryCode.ASSIGNED_SERIAL,
|
||||||
user,
|
user,
|
||||||
@ -1807,20 +1838,11 @@ class StockItem(
|
|||||||
):
|
):
|
||||||
history_items.append(entry)
|
history_items.append(entry)
|
||||||
|
|
||||||
|
# Copy any test results from this item to the new one
|
||||||
|
item.copyTestResultsFrom(self)
|
||||||
|
|
||||||
StockItemTracking.objects.bulk_create(history_items)
|
StockItemTracking.objects.bulk_create(history_items)
|
||||||
|
|
||||||
# Duplicate test results
|
|
||||||
test_results = []
|
|
||||||
|
|
||||||
for test_result in self.test_results.all():
|
|
||||||
for item in items:
|
|
||||||
test_result.pk = None
|
|
||||||
test_result.stock_item = item
|
|
||||||
|
|
||||||
test_results.append(test_result)
|
|
||||||
|
|
||||||
StockItemTestResult.objects.bulk_create(test_results)
|
|
||||||
|
|
||||||
# Remove the equivalent number of items
|
# Remove the equivalent number of items
|
||||||
self.take_stock(quantity, user, notes=notes)
|
self.take_stock(quantity, user, notes=notes)
|
||||||
|
|
||||||
@ -1835,17 +1857,24 @@ class StockItem(
|
|||||||
item.save()
|
item.save()
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def copyTestResultsFrom(self, other, filters=None):
|
def copyTestResultsFrom(self, other: StockItem, filters: Optional[dict] = None):
|
||||||
"""Copy all test results from another StockItem."""
|
"""Copy all test results from another StockItem."""
|
||||||
# Set default - see B006
|
# Set default - see B006
|
||||||
if filters is None:
|
|
||||||
filters = {}
|
|
||||||
|
|
||||||
for result in other.test_results.all().filter(**filters):
|
results = other.test_results.all()
|
||||||
|
|
||||||
|
if filters:
|
||||||
|
results = results.filter(**filters)
|
||||||
|
|
||||||
|
results_to_create = []
|
||||||
|
|
||||||
|
for result in list(results):
|
||||||
# Create a copy of the test result by nulling-out the pk
|
# Create a copy of the test result by nulling-out the pk
|
||||||
result.pk = None
|
result.pk = None
|
||||||
result.stock_item = self
|
result.stock_item = self
|
||||||
result.save()
|
results_to_create.append(result)
|
||||||
|
|
||||||
|
StockItemTestResult.objects.bulk_create(results_to_create)
|
||||||
|
|
||||||
def add_test_result(self, create_template=True, **kwargs):
|
def add_test_result(self, create_template=True, **kwargs):
|
||||||
"""Helper function to add a new StockItemTestResult.
|
"""Helper function to add a new StockItemTestResult.
|
||||||
|
@ -340,9 +340,10 @@ export function useStockItemSerializeFields({
|
|||||||
partId: number;
|
partId: number;
|
||||||
trackable: boolean;
|
trackable: boolean;
|
||||||
modalId: string;
|
modalId: string;
|
||||||
}) {
|
}): ApiFormFieldSet {
|
||||||
const serialGenerator = useSerialNumberGenerator({
|
const serialGenerator = useSerialNumberGenerator({
|
||||||
modalId: modalId,
|
modalId: modalId,
|
||||||
|
isEnabled: () => trackable,
|
||||||
initialQuery: {
|
initialQuery: {
|
||||||
part: partId
|
part: partId
|
||||||
}
|
}
|
||||||
|
@ -79,6 +79,7 @@ export function useGenerator(props: GeneratorProps): GeneratorState {
|
|||||||
'generator',
|
'generator',
|
||||||
props.key,
|
props.key,
|
||||||
props.endpoint,
|
props.endpoint,
|
||||||
|
props.modalId,
|
||||||
props.initialQuery,
|
props.initialQuery,
|
||||||
modalState.openModals,
|
modalState.openModals,
|
||||||
debouncedQuery
|
debouncedQuery
|
||||||
|
@ -863,7 +863,6 @@ export default function StockDetail() {
|
|||||||
name: t`Serialize`,
|
name: t`Serialize`,
|
||||||
tooltip: t`Serialize stock`,
|
tooltip: t`Serialize stock`,
|
||||||
hidden:
|
hidden:
|
||||||
isBuilding ||
|
|
||||||
serialized ||
|
serialized ||
|
||||||
stockitem?.quantity < 1 ||
|
stockitem?.quantity < 1 ||
|
||||||
stockitem?.part_detail?.trackable != true,
|
stockitem?.part_detail?.trackable != true,
|
||||||
@ -1038,15 +1037,15 @@ export default function StockDetail() {
|
|||||||
id={stockitem.pk}
|
id={stockitem.pk}
|
||||||
instance={stockitem}
|
instance={stockitem}
|
||||||
/>
|
/>
|
||||||
{editStockItem.modal}
|
|
||||||
{duplicateStockItem.modal}
|
|
||||||
{deleteStockItem.modal}
|
|
||||||
{serializeStockItem.modal}
|
|
||||||
{returnStockItem.modal}
|
|
||||||
{stockAdjustActions.modals.map((modal) => modal.modal)}
|
|
||||||
{orderPartsWizard.wizard}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</InstanceDetail>
|
</InstanceDetail>
|
||||||
|
{editStockItem.modal}
|
||||||
|
{duplicateStockItem.modal}
|
||||||
|
{deleteStockItem.modal}
|
||||||
|
{serializeStockItem.modal}
|
||||||
|
{returnStockItem.modal}
|
||||||
|
{stockAdjustActions.modals.map((modal) => modal.modal)}
|
||||||
|
{orderPartsWizard.wizard}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -38,7 +38,8 @@ import {
|
|||||||
} from '../../forms/BuildForms';
|
} from '../../forms/BuildForms';
|
||||||
import {
|
import {
|
||||||
type StockOperationProps,
|
type StockOperationProps,
|
||||||
useStockFields
|
useStockFields,
|
||||||
|
useStockItemSerializeFields
|
||||||
} from '../../forms/StockForms';
|
} from '../../forms/StockForms';
|
||||||
import { InvenTreeIcon } from '../../functions/icons';
|
import { InvenTreeIcon } from '../../functions/icons';
|
||||||
import {
|
import {
|
||||||
@ -356,6 +357,28 @@ export default function BuildOutputTable({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const serializeStockFields = useStockItemSerializeFields({
|
||||||
|
partId: selectedOutputs[0]?.part,
|
||||||
|
trackable: selectedOutputs[0]?.part_detail?.trackable,
|
||||||
|
modalId: 'build-output-serialize'
|
||||||
|
});
|
||||||
|
|
||||||
|
const serializeOutput = useCreateApiFormModal({
|
||||||
|
url: ApiEndpoints.stock_serialize,
|
||||||
|
pk: selectedOutputs[0]?.pk,
|
||||||
|
title: t`Serialize Build Output`,
|
||||||
|
modalId: 'build-output-serialize',
|
||||||
|
fields: serializeStockFields,
|
||||||
|
initialData: {
|
||||||
|
quantity: selectedOutputs[0]?.quantity ?? 1,
|
||||||
|
destination: selectedOutputs[0]?.location ?? build.destination
|
||||||
|
},
|
||||||
|
onFormSuccess: () => {
|
||||||
|
table.refreshTable(true);
|
||||||
|
refreshBuild();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const tableFilters: TableFilter[] = useMemo(() => {
|
const tableFilters: TableFilter[] = useMemo(() => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@ -475,6 +498,17 @@ export default function BuildOutputTable({
|
|||||||
deallocateBuildOutput.open();
|
deallocateBuildOutput.open();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: t`Serialize`,
|
||||||
|
tooltip: t`Serialize build output`,
|
||||||
|
color: 'blue',
|
||||||
|
hidden: !record.part_detail?.trackable || !!record.serial,
|
||||||
|
icon: <InvenTreeIcon icon='serial' />,
|
||||||
|
onClick: () => {
|
||||||
|
setSelectedOutputs([record]);
|
||||||
|
serializeOutput.open();
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: t`Complete`,
|
title: t`Complete`,
|
||||||
tooltip: t`Complete build output`,
|
tooltip: t`Complete build output`,
|
||||||
@ -627,6 +661,7 @@ export default function BuildOutputTable({
|
|||||||
{editBuildOutput.modal}
|
{editBuildOutput.modal}
|
||||||
{deallocateBuildOutput.modal}
|
{deallocateBuildOutput.modal}
|
||||||
{cancelBuildOutputsForm.modal}
|
{cancelBuildOutputsForm.modal}
|
||||||
|
{serializeOutput.modal}
|
||||||
{stockAdjustActions.modals.map((modal) => modal.modal)}
|
{stockAdjustActions.modals.map((modal) => modal.modal)}
|
||||||
<OutputAllocationDrawer
|
<OutputAllocationDrawer
|
||||||
build={build}
|
build={build}
|
||||||
|
@ -7,7 +7,7 @@ import {
|
|||||||
IconInfoCircle
|
IconInfoCircle
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { DataTable } from 'mantine-datatable';
|
import { DataTable, type DataTableRowExpansionProps } from 'mantine-datatable';
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
|
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
|
||||||
@ -39,6 +39,7 @@ import {
|
|||||||
RowDeleteAction,
|
RowDeleteAction,
|
||||||
RowEditAction
|
RowEditAction
|
||||||
} from '../RowActions';
|
} from '../RowActions';
|
||||||
|
import RowExpansionIcon from '../RowExpansionIcon';
|
||||||
|
|
||||||
export default function StockItemTestResultTable({
|
export default function StockItemTestResultTable({
|
||||||
partId,
|
partId,
|
||||||
@ -131,123 +132,138 @@ export default function StockItemTestResultTable({
|
|||||||
[partId, itemId, testTemplates]
|
[partId, itemId, testTemplates]
|
||||||
);
|
);
|
||||||
|
|
||||||
const tableColumns: TableColumn[] = useMemo(() => {
|
const constructTableColumns = useCallback(
|
||||||
return [
|
(child: boolean) => {
|
||||||
{
|
return [
|
||||||
accessor: 'test',
|
{
|
||||||
title: t`Test`,
|
accessor: 'test',
|
||||||
switchable: false,
|
title: t`Test`,
|
||||||
sortable: true,
|
switchable: false,
|
||||||
render: (record: any) => {
|
sortable: true,
|
||||||
const enabled = record.enabled ?? record.template_detail?.enabled;
|
render: (record: any) => {
|
||||||
const installed =
|
const enabled = record.enabled ?? record.template_detail?.enabled;
|
||||||
record.stock_item != undefined && record.stock_item != itemId;
|
const installed =
|
||||||
|
record.stock_item != undefined && record.stock_item != itemId;
|
||||||
|
|
||||||
|
const multipleResults = record.results && record.results.length > 1;
|
||||||
|
|
||||||
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 (
|
return (
|
||||||
<Badge color='lightblue' variant='filled'>{t`No Result`}</Badge>
|
<Group justify='space-between' wrap='nowrap'>
|
||||||
|
{!child && (
|
||||||
|
<RowExpansionIcon
|
||||||
|
enabled={multipleResults}
|
||||||
|
expanded={table.isRowExpanded(record.pk)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<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: 'value',
|
||||||
|
title: t`Value`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessor: 'attachment',
|
||||||
|
title: t`Attachment`,
|
||||||
|
render: (record: any) =>
|
||||||
|
record.attachment && (
|
||||||
|
<AttachmentLink attachment={record.attachment} />
|
||||||
|
),
|
||||||
|
noContext: true
|
||||||
|
},
|
||||||
|
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`,
|
||||||
|
hidden: !includeTestStation
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessor: 'started_datetime',
|
||||||
|
sortable: true,
|
||||||
|
title: t`Started`,
|
||||||
|
hidden: !includeTestStation,
|
||||||
|
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`,
|
||||||
|
hidden: !includeTestStation,
|
||||||
|
render: (record: any) => {
|
||||||
|
return (
|
||||||
|
<Group justify='space-between'>
|
||||||
|
{formatDate(record.finished_datetime, {
|
||||||
|
showTime: true,
|
||||||
|
showSeconds: true
|
||||||
|
})}
|
||||||
|
</Group>
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
return <PassFailButton value={record.result} />;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
];
|
||||||
DescriptionColumn({}),
|
},
|
||||||
{
|
[itemId, includeTestStation, table.expandedRecords]
|
||||||
accessor: 'value',
|
);
|
||||||
title: t`Value`
|
|
||||||
},
|
const tableColumns: TableColumn[] = useMemo(() => {
|
||||||
{
|
return constructTableColumns(false);
|
||||||
accessor: 'attachment',
|
}, [itemId, includeTestStation, table.expandedRecords]);
|
||||||
title: t`Attachment`,
|
|
||||||
render: (record: any) =>
|
|
||||||
record.attachment && (
|
|
||||||
<AttachmentLink attachment={record.attachment} />
|
|
||||||
),
|
|
||||||
noContext: true
|
|
||||||
},
|
|
||||||
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`,
|
|
||||||
hidden: !includeTestStation
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessor: 'started_datetime',
|
|
||||||
sortable: true,
|
|
||||||
title: t`Started`,
|
|
||||||
hidden: !includeTestStation,
|
|
||||||
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`,
|
|
||||||
hidden: !includeTestStation,
|
|
||||||
render: (record: any) => {
|
|
||||||
return (
|
|
||||||
<Group justify='space-between'>
|
|
||||||
{formatDate(record.finished_datetime, {
|
|
||||||
showTime: true,
|
|
||||||
showSeconds: true
|
|
||||||
})}
|
|
||||||
</Group>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}, [itemId, includeTestStation]);
|
|
||||||
|
|
||||||
const [selectedTemplate, setSelectedTemplate] = useState<number | undefined>(
|
const [selectedTemplate, setSelectedTemplate] = useState<number | undefined>(
|
||||||
undefined
|
undefined
|
||||||
@ -286,7 +302,7 @@ export default function StockItemTestResultTable({
|
|||||||
pk: selectedTest,
|
pk: selectedTest,
|
||||||
fields: useMemo(() => ({ ...editResultFields }), [editResultFields]),
|
fields: useMemo(() => ({ ...editResultFields }), [editResultFields]),
|
||||||
title: t`Edit Test Result`,
|
title: t`Edit Test Result`,
|
||||||
table: table,
|
onFormSuccess: () => table.refreshTable,
|
||||||
successMessage: t`Test result updated`
|
successMessage: t`Test result updated`
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -418,9 +434,9 @@ export default function StockItemTestResultTable({
|
|||||||
}, [user]);
|
}, [user]);
|
||||||
|
|
||||||
// Row expansion controller
|
// Row expansion controller
|
||||||
const rowExpansion: any = useMemo(() => {
|
const rowExpansion: DataTableRowExpansionProps<any> = useMemo(() => {
|
||||||
const cols: any = [
|
const cols: any = [
|
||||||
...tableColumns,
|
...constructTableColumns(true),
|
||||||
{
|
{
|
||||||
accessor: 'actions',
|
accessor: 'actions',
|
||||||
title: ' ',
|
title: ' ',
|
||||||
@ -435,7 +451,12 @@ export default function StockItemTestResultTable({
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
allowMultiple: true,
|
allowMultiple: true,
|
||||||
expandable: (record: any) => record.results && record.results.length > 1,
|
expandable: ({ record }: { record: any }) => {
|
||||||
|
return (
|
||||||
|
table.isRowExpanded(record.pk) ||
|
||||||
|
(record.results && record.results.length > 1)
|
||||||
|
);
|
||||||
|
},
|
||||||
content: ({ record }: { record: any }) => {
|
content: ({ record }: { record: any }) => {
|
||||||
if (!record || !record.results || record.results.length < 2) {
|
if (!record || !record.results || record.results.length < 2) {
|
||||||
return null;
|
return null;
|
||||||
@ -454,7 +475,7 @@ export default function StockItemTestResultTable({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, []);
|
}, [constructTableColumns, table.isRowExpanded]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { test } from '../baseFixtures.js';
|
import { expect, test } from '../baseFixtures.js';
|
||||||
import {
|
import {
|
||||||
clearTableFilters,
|
clearTableFilters,
|
||||||
clickButtonIfVisible,
|
clickButtonIfVisible,
|
||||||
@ -199,12 +199,25 @@ test('Stock - Serialize', async ({ browser }) => {
|
|||||||
await page.getByLabel('action-menu-stock-operations').click();
|
await page.getByLabel('action-menu-stock-operations').click();
|
||||||
await page.getByLabel('action-menu-stock-operations-serialize').click();
|
await page.getByLabel('action-menu-stock-operations-serialize').click();
|
||||||
|
|
||||||
|
// Check for expected placeholder value
|
||||||
|
await expect(
|
||||||
|
page.getByRole('textbox', { name: 'text-field-serial_numbers' })
|
||||||
|
).toHaveAttribute('placeholder', 'Next serial number: 365');
|
||||||
|
|
||||||
await page.getByLabel('text-field-serial_numbers').fill('200-250');
|
await page.getByLabel('text-field-serial_numbers').fill('200-250');
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Submit' }).click();
|
await page.getByRole('button', { name: 'Submit' }).click();
|
||||||
await page
|
await page
|
||||||
.getByText('Group range 200-250 exceeds allowed quantity')
|
.getByText('Group range 200-250 exceeds allowed quantity')
|
||||||
.waitFor();
|
.waitFor();
|
||||||
|
|
||||||
|
await page.getByLabel('text-field-serial_numbers').fill('1, 2, 3');
|
||||||
|
await page.waitForTimeout(250);
|
||||||
|
await page.getByRole('button', { name: 'Submit' }).click();
|
||||||
|
await page
|
||||||
|
.getByText('Number of unique serial numbers (3) must match quantity (10)')
|
||||||
|
.waitFor();
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Cancel' }).click();
|
await page.getByRole('button', { name: 'Cancel' }).click();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user