mirror of
https://github.com/inventree/InvenTree.git
synced 2025-04-28 19:46:46 +00:00
API date filter updates (#8544)
* Add 'stocktake_before' and 'stocktake_after' filters for StockItem API * Enable new filters for StockItemTable * Update CUI table filters * Add more date filter options for orders * Add date filters to BuildList * Update BuildOrderTable filters * Add more order date filters * Cleanup PurchaseOrderFilter code * Implement more PUI table filters * Add "Completion Date" column to PurchaseOrderTable * Update ReturnOrderTable * Add 'text' option for TableFilter * filter state management * Bump API version * Sorting for table filters * Add playwright tests for stock table filtering * Playwright updates - Add some helper functions for common operations * Refactoring for Playwright tests
This commit is contained in:
parent
5e762bc7f7
commit
809a978f7d
@ -1,13 +1,20 @@
|
|||||||
"""InvenTree API version information."""
|
"""InvenTree API version information."""
|
||||||
|
|
||||||
# InvenTree API version
|
# InvenTree API version
|
||||||
INVENTREE_API_VERSION = 283
|
INVENTREE_API_VERSION = 284
|
||||||
|
|
||||||
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
||||||
|
|
||||||
|
|
||||||
INVENTREE_API_TEXT = """
|
INVENTREE_API_TEXT = """
|
||||||
|
|
||||||
|
v284 - 2024-11-25 : https://github.com/inventree/InvenTree/pull/8544
|
||||||
|
- Adds new date filters to the StockItem API
|
||||||
|
- Adds new date filters to the BuildOrder API
|
||||||
|
- Adds new date filters to the SalesOrder API
|
||||||
|
- Adds new date filters to the PurchaseOrder API
|
||||||
|
- Adds new date filters to the ReturnOrder API
|
||||||
|
|
||||||
v283 - 2024-11-20 : https://github.com/inventree/InvenTree/pull/8524
|
v283 - 2024-11-20 : https://github.com/inventree/InvenTree/pull/8524
|
||||||
- Adds "note" field to the PartRelated API endpoint
|
- Adds "note" field to the PartRelated API endpoint
|
||||||
|
|
||||||
|
@ -23,7 +23,7 @@ import build.serializers
|
|||||||
from build.models import Build, BuildLine, BuildItem
|
from build.models import Build, BuildLine, BuildItem
|
||||||
import part.models
|
import part.models
|
||||||
from users.models import Owner
|
from users.models import Owner
|
||||||
from InvenTree.filters import SEARCH_ORDER_FILTER_ALIAS
|
from InvenTree.filters import InvenTreeDateFilter, SEARCH_ORDER_FILTER_ALIAS
|
||||||
|
|
||||||
|
|
||||||
class BuildFilter(rest_filters.FilterSet):
|
class BuildFilter(rest_filters.FilterSet):
|
||||||
@ -179,6 +179,36 @@ class BuildFilter(rest_filters.FilterSet):
|
|||||||
return queryset.exclude(project_code=None)
|
return queryset.exclude(project_code=None)
|
||||||
return queryset.filter(project_code=None)
|
return queryset.filter(project_code=None)
|
||||||
|
|
||||||
|
created_before = InvenTreeDateFilter(
|
||||||
|
label=_('Created before'),
|
||||||
|
field_name='creation_date', lookup_expr='lt'\
|
||||||
|
)
|
||||||
|
|
||||||
|
created_after = InvenTreeDateFilter(
|
||||||
|
label=_('Created after'),
|
||||||
|
field_name='creation_date', lookup_expr='gt'
|
||||||
|
)
|
||||||
|
|
||||||
|
target_date_before = InvenTreeDateFilter(
|
||||||
|
label=_('Target date before'),
|
||||||
|
field_name='target_date', lookup_expr='lt'
|
||||||
|
)
|
||||||
|
|
||||||
|
target_date_after = InvenTreeDateFilter(
|
||||||
|
label=_('Target date after'),
|
||||||
|
field_name='target_date', lookup_expr='gt'
|
||||||
|
)
|
||||||
|
|
||||||
|
completed_before = InvenTreeDateFilter(
|
||||||
|
label=_('Completed before'),
|
||||||
|
field_name='completion_date', lookup_expr='lt'
|
||||||
|
)
|
||||||
|
|
||||||
|
completed_after = InvenTreeDateFilter(
|
||||||
|
label=_('Completed after'),
|
||||||
|
field_name='completion_date', lookup_expr='gt'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class BuildMixin:
|
class BuildMixin:
|
||||||
"""Mixin class for Build API endpoints."""
|
"""Mixin class for Build API endpoints."""
|
||||||
|
@ -21,7 +21,11 @@ import company.models
|
|||||||
from generic.states.api import StatusView
|
from generic.states.api import StatusView
|
||||||
from importer.mixins import DataExportViewMixin
|
from importer.mixins import DataExportViewMixin
|
||||||
from InvenTree.api import ListCreateDestroyAPIView, MetadataView
|
from InvenTree.api import ListCreateDestroyAPIView, MetadataView
|
||||||
from InvenTree.filters import SEARCH_ORDER_FILTER, SEARCH_ORDER_FILTER_ALIAS
|
from InvenTree.filters import (
|
||||||
|
SEARCH_ORDER_FILTER,
|
||||||
|
SEARCH_ORDER_FILTER_ALIAS,
|
||||||
|
InvenTreeDateFilter,
|
||||||
|
)
|
||||||
from InvenTree.helpers import str2bool
|
from InvenTree.helpers import str2bool
|
||||||
from InvenTree.helpers_model import construct_absolute_url, get_base_url
|
from InvenTree.helpers_model import construct_absolute_url, get_base_url
|
||||||
from InvenTree.mixins import CreateAPI, ListAPI, ListCreateAPI, RetrieveUpdateDestroyAPI
|
from InvenTree.mixins import CreateAPI, ListAPI, ListCreateAPI, RetrieveUpdateDestroyAPI
|
||||||
@ -140,6 +144,22 @@ class OrderFilter(rest_filters.FilterSet):
|
|||||||
queryset=Owner.objects.all(), field_name='responsible', label=_('Responsible')
|
queryset=Owner.objects.all(), field_name='responsible', label=_('Responsible')
|
||||||
)
|
)
|
||||||
|
|
||||||
|
created_before = InvenTreeDateFilter(
|
||||||
|
label=_('Created Before'), field_name='creation_date', lookup_expr='lt'
|
||||||
|
)
|
||||||
|
|
||||||
|
created_after = InvenTreeDateFilter(
|
||||||
|
label=_('Created After'), field_name='creation_date', lookup_expr='gt'
|
||||||
|
)
|
||||||
|
|
||||||
|
target_date_before = InvenTreeDateFilter(
|
||||||
|
label=_('Target Date Before'), field_name='target_date', lookup_expr='lt'
|
||||||
|
)
|
||||||
|
|
||||||
|
target_date_after = InvenTreeDateFilter(
|
||||||
|
label=_('Target Date After'), field_name='target_date', lookup_expr='gt'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class LineItemFilter(rest_filters.FilterSet):
|
class LineItemFilter(rest_filters.FilterSet):
|
||||||
"""Base class for custom API filters for order line item list(s)."""
|
"""Base class for custom API filters for order line item list(s)."""
|
||||||
@ -171,6 +191,41 @@ class PurchaseOrderFilter(OrderFilter):
|
|||||||
model = models.PurchaseOrder
|
model = models.PurchaseOrder
|
||||||
fields = ['supplier']
|
fields = ['supplier']
|
||||||
|
|
||||||
|
part = rest_filters.ModelChoiceFilter(
|
||||||
|
queryset=Part.objects.all(),
|
||||||
|
field_name='part',
|
||||||
|
label=_('Part'),
|
||||||
|
method='filter_part',
|
||||||
|
)
|
||||||
|
|
||||||
|
def filter_part(self, queryset, name, part: Part):
|
||||||
|
"""Filter by provided Part instance."""
|
||||||
|
orders = part.purchase_orders()
|
||||||
|
|
||||||
|
return queryset.filter(pk__in=[o.pk for o in orders])
|
||||||
|
|
||||||
|
supplier_part = rest_filters.ModelChoiceFilter(
|
||||||
|
queryset=company.models.SupplierPart.objects.all(),
|
||||||
|
label=_('Supplier Part'),
|
||||||
|
method='filter_supplier_part',
|
||||||
|
)
|
||||||
|
|
||||||
|
def filter_supplier_part(
|
||||||
|
self, queryset, name, supplier_part: company.models.SupplierPart
|
||||||
|
):
|
||||||
|
"""Filter by provided SupplierPart instance."""
|
||||||
|
orders = supplier_part.purchase_orders()
|
||||||
|
|
||||||
|
return queryset.filter(pk__in=[o.pk for o in orders])
|
||||||
|
|
||||||
|
completed_before = InvenTreeDateFilter(
|
||||||
|
label=_('Completed Before'), field_name='complete_date', lookup_expr='lt'
|
||||||
|
)
|
||||||
|
|
||||||
|
completed_after = InvenTreeDateFilter(
|
||||||
|
label=_('Completed After'), field_name='complete_date', lookup_expr='gt'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class PurchaseOrderMixin:
|
class PurchaseOrderMixin:
|
||||||
"""Mixin class for PurchaseOrder endpoints."""
|
"""Mixin class for PurchaseOrder endpoints."""
|
||||||
@ -221,32 +276,6 @@ class PurchaseOrderList(PurchaseOrderMixin, DataExportViewMixin, ListCreateAPI):
|
|||||||
|
|
||||||
params = self.request.query_params
|
params = self.request.query_params
|
||||||
|
|
||||||
# Attempt to filter by part
|
|
||||||
part = params.get('part', None)
|
|
||||||
|
|
||||||
if part is not None:
|
|
||||||
try:
|
|
||||||
part = Part.objects.get(pk=part)
|
|
||||||
queryset = queryset.filter(
|
|
||||||
id__in=[p.id for p in part.purchase_orders()]
|
|
||||||
)
|
|
||||||
except (Part.DoesNotExist, ValueError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Attempt to filter by supplier part
|
|
||||||
supplier_part = params.get('supplier_part', None)
|
|
||||||
|
|
||||||
if supplier_part is not None:
|
|
||||||
try:
|
|
||||||
supplier_part = company.models.SupplierPart.objects.get(
|
|
||||||
pk=supplier_part
|
|
||||||
)
|
|
||||||
queryset = queryset.filter(
|
|
||||||
id__in=[p.id for p in supplier_part.purchase_orders()]
|
|
||||||
)
|
|
||||||
except (ValueError, company.models.SupplierPart.DoesNotExist):
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Filter by 'date range'
|
# Filter by 'date range'
|
||||||
min_date = params.get('min_date', None)
|
min_date = params.get('min_date', None)
|
||||||
max_date = params.get('max_date', None)
|
max_date = params.get('max_date', None)
|
||||||
@ -276,6 +305,7 @@ class PurchaseOrderList(PurchaseOrderMixin, DataExportViewMixin, ListCreateAPI):
|
|||||||
'reference',
|
'reference',
|
||||||
'supplier__name',
|
'supplier__name',
|
||||||
'target_date',
|
'target_date',
|
||||||
|
'complete_date',
|
||||||
'line_items',
|
'line_items',
|
||||||
'status',
|
'status',
|
||||||
'responsible',
|
'responsible',
|
||||||
@ -648,6 +678,14 @@ class SalesOrderFilter(OrderFilter):
|
|||||||
# Now we have a list of matching IDs, filter the queryset
|
# Now we have a list of matching IDs, filter the queryset
|
||||||
return queryset.filter(pk__in=sales_orders)
|
return queryset.filter(pk__in=sales_orders)
|
||||||
|
|
||||||
|
completed_before = InvenTreeDateFilter(
|
||||||
|
label=_('Completed Before'), field_name='shipment_date', lookup_expr='lt'
|
||||||
|
)
|
||||||
|
|
||||||
|
completed_after = InvenTreeDateFilter(
|
||||||
|
label=_('Completed After'), field_name='shipment_date', lookup_expr='gt'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class SalesOrderMixin:
|
class SalesOrderMixin:
|
||||||
"""Mixin class for SalesOrder endpoints."""
|
"""Mixin class for SalesOrder endpoints."""
|
||||||
@ -1257,6 +1295,14 @@ class ReturnOrderFilter(OrderFilter):
|
|||||||
# Now we have a list of matching IDs, filter the queryset
|
# Now we have a list of matching IDs, filter the queryset
|
||||||
return queryset.filter(pk__in=return_orders)
|
return queryset.filter(pk__in=return_orders)
|
||||||
|
|
||||||
|
completed_before = InvenTreeDateFilter(
|
||||||
|
label=_('Completed Before'), field_name='complete_date', lookup_expr='lt'
|
||||||
|
)
|
||||||
|
|
||||||
|
completed_after = InvenTreeDateFilter(
|
||||||
|
label=_('Completed After'), field_name='complete_date', lookup_expr='gt'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ReturnOrderMixin:
|
class ReturnOrderMixin:
|
||||||
"""Mixin class for ReturnOrder endpoints."""
|
"""Mixin class for ReturnOrder endpoints."""
|
||||||
@ -1325,6 +1371,7 @@ class ReturnOrderList(ReturnOrderMixin, DataExportViewMixin, ListCreateAPI):
|
|||||||
'line_items',
|
'line_items',
|
||||||
'status',
|
'status',
|
||||||
'target_date',
|
'target_date',
|
||||||
|
'complete_date',
|
||||||
'project_code',
|
'project_code',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -1159,10 +1159,10 @@ class PartFilter(rest_filters.FilterSet):
|
|||||||
|
|
||||||
# Created date filters
|
# Created date filters
|
||||||
created_before = InvenTreeDateFilter(
|
created_before = InvenTreeDateFilter(
|
||||||
label='Updated before', field_name='creation_date', lookup_expr='lte'
|
label='Updated before', field_name='creation_date', lookup_expr='lt'
|
||||||
)
|
)
|
||||||
created_after = InvenTreeDateFilter(
|
created_after = InvenTreeDateFilter(
|
||||||
label='Updated after', field_name='creation_date', lookup_expr='gte'
|
label='Updated after', field_name='creation_date', lookup_expr='gt'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -807,19 +807,28 @@ class StockFilter(rest_filters.FilterSet):
|
|||||||
|
|
||||||
# Update date filters
|
# Update date filters
|
||||||
updated_before = InvenTreeDateFilter(
|
updated_before = InvenTreeDateFilter(
|
||||||
label='Updated before', field_name='updated', lookup_expr='lte'
|
label=_('Updated before'), field_name='updated', lookup_expr='lt'
|
||||||
)
|
)
|
||||||
|
|
||||||
updated_after = InvenTreeDateFilter(
|
updated_after = InvenTreeDateFilter(
|
||||||
label='Updated after', field_name='updated', lookup_expr='gte'
|
label=_('Updated after'), field_name='updated', lookup_expr='gt'
|
||||||
|
)
|
||||||
|
|
||||||
|
stocktake_before = InvenTreeDateFilter(
|
||||||
|
label=_('Stocktake Before'), field_name='stocktake_date', lookup_expr='lt'
|
||||||
|
)
|
||||||
|
|
||||||
|
stocktake_after = InvenTreeDateFilter(
|
||||||
|
label=_('Stocktake After'), field_name='stocktake_date', lookup_expr='gt'
|
||||||
)
|
)
|
||||||
|
|
||||||
# Stock "expiry" filters
|
# Stock "expiry" filters
|
||||||
expiry_date_lte = InvenTreeDateFilter(
|
expiry_before = InvenTreeDateFilter(
|
||||||
label=_('Expiry date before'), field_name='expiry_date', lookup_expr='lte'
|
label=_('Expiry date before'), field_name='expiry_date', lookup_expr='lt'
|
||||||
)
|
)
|
||||||
|
|
||||||
expiry_date_gte = InvenTreeDateFilter(
|
expiry_after = InvenTreeDateFilter(
|
||||||
label=_('Expiry date after'), field_name='expiry_date', lookup_expr='gte'
|
label=_('Expiry date after'), field_name='expiry_date', lookup_expr='gt'
|
||||||
)
|
)
|
||||||
|
|
||||||
stale = rest_filters.BooleanFilter(label=_('Stale'), method='filter_stale')
|
stale = rest_filters.BooleanFilter(label=_('Stale'), method='filter_stale')
|
||||||
|
@ -421,11 +421,11 @@ function getStockTableFilters() {
|
|||||||
title: '{% trans "Has purchase price" %}',
|
title: '{% trans "Has purchase price" %}',
|
||||||
description: '{% trans "Show stock items which have a purchase price set" %}',
|
description: '{% trans "Show stock items which have a purchase price set" %}',
|
||||||
},
|
},
|
||||||
expiry_date_lte: {
|
expiry_before: {
|
||||||
type: 'date',
|
type: 'date',
|
||||||
title: '{% trans "Expiry Date before" %}',
|
title: '{% trans "Expiry Date before" %}',
|
||||||
},
|
},
|
||||||
expiry_date_gte: {
|
expiry_after: {
|
||||||
type: 'date',
|
type: 'date',
|
||||||
title: '{% trans "Expiry Date after" %}',
|
title: '{% trans "Expiry Date after" %}',
|
||||||
},
|
},
|
||||||
|
@ -3,8 +3,6 @@ import { Group, Skeleton, Stack, Text } from '@mantine/core';
|
|||||||
import { IconInfoCircle, IconPackages, IconSitemap } from '@tabler/icons-react';
|
import { IconInfoCircle, IconPackages, IconSitemap } from '@tabler/icons-react';
|
||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
|
|
||||||
import { ActionButton } from '../../components/buttons/ActionButton';
|
|
||||||
import AdminButton from '../../components/buttons/AdminButton';
|
import AdminButton from '../../components/buttons/AdminButton';
|
||||||
import { PrintingActions } from '../../components/buttons/PrintingActions';
|
import { PrintingActions } from '../../components/buttons/PrintingActions';
|
||||||
import {
|
import {
|
||||||
@ -278,12 +276,6 @@ export default function Stock() {
|
|||||||
() => [
|
() => [
|
||||||
<AdminButton model={ModelType.stocklocation} id={location.pk} />,
|
<AdminButton model={ModelType.stocklocation} id={location.pk} />,
|
||||||
<LocateItemButton locationId={location.pk} />,
|
<LocateItemButton locationId={location.pk} />,
|
||||||
<ActionButton
|
|
||||||
icon={<InvenTreeIcon icon='stocktake' />}
|
|
||||||
onClick={notYetImplemented}
|
|
||||||
variant='outline'
|
|
||||||
size='lg'
|
|
||||||
/>,
|
|
||||||
location.pk ? (
|
location.pk ? (
|
||||||
<BarcodeActionDropdown
|
<BarcodeActionDropdown
|
||||||
model={ModelType.stocklocation}
|
model={ModelType.stocklocation}
|
||||||
|
@ -242,6 +242,14 @@ export function CreationDateColumn(props: TableColumnProps): TableColumn {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function CompletionDateColumn(props: TableColumnProps): TableColumn {
|
||||||
|
return DateColumn({
|
||||||
|
accessor: 'completion_date',
|
||||||
|
title: t`Completion Date`,
|
||||||
|
...props
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function ShipmentDateColumn(props: TableColumnProps): TableColumn {
|
export function ShipmentDateColumn(props: TableColumnProps): TableColumn {
|
||||||
return DateColumn({
|
return DateColumn({
|
||||||
accessor: 'shipment_date',
|
accessor: 'shipment_date',
|
||||||
|
@ -17,8 +17,9 @@ export type TableFilterChoice = {
|
|||||||
* boolean: A simple true/false filter
|
* boolean: A simple true/false filter
|
||||||
* choice: A filter which allows selection from a list of (supplied)
|
* choice: A filter which allows selection from a list of (supplied)
|
||||||
* date: A filter which allows selection from a date input
|
* date: A filter which allows selection from a date input
|
||||||
|
* text: A filter which allows raw text input
|
||||||
*/
|
*/
|
||||||
export type TableFilterType = 'boolean' | 'choice' | 'date';
|
export type TableFilterType = 'boolean' | 'choice' | 'date' | 'text';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface for the table filter type. Provides a number of options for selecting filter value:
|
* Interface for the table filter type. Provides a number of options for selecting filter value:
|
||||||
@ -137,6 +138,60 @@ export function MaxDateFilter(): TableFilter {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function CreatedBeforeFilter(): TableFilter {
|
||||||
|
return {
|
||||||
|
name: 'created_before',
|
||||||
|
label: t`Created Before`,
|
||||||
|
description: t`Show items created before this date`,
|
||||||
|
type: 'date'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CreatedAfterFilter(): TableFilter {
|
||||||
|
return {
|
||||||
|
name: 'created_after',
|
||||||
|
label: t`Created After`,
|
||||||
|
description: t`Show items created after this date`,
|
||||||
|
type: 'date'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TargetDateBeforeFilter(): TableFilter {
|
||||||
|
return {
|
||||||
|
name: 'target_date_before',
|
||||||
|
label: t`Target Date Before`,
|
||||||
|
description: t`Show items with a target date before this date`,
|
||||||
|
type: 'date'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TargetDateAfterFilter(): TableFilter {
|
||||||
|
return {
|
||||||
|
name: 'target_date_after',
|
||||||
|
label: t`Target Date After`,
|
||||||
|
description: t`Show items with a target date after this date`,
|
||||||
|
type: 'date'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CompletedBeforeFilter(): TableFilter {
|
||||||
|
return {
|
||||||
|
name: 'completed_before',
|
||||||
|
label: t`Completed Before`,
|
||||||
|
description: t`Show items completed before this date`,
|
||||||
|
type: 'date'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CompletedAfterFilter(): TableFilter {
|
||||||
|
return {
|
||||||
|
name: 'completed_after',
|
||||||
|
label: t`Completed After`,
|
||||||
|
description: t`Show items completed after this date`,
|
||||||
|
type: 'date'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function HasProjectCodeFilter(): TableFilter {
|
export function HasProjectCodeFilter(): TableFilter {
|
||||||
return {
|
return {
|
||||||
name: 'has_project_code',
|
name: 'has_project_code',
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import {
|
import {
|
||||||
|
ActionIcon,
|
||||||
Badge,
|
Badge,
|
||||||
Button,
|
Button,
|
||||||
CloseButton,
|
CloseButton,
|
||||||
@ -10,12 +11,14 @@ import {
|
|||||||
Select,
|
Select,
|
||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
|
TextInput,
|
||||||
Tooltip
|
Tooltip
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { DateInput, type DateValue } from '@mantine/dates';
|
import { DateInput, type DateValue } from '@mantine/dates';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
|
import { IconCheck } from '@tabler/icons-react';
|
||||||
import { StylishText } from '../components/items/StylishText';
|
import { StylishText } from '../components/items/StylishText';
|
||||||
import type { TableState } from '../hooks/UseTable';
|
import type { TableState } from '../hooks/UseTable';
|
||||||
import {
|
import {
|
||||||
@ -60,6 +63,77 @@ function FilterItem({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function FilterElement({
|
||||||
|
filterType,
|
||||||
|
valueOptions,
|
||||||
|
onValueChange
|
||||||
|
}: {
|
||||||
|
filterType: TableFilterType;
|
||||||
|
valueOptions: TableFilterChoice[];
|
||||||
|
onValueChange: (value: string | null) => void;
|
||||||
|
}) {
|
||||||
|
const setDateValue = useCallback(
|
||||||
|
(value: DateValue) => {
|
||||||
|
if (value) {
|
||||||
|
const date = value.toString();
|
||||||
|
onValueChange(dayjs(date).format('YYYY-MM-DD'));
|
||||||
|
} else {
|
||||||
|
onValueChange('');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[onValueChange]
|
||||||
|
);
|
||||||
|
|
||||||
|
const [textValue, setTextValue] = useState<string>('');
|
||||||
|
|
||||||
|
switch (filterType) {
|
||||||
|
case 'text':
|
||||||
|
return (
|
||||||
|
<TextInput
|
||||||
|
label={t`Value`}
|
||||||
|
value={textValue}
|
||||||
|
placeholder={t`Enter filter value`}
|
||||||
|
rightSection={
|
||||||
|
<ActionIcon
|
||||||
|
aria-label='apply-text-filter'
|
||||||
|
variant='transparent'
|
||||||
|
onClick={() => onValueChange(textValue)}
|
||||||
|
>
|
||||||
|
<IconCheck />
|
||||||
|
</ActionIcon>
|
||||||
|
}
|
||||||
|
onChange={(e) => setTextValue(e.currentTarget.value)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
onValueChange(textValue);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case 'date':
|
||||||
|
return (
|
||||||
|
<DateInput
|
||||||
|
label={t`Value`}
|
||||||
|
placeholder={t`Select date value`}
|
||||||
|
onChange={setDateValue}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case 'choice':
|
||||||
|
case 'boolean':
|
||||||
|
default:
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
data={valueOptions}
|
||||||
|
searchable={filterType != 'boolean'}
|
||||||
|
label={t`Value`}
|
||||||
|
placeholder={t`Select filter value`}
|
||||||
|
onChange={(value: string | null) => onValueChange(value)}
|
||||||
|
maxDropdownHeight={800}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function FilterAddGroup({
|
function FilterAddGroup({
|
||||||
tableState,
|
tableState,
|
||||||
availableFilters
|
availableFilters
|
||||||
@ -79,6 +153,7 @@ function FilterAddGroup({
|
|||||||
return (
|
return (
|
||||||
availableFilters
|
availableFilters
|
||||||
?.filter((flt) => !activeFilterNames.includes(flt.name))
|
?.filter((flt) => !activeFilterNames.includes(flt.name))
|
||||||
|
?.sort((a, b) => a.label.localeCompare(b.label))
|
||||||
?.map((flt) => ({
|
?.map((flt) => ({
|
||||||
value: flt.name,
|
value: flt.name,
|
||||||
label: flt.label,
|
label: flt.label,
|
||||||
@ -133,22 +208,13 @@ function FilterAddGroup({
|
|||||||
};
|
};
|
||||||
|
|
||||||
tableState.setActiveFilters([...filters, newFilter]);
|
tableState.setActiveFilters([...filters, newFilter]);
|
||||||
|
|
||||||
|
// Clear selected filter
|
||||||
|
setSelectedFilter(null);
|
||||||
},
|
},
|
||||||
[selectedFilter]
|
[selectedFilter]
|
||||||
);
|
);
|
||||||
|
|
||||||
const setDateValue = useCallback(
|
|
||||||
(value: DateValue) => {
|
|
||||||
if (value) {
|
|
||||||
const date = value.toString();
|
|
||||||
setSelectedValue(dayjs(date).format('YYYY-MM-DD'));
|
|
||||||
} else {
|
|
||||||
setSelectedValue('');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[setSelectedValue]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack gap='xs'>
|
<Stack gap='xs'>
|
||||||
<Divider />
|
<Divider />
|
||||||
@ -160,23 +226,13 @@ function FilterAddGroup({
|
|||||||
onChange={(value: string | null) => setSelectedFilter(value)}
|
onChange={(value: string | null) => setSelectedFilter(value)}
|
||||||
maxDropdownHeight={800}
|
maxDropdownHeight={800}
|
||||||
/>
|
/>
|
||||||
{selectedFilter &&
|
{selectedFilter && (
|
||||||
(filterType === 'date' ? (
|
<FilterElement
|
||||||
<DateInput
|
filterType={filterType}
|
||||||
label={t`Value`}
|
valueOptions={valueOptions}
|
||||||
placeholder={t`Select date value`}
|
onValueChange={setSelectedValue}
|
||||||
onChange={setDateValue}
|
/>
|
||||||
/>
|
)}
|
||||||
) : (
|
|
||||||
<Select
|
|
||||||
data={valueOptions}
|
|
||||||
label={t`Value`}
|
|
||||||
searchable={true}
|
|
||||||
placeholder={t`Select filter value`}
|
|
||||||
onChange={(value: string | null) => setSelectedValue(value)}
|
|
||||||
maxDropdownHeight={800}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -25,12 +25,18 @@ import {
|
|||||||
} from '../ColumnRenderers';
|
} from '../ColumnRenderers';
|
||||||
import {
|
import {
|
||||||
AssignedToMeFilter,
|
AssignedToMeFilter,
|
||||||
|
CompletedAfterFilter,
|
||||||
|
CompletedBeforeFilter,
|
||||||
|
CreatedAfterFilter,
|
||||||
|
CreatedBeforeFilter,
|
||||||
HasProjectCodeFilter,
|
HasProjectCodeFilter,
|
||||||
MaxDateFilter,
|
MaxDateFilter,
|
||||||
MinDateFilter,
|
MinDateFilter,
|
||||||
OverdueFilter,
|
OverdueFilter,
|
||||||
StatusFilterOptions,
|
StatusFilterOptions,
|
||||||
type TableFilter
|
type TableFilter,
|
||||||
|
TargetDateAfterFilter,
|
||||||
|
TargetDateBeforeFilter
|
||||||
} from '../Filter';
|
} from '../Filter';
|
||||||
import { InvenTreeTable } from '../InvenTreeTable';
|
import { InvenTreeTable } from '../InvenTreeTable';
|
||||||
|
|
||||||
@ -130,6 +136,12 @@ export function BuildOrderTable({
|
|||||||
AssignedToMeFilter(),
|
AssignedToMeFilter(),
|
||||||
MinDateFilter(),
|
MinDateFilter(),
|
||||||
MaxDateFilter(),
|
MaxDateFilter(),
|
||||||
|
CreatedBeforeFilter(),
|
||||||
|
CreatedAfterFilter(),
|
||||||
|
TargetDateBeforeFilter(),
|
||||||
|
TargetDateAfterFilter(),
|
||||||
|
CompletedBeforeFilter(),
|
||||||
|
CompletedAfterFilter(),
|
||||||
{
|
{
|
||||||
name: 'project_code',
|
name: 'project_code',
|
||||||
label: t`Project Code`,
|
label: t`Project Code`,
|
||||||
|
@ -14,6 +14,7 @@ import { useTable } from '../../hooks/UseTable';
|
|||||||
import { apiUrl } from '../../states/ApiState';
|
import { apiUrl } from '../../states/ApiState';
|
||||||
import { useUserState } from '../../states/UserState';
|
import { useUserState } from '../../states/UserState';
|
||||||
import {
|
import {
|
||||||
|
CompletionDateColumn,
|
||||||
CreationDateColumn,
|
CreationDateColumn,
|
||||||
DescriptionColumn,
|
DescriptionColumn,
|
||||||
LineItemsProgressColumn,
|
LineItemsProgressColumn,
|
||||||
@ -25,13 +26,19 @@ import {
|
|||||||
} from '../ColumnRenderers';
|
} from '../ColumnRenderers';
|
||||||
import {
|
import {
|
||||||
AssignedToMeFilter,
|
AssignedToMeFilter,
|
||||||
|
CompletedAfterFilter,
|
||||||
|
CompletedBeforeFilter,
|
||||||
|
CreatedAfterFilter,
|
||||||
|
CreatedBeforeFilter,
|
||||||
HasProjectCodeFilter,
|
HasProjectCodeFilter,
|
||||||
MaxDateFilter,
|
MaxDateFilter,
|
||||||
MinDateFilter,
|
MinDateFilter,
|
||||||
OutstandingFilter,
|
OutstandingFilter,
|
||||||
OverdueFilter,
|
OverdueFilter,
|
||||||
StatusFilterOptions,
|
StatusFilterOptions,
|
||||||
type TableFilter
|
type TableFilter,
|
||||||
|
TargetDateAfterFilter,
|
||||||
|
TargetDateBeforeFilter
|
||||||
} from '../Filter';
|
} from '../Filter';
|
||||||
import { InvenTreeTable } from '../InvenTreeTable';
|
import { InvenTreeTable } from '../InvenTreeTable';
|
||||||
|
|
||||||
@ -64,6 +71,12 @@ export function PurchaseOrderTable({
|
|||||||
AssignedToMeFilter(),
|
AssignedToMeFilter(),
|
||||||
MinDateFilter(),
|
MinDateFilter(),
|
||||||
MaxDateFilter(),
|
MaxDateFilter(),
|
||||||
|
CreatedBeforeFilter(),
|
||||||
|
CreatedAfterFilter(),
|
||||||
|
TargetDateBeforeFilter(),
|
||||||
|
TargetDateAfterFilter(),
|
||||||
|
CompletedBeforeFilter(),
|
||||||
|
CompletedAfterFilter(),
|
||||||
{
|
{
|
||||||
name: 'project_code',
|
name: 'project_code',
|
||||||
label: t`Project Code`,
|
label: t`Project Code`,
|
||||||
@ -108,6 +121,9 @@ export function PurchaseOrderTable({
|
|||||||
ProjectCodeColumn({}),
|
ProjectCodeColumn({}),
|
||||||
CreationDateColumn({}),
|
CreationDateColumn({}),
|
||||||
TargetDateColumn({}),
|
TargetDateColumn({}),
|
||||||
|
CompletionDateColumn({
|
||||||
|
accessor: 'complete_date'
|
||||||
|
}),
|
||||||
{
|
{
|
||||||
accessor: 'total_price',
|
accessor: 'total_price',
|
||||||
title: t`Total Price`,
|
title: t`Total Price`,
|
||||||
|
@ -14,8 +14,8 @@ import { useTable } from '../../hooks/UseTable';
|
|||||||
import { apiUrl } from '../../states/ApiState';
|
import { apiUrl } from '../../states/ApiState';
|
||||||
import { useUserState } from '../../states/UserState';
|
import { useUserState } from '../../states/UserState';
|
||||||
import {
|
import {
|
||||||
|
CompletionDateColumn,
|
||||||
CreationDateColumn,
|
CreationDateColumn,
|
||||||
DateColumn,
|
|
||||||
DescriptionColumn,
|
DescriptionColumn,
|
||||||
LineItemsProgressColumn,
|
LineItemsProgressColumn,
|
||||||
ProjectCodeColumn,
|
ProjectCodeColumn,
|
||||||
@ -26,13 +26,19 @@ import {
|
|||||||
} from '../ColumnRenderers';
|
} from '../ColumnRenderers';
|
||||||
import {
|
import {
|
||||||
AssignedToMeFilter,
|
AssignedToMeFilter,
|
||||||
|
CompletedAfterFilter,
|
||||||
|
CompletedBeforeFilter,
|
||||||
|
CreatedAfterFilter,
|
||||||
|
CreatedBeforeFilter,
|
||||||
HasProjectCodeFilter,
|
HasProjectCodeFilter,
|
||||||
MaxDateFilter,
|
MaxDateFilter,
|
||||||
MinDateFilter,
|
MinDateFilter,
|
||||||
OutstandingFilter,
|
OutstandingFilter,
|
||||||
OverdueFilter,
|
OverdueFilter,
|
||||||
StatusFilterOptions,
|
StatusFilterOptions,
|
||||||
type TableFilter
|
type TableFilter,
|
||||||
|
TargetDateAfterFilter,
|
||||||
|
TargetDateBeforeFilter
|
||||||
} from '../Filter';
|
} from '../Filter';
|
||||||
import { InvenTreeTable } from '../InvenTreeTable';
|
import { InvenTreeTable } from '../InvenTreeTable';
|
||||||
|
|
||||||
@ -62,6 +68,12 @@ export function ReturnOrderTable({
|
|||||||
AssignedToMeFilter(),
|
AssignedToMeFilter(),
|
||||||
MinDateFilter(),
|
MinDateFilter(),
|
||||||
MaxDateFilter(),
|
MaxDateFilter(),
|
||||||
|
CreatedBeforeFilter(),
|
||||||
|
CreatedAfterFilter(),
|
||||||
|
TargetDateBeforeFilter(),
|
||||||
|
TargetDateAfterFilter(),
|
||||||
|
CompletedBeforeFilter(),
|
||||||
|
CompletedAfterFilter(),
|
||||||
{
|
{
|
||||||
name: 'project_code',
|
name: 'project_code',
|
||||||
label: t`Project Code`,
|
label: t`Project Code`,
|
||||||
@ -117,9 +129,8 @@ export function ReturnOrderTable({
|
|||||||
ProjectCodeColumn({}),
|
ProjectCodeColumn({}),
|
||||||
CreationDateColumn({}),
|
CreationDateColumn({}),
|
||||||
TargetDateColumn({}),
|
TargetDateColumn({}),
|
||||||
DateColumn({
|
CompletionDateColumn({
|
||||||
accessor: 'complete_date',
|
accessor: 'complete_date'
|
||||||
title: t`Completion Date`
|
|
||||||
}),
|
}),
|
||||||
ResponsibleColumn({}),
|
ResponsibleColumn({}),
|
||||||
{
|
{
|
||||||
|
@ -27,13 +27,19 @@ import {
|
|||||||
} from '../ColumnRenderers';
|
} from '../ColumnRenderers';
|
||||||
import {
|
import {
|
||||||
AssignedToMeFilter,
|
AssignedToMeFilter,
|
||||||
|
CompletedAfterFilter,
|
||||||
|
CompletedBeforeFilter,
|
||||||
|
CreatedAfterFilter,
|
||||||
|
CreatedBeforeFilter,
|
||||||
HasProjectCodeFilter,
|
HasProjectCodeFilter,
|
||||||
MaxDateFilter,
|
MaxDateFilter,
|
||||||
MinDateFilter,
|
MinDateFilter,
|
||||||
OutstandingFilter,
|
OutstandingFilter,
|
||||||
OverdueFilter,
|
OverdueFilter,
|
||||||
StatusFilterOptions,
|
StatusFilterOptions,
|
||||||
type TableFilter
|
type TableFilter,
|
||||||
|
TargetDateAfterFilter,
|
||||||
|
TargetDateBeforeFilter
|
||||||
} from '../Filter';
|
} from '../Filter';
|
||||||
import { InvenTreeTable } from '../InvenTreeTable';
|
import { InvenTreeTable } from '../InvenTreeTable';
|
||||||
|
|
||||||
@ -63,6 +69,12 @@ export function SalesOrderTable({
|
|||||||
AssignedToMeFilter(),
|
AssignedToMeFilter(),
|
||||||
MinDateFilter(),
|
MinDateFilter(),
|
||||||
MaxDateFilter(),
|
MaxDateFilter(),
|
||||||
|
CreatedBeforeFilter(),
|
||||||
|
CreatedAfterFilter(),
|
||||||
|
TargetDateBeforeFilter(),
|
||||||
|
TargetDateAfterFilter(),
|
||||||
|
CompletedBeforeFilter(),
|
||||||
|
CompletedAfterFilter(),
|
||||||
{
|
{
|
||||||
name: 'project_code',
|
name: 'project_code',
|
||||||
label: t`Project Code`,
|
label: t`Project Code`,
|
||||||
|
@ -286,7 +286,11 @@ function stockItemTableColumns(): TableColumn[] {
|
|||||||
/**
|
/**
|
||||||
* Construct a list of available filters for the stock item table
|
* Construct a list of available filters for the stock item table
|
||||||
*/
|
*/
|
||||||
function stockItemTableFilters(): TableFilter[] {
|
function stockItemTableFilters({
|
||||||
|
enableExpiry
|
||||||
|
}: {
|
||||||
|
enableExpiry: boolean;
|
||||||
|
}): TableFilter[] {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
name: 'active',
|
name: 'active',
|
||||||
@ -354,15 +358,35 @@ function stockItemTableFilters(): TableFilter[] {
|
|||||||
label: t`Is Serialized`,
|
label: t`Is Serialized`,
|
||||||
description: t`Show items which have a serial number`
|
description: t`Show items which have a serial number`
|
||||||
},
|
},
|
||||||
// TODO: serial
|
{
|
||||||
// TODO: serial_gte
|
name: 'batch',
|
||||||
// TODO: serial_lte
|
label: t`Batch Code`,
|
||||||
|
description: t`Filter items by batch code`,
|
||||||
|
type: 'text'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'serial',
|
||||||
|
label: t`Serial Number`,
|
||||||
|
description: t`Filter items by serial number`,
|
||||||
|
type: 'text'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'serial_lte',
|
||||||
|
label: t`Serial Number LTE`,
|
||||||
|
description: t`Show items with serial numbers less than or equal to a given value`,
|
||||||
|
type: 'text'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'serial_gte',
|
||||||
|
label: t`Serial Number GTE`,
|
||||||
|
description: t`Show items with serial numbers greater than or equal to a given value`,
|
||||||
|
type: 'text'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'has_batch',
|
name: 'has_batch',
|
||||||
label: t`Has Batch Code`,
|
label: t`Has Batch Code`,
|
||||||
description: t`Show items which have a batch code`
|
description: t`Show items which have a batch code`
|
||||||
},
|
},
|
||||||
// TODO: batch
|
|
||||||
{
|
{
|
||||||
name: 'tracked',
|
name: 'tracked',
|
||||||
label: t`Tracked`,
|
label: t`Tracked`,
|
||||||
@ -373,10 +397,56 @@ function stockItemTableFilters(): TableFilter[] {
|
|||||||
label: t`Has Purchase Price`,
|
label: t`Has Purchase Price`,
|
||||||
description: t`Show items which have a purchase price`
|
description: t`Show items which have a purchase price`
|
||||||
},
|
},
|
||||||
// TODO: Expired
|
{
|
||||||
// TODO: stale
|
name: 'expired',
|
||||||
// TODO: expiry_date_lte
|
label: t`Expired`,
|
||||||
// TODO: expiry_date_gte
|
description: t`Show items which have expired`,
|
||||||
|
active: enableExpiry
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'stale',
|
||||||
|
label: t`Stale`,
|
||||||
|
description: t`Show items which are stale`,
|
||||||
|
active: enableExpiry
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'expiry_before',
|
||||||
|
label: t`Expired Before`,
|
||||||
|
description: t`Show items which expired before this date`,
|
||||||
|
type: 'date',
|
||||||
|
active: enableExpiry
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'expiry_after',
|
||||||
|
label: t`Expired After`,
|
||||||
|
description: t`Show items which expired after this date`,
|
||||||
|
type: 'date',
|
||||||
|
active: enableExpiry
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'updated_before',
|
||||||
|
label: t`Updated Before`,
|
||||||
|
description: t`Show items updated before this date`,
|
||||||
|
type: 'date'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'updated_after',
|
||||||
|
label: t`Updated After`,
|
||||||
|
description: t`Show items updated after this date`,
|
||||||
|
type: 'date'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'stocktake_before',
|
||||||
|
label: t`Stocktake Before`,
|
||||||
|
description: t`Show items counted before this date`,
|
||||||
|
type: 'date'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'stocktake_after',
|
||||||
|
label: t`Stocktake After`,
|
||||||
|
description: t`Show items counted after this date`,
|
||||||
|
type: 'date'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'external',
|
name: 'external',
|
||||||
label: t`External Location`,
|
label: t`External Location`,
|
||||||
@ -397,12 +467,25 @@ export function StockItemTable({
|
|||||||
allowAdd?: boolean;
|
allowAdd?: boolean;
|
||||||
tableName: string;
|
tableName: string;
|
||||||
}>) {
|
}>) {
|
||||||
const tableColumns = useMemo(() => stockItemTableColumns(), []);
|
|
||||||
const tableFilters = useMemo(() => stockItemTableFilters(), []);
|
|
||||||
|
|
||||||
const table = useTable(tableName);
|
const table = useTable(tableName);
|
||||||
const user = useUserState();
|
const user = useUserState();
|
||||||
|
|
||||||
|
const settings = useGlobalSettingsState();
|
||||||
|
|
||||||
|
const stockExpiryEnabled = useMemo(
|
||||||
|
() => settings.isSet('STOCK_ENABLE_EXPIRY'),
|
||||||
|
[settings]
|
||||||
|
);
|
||||||
|
|
||||||
|
const tableColumns = useMemo(() => stockItemTableColumns(), []);
|
||||||
|
const tableFilters = useMemo(
|
||||||
|
() =>
|
||||||
|
stockItemTableFilters({
|
||||||
|
enableExpiry: stockExpiryEnabled
|
||||||
|
}),
|
||||||
|
[stockExpiryEnabled]
|
||||||
|
);
|
||||||
|
|
||||||
const tableActionParams: StockOperationProps = useMemo(() => {
|
const tableActionParams: StockOperationProps = useMemo(() => {
|
||||||
return {
|
return {
|
||||||
items: table.selectedRecords,
|
items: table.selectedRecords,
|
||||||
|
46
src/frontend/tests/helpers.ts
Normal file
46
src/frontend/tests/helpers.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
/**
|
||||||
|
* Open the filter drawer for the currently visible table
|
||||||
|
* @param page - The page object
|
||||||
|
*/
|
||||||
|
export const openFilterDrawer = async (page) => {
|
||||||
|
await page.getByLabel('table-select-filters').click();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close the filter drawer for the currently visible table
|
||||||
|
* @param page - The page object
|
||||||
|
*/
|
||||||
|
export const closeFilterDrawer = async (page) => {
|
||||||
|
await page.getByLabel('filter-drawer-close').click();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Click the specified button (if it is visible)
|
||||||
|
* @param page - The page object
|
||||||
|
* @param name - The name of the button to click
|
||||||
|
*/
|
||||||
|
export const clickButtonIfVisible = async (page, name, timeout = 500) => {
|
||||||
|
await page.waitForTimeout(timeout);
|
||||||
|
|
||||||
|
if (await page.getByRole('button', { name }).isVisible()) {
|
||||||
|
await page.getByRole('button', { name }).click();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all filters from the currently visible table
|
||||||
|
* @param page - The page object
|
||||||
|
*/
|
||||||
|
export const clearTableFilters = async (page) => {
|
||||||
|
await openFilterDrawer(page);
|
||||||
|
await clickButtonIfVisible(page, 'Clear Filters');
|
||||||
|
await page.getByLabel('filter-drawer-close').click();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the parent 'row' element for a given 'cell' element
|
||||||
|
* @param cell - The cell element
|
||||||
|
*/
|
||||||
|
export const getRowFromCell = async (cell) => {
|
||||||
|
return cell.locator('xpath=ancestor::tr').first();
|
||||||
|
};
|
@ -1,8 +1,13 @@
|
|||||||
import { test } from '../baseFixtures.ts';
|
import { test } from '../baseFixtures.ts';
|
||||||
import { baseUrl } from '../defaults.ts';
|
import { baseUrl } from '../defaults.ts';
|
||||||
|
import {
|
||||||
|
clickButtonIfVisible,
|
||||||
|
getRowFromCell,
|
||||||
|
openFilterDrawer
|
||||||
|
} from '../helpers.ts';
|
||||||
import { doQuickLogin } from '../login.ts';
|
import { doQuickLogin } from '../login.ts';
|
||||||
|
|
||||||
test('Pages - Build Order', async ({ page }) => {
|
test('Build Order - Basic Tests', async ({ page }) => {
|
||||||
await doQuickLogin(page);
|
await doQuickLogin(page);
|
||||||
|
|
||||||
await page.goto(`${baseUrl}/part/`);
|
await page.goto(`${baseUrl}/part/`);
|
||||||
@ -82,7 +87,7 @@ test('Pages - Build Order', async ({ page }) => {
|
|||||||
.waitFor();
|
.waitFor();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Pages - Build Order - Build Outputs', async ({ page }) => {
|
test('Build Order - Build Outputs', async ({ page }) => {
|
||||||
await doQuickLogin(page);
|
await doQuickLogin(page);
|
||||||
|
|
||||||
await page.goto(`${baseUrl}/part/`);
|
await page.goto(`${baseUrl}/part/`);
|
||||||
@ -140,7 +145,7 @@ test('Pages - Build Order - Build Outputs', async ({ page }) => {
|
|||||||
|
|
||||||
// Cancel one of the newly created outputs
|
// Cancel one of the newly created outputs
|
||||||
const cell = await page.getByRole('cell', { name: `# ${sn}` });
|
const cell = await page.getByRole('cell', { name: `# ${sn}` });
|
||||||
const row = await cell.locator('xpath=ancestor::tr').first();
|
const row = await getRowFromCell(cell);
|
||||||
await row.getByLabel(/row-action-menu-/i).click();
|
await row.getByLabel(/row-action-menu-/i).click();
|
||||||
await page.getByRole('menuitem', { name: 'Cancel' }).click();
|
await page.getByRole('menuitem', { name: 'Cancel' }).click();
|
||||||
await page.getByRole('button', { name: 'Submit' }).click();
|
await page.getByRole('button', { name: 'Submit' }).click();
|
||||||
@ -148,7 +153,7 @@ test('Pages - Build Order - Build Outputs', async ({ page }) => {
|
|||||||
|
|
||||||
// Complete the other output
|
// Complete the other output
|
||||||
const cell2 = await page.getByRole('cell', { name: `# ${sn + 1}` });
|
const cell2 = await page.getByRole('cell', { name: `# ${sn + 1}` });
|
||||||
const row2 = await cell2.locator('xpath=ancestor::tr').first();
|
const row2 = await getRowFromCell(cell2);
|
||||||
await row2.getByLabel(/row-action-menu-/i).click();
|
await row2.getByLabel(/row-action-menu-/i).click();
|
||||||
await page.getByRole('menuitem', { name: 'Complete' }).click();
|
await page.getByRole('menuitem', { name: 'Complete' }).click();
|
||||||
await page.getByLabel('related-field-location').click();
|
await page.getByLabel('related-field-location').click();
|
||||||
@ -158,7 +163,7 @@ test('Pages - Build Order - Build Outputs', async ({ page }) => {
|
|||||||
await page.getByText('Build outputs have been completed').waitFor();
|
await page.getByText('Build outputs have been completed').waitFor();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Pages - Build Order - Allocation', async ({ page }) => {
|
test('Build Order - Allocation', async ({ page }) => {
|
||||||
await doQuickLogin(page);
|
await doQuickLogin(page);
|
||||||
|
|
||||||
await page.goto(`${baseUrl}/manufacturing/build-order/1/line-items`);
|
await page.goto(`${baseUrl}/manufacturing/build-order/1/line-items`);
|
||||||
@ -170,7 +175,7 @@ test('Pages - Build Order - Allocation', async ({ page }) => {
|
|||||||
|
|
||||||
// The capacitor stock should be fully allocated
|
// The capacitor stock should be fully allocated
|
||||||
const cell = await page.getByRole('cell', { name: /C_1uF_0805/ });
|
const cell = await page.getByRole('cell', { name: /C_1uF_0805/ });
|
||||||
const row = await cell.locator('xpath=ancestor::tr').first();
|
const row = await getRowFromCell(cell);
|
||||||
|
|
||||||
await row.getByText(/150 \/ 150/).waitFor();
|
await row.getByText(/150 \/ 150/).waitFor();
|
||||||
|
|
||||||
@ -237,7 +242,7 @@ test('Pages - Build Order - Allocation', async ({ page }) => {
|
|||||||
const item = data[idx];
|
const item = data[idx];
|
||||||
|
|
||||||
const cell = await page.getByRole('cell', { name: item.name });
|
const cell = await page.getByRole('cell', { name: item.name });
|
||||||
const row = await cell.locator('xpath=ancestor::tr').first();
|
const row = await getRowFromCell(cell);
|
||||||
const progress = `${item.allocated} / ${item.required}`;
|
const progress = `${item.allocated} / ${item.required}`;
|
||||||
|
|
||||||
await row.getByRole('cell', { name: item.ipn }).first().waitFor();
|
await row.getByRole('cell', { name: item.ipn }).first().waitFor();
|
||||||
@ -257,3 +262,14 @@ test('Pages - Build Order - Allocation', async ({ page }) => {
|
|||||||
.getByRole('menuitem', { name: 'Deallocate Stock', exact: true })
|
.getByRole('menuitem', { name: 'Deallocate Stock', exact: true })
|
||||||
.waitFor();
|
.waitFor();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Build Order - Filters', async ({ page }) => {
|
||||||
|
await doQuickLogin(page);
|
||||||
|
|
||||||
|
await page.goto(`${baseUrl}/manufacturing/index/buildorders`);
|
||||||
|
|
||||||
|
await openFilterDrawer(page);
|
||||||
|
await clickButtonIfVisible(page, 'Clear Filters');
|
||||||
|
|
||||||
|
await page.waitForTimeout(2500);
|
||||||
|
});
|
||||||
|
@ -2,7 +2,7 @@ import { test } from '../baseFixtures.js';
|
|||||||
import { doQuickLogin } from '../login.js';
|
import { doQuickLogin } from '../login.js';
|
||||||
import { setPluginState } from '../settings.js';
|
import { setPluginState } from '../settings.js';
|
||||||
|
|
||||||
test('Pages - Dashboard - Basic', async ({ page }) => {
|
test('Dashboard - Basic', async ({ page }) => {
|
||||||
await doQuickLogin(page);
|
await doQuickLogin(page);
|
||||||
|
|
||||||
await page.getByText('Use the menu to add widgets').waitFor();
|
await page.getByText('Use the menu to add widgets').waitFor();
|
||||||
@ -35,7 +35,7 @@ test('Pages - Dashboard - Basic', async ({ page }) => {
|
|||||||
await page.getByLabel('dashboard-accept-layout').click();
|
await page.getByLabel('dashboard-accept-layout').click();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Pages - Dashboard - Plugins', async ({ page, request }) => {
|
test('Dashboard - Plugins', async ({ page, request }) => {
|
||||||
// Ensure that the "SampleUI" plugin is enabled
|
// Ensure that the "SampleUI" plugin is enabled
|
||||||
await setPluginState({
|
await setPluginState({
|
||||||
request,
|
request,
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { test } from '../baseFixtures';
|
import { test } from '../baseFixtures';
|
||||||
import { baseUrl } from '../defaults';
|
import { baseUrl } from '../defaults';
|
||||||
|
import { getRowFromCell } from '../helpers';
|
||||||
import { doQuickLogin } from '../login';
|
import { doQuickLogin } from '../login';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -129,9 +130,7 @@ test('Parts - Allocations', async ({ page }) => {
|
|||||||
|
|
||||||
// Check "progress" bar of BO0001
|
// Check "progress" bar of BO0001
|
||||||
const build_order_cell = await page.getByRole('cell', { name: 'BO0001' });
|
const build_order_cell = await page.getByRole('cell', { name: 'BO0001' });
|
||||||
const build_order_row = await build_order_cell
|
const build_order_row = await getRowFromCell(build_order_cell);
|
||||||
.locator('xpath=ancestor::tr')
|
|
||||||
.first();
|
|
||||||
await build_order_row.getByText('11 / 75').waitFor();
|
await build_order_row.getByText('11 / 75').waitFor();
|
||||||
|
|
||||||
// Expand allocations against BO0001
|
// Expand allocations against BO0001
|
||||||
@ -147,9 +146,7 @@ test('Parts - Allocations', async ({ page }) => {
|
|||||||
|
|
||||||
// Check "progress" bar of SO0025
|
// Check "progress" bar of SO0025
|
||||||
const sales_order_cell = await page.getByRole('cell', { name: 'SO0025' });
|
const sales_order_cell = await page.getByRole('cell', { name: 'SO0025' });
|
||||||
const sales_order_row = await sales_order_cell
|
const sales_order_row = await getRowFromCell(sales_order_cell);
|
||||||
.locator('xpath=ancestor::tr')
|
|
||||||
.first();
|
|
||||||
await sales_order_row.getByText('3 / 10').waitFor();
|
await sales_order_row.getByText('3 / 10').waitFor();
|
||||||
|
|
||||||
// Expand allocations against SO0025
|
// Expand allocations against SO0025
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { test } from '../baseFixtures.ts';
|
import { test } from '../baseFixtures.ts';
|
||||||
|
import { clickButtonIfVisible, openFilterDrawer } from '../helpers.ts';
|
||||||
import { doQuickLogin } from '../login.ts';
|
import { doQuickLogin } from '../login.ts';
|
||||||
|
|
||||||
test('Purchase Orders - General', async ({ page }) => {
|
test('Purchase Orders - General', async ({ page }) => {
|
||||||
@ -51,6 +52,30 @@ test('Purchase Orders - General', async ({ page }) => {
|
|||||||
await page.getByRole('tab', { name: 'Details' }).waitFor();
|
await page.getByRole('tab', { name: 'Details' }).waitFor();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Purchase Orders - Filters', async ({ page }) => {
|
||||||
|
await doQuickLogin(page, 'reader', 'readonly');
|
||||||
|
|
||||||
|
await page.getByRole('tab', { name: 'Purchasing' }).click();
|
||||||
|
await page.getByRole('tab', { name: 'Purchase Orders' }).click();
|
||||||
|
|
||||||
|
// Open filters drawer
|
||||||
|
await openFilterDrawer(page);
|
||||||
|
await clickButtonIfVisible(page, 'Clear Filters');
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Add Filter' }).click();
|
||||||
|
|
||||||
|
// Check for expected filter options
|
||||||
|
await page.getByPlaceholder('Select filter').fill('before');
|
||||||
|
await page.getByRole('option', { name: 'Created Before' }).waitFor();
|
||||||
|
await page.getByRole('option', { name: 'Completed Before' }).waitFor();
|
||||||
|
await page.getByRole('option', { name: 'Target Date Before' }).waitFor();
|
||||||
|
|
||||||
|
await page.getByPlaceholder('Select filter').fill('after');
|
||||||
|
await page.getByRole('option', { name: 'Created After' }).waitFor();
|
||||||
|
await page.getByRole('option', { name: 'Completed After' }).waitFor();
|
||||||
|
await page.getByRole('option', { name: 'Target Date After' }).waitFor();
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests for receiving items against a purchase order
|
* Tests for receiving items against a purchase order
|
||||||
*/
|
*/
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import { test } from '../baseFixtures.js';
|
import { test } from '../baseFixtures.js';
|
||||||
import { baseUrl } from '../defaults.js';
|
import { baseUrl } from '../defaults.js';
|
||||||
|
import { clickButtonIfVisible, openFilterDrawer } from '../helpers.js';
|
||||||
import { doQuickLogin } from '../login.js';
|
import { doQuickLogin } from '../login.js';
|
||||||
|
|
||||||
test('Stock', async ({ page }) => {
|
test('Stock - Basic Tests', async ({ page }) => {
|
||||||
await doQuickLogin(page);
|
await doQuickLogin(page);
|
||||||
|
|
||||||
await page.goto(`${baseUrl}/stock/location/index/`);
|
await page.goto(`${baseUrl}/stock/location/index/`);
|
||||||
@ -49,6 +50,45 @@ test('Stock - Location Tree', async ({ page }) => {
|
|||||||
await page.getByRole('cell', { name: 'Factory' }).first().waitFor();
|
await page.getByRole('cell', { name: 'Factory' }).first().waitFor();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Stock - Filters', async ({ page }) => {
|
||||||
|
await doQuickLogin(page, 'steven', 'wizardstaff');
|
||||||
|
|
||||||
|
await page.goto(`${baseUrl}/stock/location/index/`);
|
||||||
|
await page.getByRole('tab', { name: 'Stock Items' }).click();
|
||||||
|
|
||||||
|
await openFilterDrawer(page);
|
||||||
|
await clickButtonIfVisible(page, 'Clear Filters');
|
||||||
|
|
||||||
|
// Filter by updated date
|
||||||
|
await page.getByRole('button', { name: 'Add Filter' }).click();
|
||||||
|
await page.getByPlaceholder('Select filter').fill('updated');
|
||||||
|
await page.getByText('Updated After').click();
|
||||||
|
await page.getByPlaceholder('Select date value').fill('2010-01-01');
|
||||||
|
await page.getByText('Show items updated after this date').waitFor();
|
||||||
|
|
||||||
|
// Filter by batch code
|
||||||
|
await page.getByRole('button', { name: 'Add Filter' }).click();
|
||||||
|
await page.getByPlaceholder('Select filter').fill('batch');
|
||||||
|
await page
|
||||||
|
.getByRole('option', { name: 'Batch Code', exact: true })
|
||||||
|
.locator('span')
|
||||||
|
.click();
|
||||||
|
await page.getByPlaceholder('Enter filter value').fill('TABLE-B02');
|
||||||
|
await page.getByLabel('apply-text-filter').click();
|
||||||
|
|
||||||
|
// Close dialog
|
||||||
|
await page.keyboard.press('Escape');
|
||||||
|
|
||||||
|
// Ensure correct result is displayed
|
||||||
|
await page
|
||||||
|
.getByRole('cell', { name: 'A round table - with blue paint' })
|
||||||
|
.waitFor();
|
||||||
|
|
||||||
|
// Clear filters (ready for next set of tests)
|
||||||
|
await openFilterDrawer(page);
|
||||||
|
await clickButtonIfVisible(page, 'Clear Filters');
|
||||||
|
});
|
||||||
|
|
||||||
test('Stock - Serial Numbers', async ({ page }) => {
|
test('Stock - Serial Numbers', async ({ page }) => {
|
||||||
await doQuickLogin(page);
|
await doQuickLogin(page);
|
||||||
|
|
||||||
|
@ -1,23 +1,23 @@
|
|||||||
import { test } from './baseFixtures.js';
|
import { test } from './baseFixtures.js';
|
||||||
import { baseUrl } from './defaults.js';
|
import { baseUrl } from './defaults.js';
|
||||||
|
import {
|
||||||
|
clearTableFilters,
|
||||||
|
closeFilterDrawer,
|
||||||
|
openFilterDrawer
|
||||||
|
} from './helpers.js';
|
||||||
import { doQuickLogin } from './login.js';
|
import { doQuickLogin } from './login.js';
|
||||||
|
|
||||||
// Helper function to set the value of a specific table filter
|
// Helper function to set the value of a specific table filter
|
||||||
const setFilter = async (page, name: string, value: string) => {
|
const setFilter = async (page, name: string, value: string) => {
|
||||||
await page.getByLabel('table-select-filters').click();
|
await openFilterDrawer(page);
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Add Filter' }).click();
|
await page.getByRole('button', { name: 'Add Filter' }).click();
|
||||||
await page.getByPlaceholder('Select filter').click();
|
await page.getByPlaceholder('Select filter').click();
|
||||||
await page.getByRole('option', { name: name, exact: true }).click();
|
await page.getByRole('option', { name: name, exact: true }).click();
|
||||||
await page.getByPlaceholder('Select filter value').click();
|
await page.getByPlaceholder('Select filter value').click();
|
||||||
await page.getByRole('option', { name: value, exact: true }).click();
|
await page.getByRole('option', { name: value, exact: true }).click();
|
||||||
await page.getByLabel('filter-drawer-close').click();
|
|
||||||
};
|
|
||||||
|
|
||||||
// Helper function to clear table filters
|
await closeFilterDrawer(page);
|
||||||
const clearFilters = async (page) => {
|
|
||||||
await page.getByLabel('table-select-filters').click();
|
|
||||||
await page.getByRole('button', { name: 'Clear Filters' }).click();
|
|
||||||
await page.getByLabel('filter-drawer-close').click();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
test('Tables - Filters', async ({ page }) => {
|
test('Tables - Filters', async ({ page }) => {
|
||||||
@ -30,14 +30,14 @@ test('Tables - Filters', async ({ page }) => {
|
|||||||
await setFilter(page, 'Responsible', 'allaccess');
|
await setFilter(page, 'Responsible', 'allaccess');
|
||||||
await setFilter(page, 'Project Code', 'PRJ-NIM');
|
await setFilter(page, 'Project Code', 'PRJ-NIM');
|
||||||
|
|
||||||
await clearFilters(page);
|
await clearTableFilters(page);
|
||||||
|
|
||||||
// Head to the "part list" page
|
// Head to the "part list" page
|
||||||
await page.goto(`${baseUrl}/part/category/index/parts/`);
|
await page.goto(`${baseUrl}/part/category/index/parts/`);
|
||||||
|
|
||||||
await setFilter(page, 'Assembly', 'Yes');
|
await setFilter(page, 'Assembly', 'Yes');
|
||||||
|
|
||||||
await clearFilters(page);
|
await clearTableFilters(page);
|
||||||
|
|
||||||
// Head to the "purchase order list" page
|
// Head to the "purchase order list" page
|
||||||
await page.goto(`${baseUrl}/purchasing/index/purchaseorders/`);
|
await page.goto(`${baseUrl}/purchasing/index/purchaseorders/`);
|
||||||
@ -47,7 +47,7 @@ test('Tables - Filters', async ({ page }) => {
|
|||||||
await setFilter(page, 'Assigned to me', 'No');
|
await setFilter(page, 'Assigned to me', 'No');
|
||||||
await setFilter(page, 'Project Code', 'PRO-ZEN');
|
await setFilter(page, 'Project Code', 'PRO-ZEN');
|
||||||
|
|
||||||
await clearFilters(page);
|
await clearTableFilters(page);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Tables - Columns', async ({ page }) => {
|
test('Tables - Columns', async ({ page }) => {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user