2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-04-28 11:36:44 +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:
Oliver 2024-11-25 21:36:31 +11:00 committed by GitHub
parent 5e762bc7f7
commit 809a978f7d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 586 additions and 124 deletions

View File

@ -1,13 +1,20 @@
"""InvenTree API version information."""
# 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."""
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
- Adds "note" field to the PartRelated API endpoint

View File

@ -23,7 +23,7 @@ import build.serializers
from build.models import Build, BuildLine, BuildItem
import part.models
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):
@ -179,6 +179,36 @@ class BuildFilter(rest_filters.FilterSet):
return queryset.exclude(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:
"""Mixin class for Build API endpoints."""

View File

@ -21,7 +21,11 @@ import company.models
from generic.states.api import StatusView
from importer.mixins import DataExportViewMixin
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_model import construct_absolute_url, get_base_url
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')
)
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):
"""Base class for custom API filters for order line item list(s)."""
@ -171,6 +191,41 @@ class PurchaseOrderFilter(OrderFilter):
model = models.PurchaseOrder
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:
"""Mixin class for PurchaseOrder endpoints."""
@ -221,32 +276,6 @@ class PurchaseOrderList(PurchaseOrderMixin, DataExportViewMixin, ListCreateAPI):
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'
min_date = params.get('min_date', None)
max_date = params.get('max_date', None)
@ -276,6 +305,7 @@ class PurchaseOrderList(PurchaseOrderMixin, DataExportViewMixin, ListCreateAPI):
'reference',
'supplier__name',
'target_date',
'complete_date',
'line_items',
'status',
'responsible',
@ -648,6 +678,14 @@ class SalesOrderFilter(OrderFilter):
# Now we have a list of matching IDs, filter the queryset
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:
"""Mixin class for SalesOrder endpoints."""
@ -1257,6 +1295,14 @@ class ReturnOrderFilter(OrderFilter):
# Now we have a list of matching IDs, filter the queryset
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:
"""Mixin class for ReturnOrder endpoints."""
@ -1325,6 +1371,7 @@ class ReturnOrderList(ReturnOrderMixin, DataExportViewMixin, ListCreateAPI):
'line_items',
'status',
'target_date',
'complete_date',
'project_code',
]

View File

@ -1159,10 +1159,10 @@ class PartFilter(rest_filters.FilterSet):
# Created date filters
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(
label='Updated after', field_name='creation_date', lookup_expr='gte'
label='Updated after', field_name='creation_date', lookup_expr='gt'
)

View File

@ -807,19 +807,28 @@ class StockFilter(rest_filters.FilterSet):
# Update date filters
updated_before = InvenTreeDateFilter(
label='Updated before', field_name='updated', lookup_expr='lte'
label=_('Updated before'), field_name='updated', lookup_expr='lt'
)
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
expiry_date_lte = InvenTreeDateFilter(
label=_('Expiry date before'), field_name='expiry_date', lookup_expr='lte'
expiry_before = InvenTreeDateFilter(
label=_('Expiry date before'), field_name='expiry_date', lookup_expr='lt'
)
expiry_date_gte = InvenTreeDateFilter(
label=_('Expiry date after'), field_name='expiry_date', lookup_expr='gte'
expiry_after = InvenTreeDateFilter(
label=_('Expiry date after'), field_name='expiry_date', lookup_expr='gt'
)
stale = rest_filters.BooleanFilter(label=_('Stale'), method='filter_stale')

View File

@ -421,11 +421,11 @@ function getStockTableFilters() {
title: '{% trans "Has purchase price" %}',
description: '{% trans "Show stock items which have a purchase price set" %}',
},
expiry_date_lte: {
expiry_before: {
type: 'date',
title: '{% trans "Expiry Date before" %}',
},
expiry_date_gte: {
expiry_after: {
type: 'date',
title: '{% trans "Expiry Date after" %}',
},

View File

@ -3,8 +3,6 @@ import { Group, Skeleton, Stack, Text } from '@mantine/core';
import { IconInfoCircle, IconPackages, IconSitemap } from '@tabler/icons-react';
import { useMemo, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { ActionButton } from '../../components/buttons/ActionButton';
import AdminButton from '../../components/buttons/AdminButton';
import { PrintingActions } from '../../components/buttons/PrintingActions';
import {
@ -278,12 +276,6 @@ export default function Stock() {
() => [
<AdminButton model={ModelType.stocklocation} id={location.pk} />,
<LocateItemButton locationId={location.pk} />,
<ActionButton
icon={<InvenTreeIcon icon='stocktake' />}
onClick={notYetImplemented}
variant='outline'
size='lg'
/>,
location.pk ? (
<BarcodeActionDropdown
model={ModelType.stocklocation}

View File

@ -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 {
return DateColumn({
accessor: 'shipment_date',

View File

@ -17,8 +17,9 @@ export type TableFilterChoice = {
* boolean: A simple true/false filter
* choice: A filter which allows selection from a list of (supplied)
* 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:
@ -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 {
return {
name: 'has_project_code',

View File

@ -1,5 +1,6 @@
import { t } from '@lingui/macro';
import {
ActionIcon,
Badge,
Button,
CloseButton,
@ -10,12 +11,14 @@ import {
Select,
Stack,
Text,
TextInput,
Tooltip
} from '@mantine/core';
import { DateInput, type DateValue } from '@mantine/dates';
import dayjs from 'dayjs';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { IconCheck } from '@tabler/icons-react';
import { StylishText } from '../components/items/StylishText';
import type { TableState } from '../hooks/UseTable';
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({
tableState,
availableFilters
@ -79,6 +153,7 @@ function FilterAddGroup({
return (
availableFilters
?.filter((flt) => !activeFilterNames.includes(flt.name))
?.sort((a, b) => a.label.localeCompare(b.label))
?.map((flt) => ({
value: flt.name,
label: flt.label,
@ -133,22 +208,13 @@ function FilterAddGroup({
};
tableState.setActiveFilters([...filters, newFilter]);
// Clear selected filter
setSelectedFilter(null);
},
[selectedFilter]
);
const setDateValue = useCallback(
(value: DateValue) => {
if (value) {
const date = value.toString();
setSelectedValue(dayjs(date).format('YYYY-MM-DD'));
} else {
setSelectedValue('');
}
},
[setSelectedValue]
);
return (
<Stack gap='xs'>
<Divider />
@ -160,23 +226,13 @@ function FilterAddGroup({
onChange={(value: string | null) => setSelectedFilter(value)}
maxDropdownHeight={800}
/>
{selectedFilter &&
(filterType === 'date' ? (
<DateInput
label={t`Value`}
placeholder={t`Select date value`}
onChange={setDateValue}
{selectedFilter && (
<FilterElement
filterType={filterType}
valueOptions={valueOptions}
onValueChange={setSelectedValue}
/>
) : (
<Select
data={valueOptions}
label={t`Value`}
searchable={true}
placeholder={t`Select filter value`}
onChange={(value: string | null) => setSelectedValue(value)}
maxDropdownHeight={800}
/>
))}
)}
</Stack>
);
}

View File

@ -25,12 +25,18 @@ import {
} from '../ColumnRenderers';
import {
AssignedToMeFilter,
CompletedAfterFilter,
CompletedBeforeFilter,
CreatedAfterFilter,
CreatedBeforeFilter,
HasProjectCodeFilter,
MaxDateFilter,
MinDateFilter,
OverdueFilter,
StatusFilterOptions,
type TableFilter
type TableFilter,
TargetDateAfterFilter,
TargetDateBeforeFilter
} from '../Filter';
import { InvenTreeTable } from '../InvenTreeTable';
@ -130,6 +136,12 @@ export function BuildOrderTable({
AssignedToMeFilter(),
MinDateFilter(),
MaxDateFilter(),
CreatedBeforeFilter(),
CreatedAfterFilter(),
TargetDateBeforeFilter(),
TargetDateAfterFilter(),
CompletedBeforeFilter(),
CompletedAfterFilter(),
{
name: 'project_code',
label: t`Project Code`,

View File

@ -14,6 +14,7 @@ import { useTable } from '../../hooks/UseTable';
import { apiUrl } from '../../states/ApiState';
import { useUserState } from '../../states/UserState';
import {
CompletionDateColumn,
CreationDateColumn,
DescriptionColumn,
LineItemsProgressColumn,
@ -25,13 +26,19 @@ import {
} from '../ColumnRenderers';
import {
AssignedToMeFilter,
CompletedAfterFilter,
CompletedBeforeFilter,
CreatedAfterFilter,
CreatedBeforeFilter,
HasProjectCodeFilter,
MaxDateFilter,
MinDateFilter,
OutstandingFilter,
OverdueFilter,
StatusFilterOptions,
type TableFilter
type TableFilter,
TargetDateAfterFilter,
TargetDateBeforeFilter
} from '../Filter';
import { InvenTreeTable } from '../InvenTreeTable';
@ -64,6 +71,12 @@ export function PurchaseOrderTable({
AssignedToMeFilter(),
MinDateFilter(),
MaxDateFilter(),
CreatedBeforeFilter(),
CreatedAfterFilter(),
TargetDateBeforeFilter(),
TargetDateAfterFilter(),
CompletedBeforeFilter(),
CompletedAfterFilter(),
{
name: 'project_code',
label: t`Project Code`,
@ -108,6 +121,9 @@ export function PurchaseOrderTable({
ProjectCodeColumn({}),
CreationDateColumn({}),
TargetDateColumn({}),
CompletionDateColumn({
accessor: 'complete_date'
}),
{
accessor: 'total_price',
title: t`Total Price`,

View File

@ -14,8 +14,8 @@ import { useTable } from '../../hooks/UseTable';
import { apiUrl } from '../../states/ApiState';
import { useUserState } from '../../states/UserState';
import {
CompletionDateColumn,
CreationDateColumn,
DateColumn,
DescriptionColumn,
LineItemsProgressColumn,
ProjectCodeColumn,
@ -26,13 +26,19 @@ import {
} from '../ColumnRenderers';
import {
AssignedToMeFilter,
CompletedAfterFilter,
CompletedBeforeFilter,
CreatedAfterFilter,
CreatedBeforeFilter,
HasProjectCodeFilter,
MaxDateFilter,
MinDateFilter,
OutstandingFilter,
OverdueFilter,
StatusFilterOptions,
type TableFilter
type TableFilter,
TargetDateAfterFilter,
TargetDateBeforeFilter
} from '../Filter';
import { InvenTreeTable } from '../InvenTreeTable';
@ -62,6 +68,12 @@ export function ReturnOrderTable({
AssignedToMeFilter(),
MinDateFilter(),
MaxDateFilter(),
CreatedBeforeFilter(),
CreatedAfterFilter(),
TargetDateBeforeFilter(),
TargetDateAfterFilter(),
CompletedBeforeFilter(),
CompletedAfterFilter(),
{
name: 'project_code',
label: t`Project Code`,
@ -117,9 +129,8 @@ export function ReturnOrderTable({
ProjectCodeColumn({}),
CreationDateColumn({}),
TargetDateColumn({}),
DateColumn({
accessor: 'complete_date',
title: t`Completion Date`
CompletionDateColumn({
accessor: 'complete_date'
}),
ResponsibleColumn({}),
{

View File

@ -27,13 +27,19 @@ import {
} from '../ColumnRenderers';
import {
AssignedToMeFilter,
CompletedAfterFilter,
CompletedBeforeFilter,
CreatedAfterFilter,
CreatedBeforeFilter,
HasProjectCodeFilter,
MaxDateFilter,
MinDateFilter,
OutstandingFilter,
OverdueFilter,
StatusFilterOptions,
type TableFilter
type TableFilter,
TargetDateAfterFilter,
TargetDateBeforeFilter
} from '../Filter';
import { InvenTreeTable } from '../InvenTreeTable';
@ -63,6 +69,12 @@ export function SalesOrderTable({
AssignedToMeFilter(),
MinDateFilter(),
MaxDateFilter(),
CreatedBeforeFilter(),
CreatedAfterFilter(),
TargetDateBeforeFilter(),
TargetDateAfterFilter(),
CompletedBeforeFilter(),
CompletedAfterFilter(),
{
name: 'project_code',
label: t`Project Code`,

View File

@ -286,7 +286,11 @@ function stockItemTableColumns(): TableColumn[] {
/**
* Construct a list of available filters for the stock item table
*/
function stockItemTableFilters(): TableFilter[] {
function stockItemTableFilters({
enableExpiry
}: {
enableExpiry: boolean;
}): TableFilter[] {
return [
{
name: 'active',
@ -354,15 +358,35 @@ function stockItemTableFilters(): TableFilter[] {
label: t`Is Serialized`,
description: t`Show items which have a serial number`
},
// TODO: serial
// TODO: serial_gte
// TODO: serial_lte
{
name: 'batch',
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',
label: t`Has Batch Code`,
description: t`Show items which have a batch code`
},
// TODO: batch
{
name: 'tracked',
label: t`Tracked`,
@ -373,10 +397,56 @@ function stockItemTableFilters(): TableFilter[] {
label: t`Has Purchase Price`,
description: t`Show items which have a purchase price`
},
// TODO: Expired
// TODO: stale
// TODO: expiry_date_lte
// TODO: expiry_date_gte
{
name: 'expired',
label: t`Expired`,
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',
label: t`External Location`,
@ -397,12 +467,25 @@ export function StockItemTable({
allowAdd?: boolean;
tableName: string;
}>) {
const tableColumns = useMemo(() => stockItemTableColumns(), []);
const tableFilters = useMemo(() => stockItemTableFilters(), []);
const table = useTable(tableName);
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(() => {
return {
items: table.selectedRecords,

View 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();
};

View File

@ -1,8 +1,13 @@
import { test } from '../baseFixtures.ts';
import { baseUrl } from '../defaults.ts';
import {
clickButtonIfVisible,
getRowFromCell,
openFilterDrawer
} from '../helpers.ts';
import { doQuickLogin } from '../login.ts';
test('Pages - Build Order', async ({ page }) => {
test('Build Order - Basic Tests', async ({ page }) => {
await doQuickLogin(page);
await page.goto(`${baseUrl}/part/`);
@ -82,7 +87,7 @@ test('Pages - Build Order', async ({ page }) => {
.waitFor();
});
test('Pages - Build Order - Build Outputs', async ({ page }) => {
test('Build Order - Build Outputs', async ({ page }) => {
await doQuickLogin(page);
await page.goto(`${baseUrl}/part/`);
@ -140,7 +145,7 @@ test('Pages - Build Order - Build Outputs', async ({ page }) => {
// Cancel one of the newly created outputs
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 page.getByRole('menuitem', { name: 'Cancel' }).click();
await page.getByRole('button', { name: 'Submit' }).click();
@ -148,7 +153,7 @@ test('Pages - Build Order - Build Outputs', async ({ page }) => {
// Complete the other output
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 page.getByRole('menuitem', { name: 'Complete' }).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();
});
test('Pages - Build Order - Allocation', async ({ page }) => {
test('Build Order - Allocation', async ({ page }) => {
await doQuickLogin(page);
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
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();
@ -237,7 +242,7 @@ test('Pages - Build Order - Allocation', async ({ page }) => {
const item = data[idx];
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}`;
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 })
.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);
});

View File

@ -2,7 +2,7 @@ import { test } from '../baseFixtures.js';
import { doQuickLogin } from '../login.js';
import { setPluginState } from '../settings.js';
test('Pages - Dashboard - Basic', async ({ page }) => {
test('Dashboard - Basic', async ({ page }) => {
await doQuickLogin(page);
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();
});
test('Pages - Dashboard - Plugins', async ({ page, request }) => {
test('Dashboard - Plugins', async ({ page, request }) => {
// Ensure that the "SampleUI" plugin is enabled
await setPluginState({
request,

View File

@ -1,5 +1,6 @@
import { test } from '../baseFixtures';
import { baseUrl } from '../defaults';
import { getRowFromCell } from '../helpers';
import { doQuickLogin } from '../login';
/**
@ -129,9 +130,7 @@ test('Parts - Allocations', async ({ page }) => {
// Check "progress" bar of BO0001
const build_order_cell = await page.getByRole('cell', { name: 'BO0001' });
const build_order_row = await build_order_cell
.locator('xpath=ancestor::tr')
.first();
const build_order_row = await getRowFromCell(build_order_cell);
await build_order_row.getByText('11 / 75').waitFor();
// Expand allocations against BO0001
@ -147,9 +146,7 @@ test('Parts - Allocations', async ({ page }) => {
// Check "progress" bar of SO0025
const sales_order_cell = await page.getByRole('cell', { name: 'SO0025' });
const sales_order_row = await sales_order_cell
.locator('xpath=ancestor::tr')
.first();
const sales_order_row = await getRowFromCell(sales_order_cell);
await sales_order_row.getByText('3 / 10').waitFor();
// Expand allocations against SO0025

View File

@ -1,4 +1,5 @@
import { test } from '../baseFixtures.ts';
import { clickButtonIfVisible, openFilterDrawer } from '../helpers.ts';
import { doQuickLogin } from '../login.ts';
test('Purchase Orders - General', async ({ page }) => {
@ -51,6 +52,30 @@ test('Purchase Orders - General', async ({ page }) => {
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
*/

View File

@ -1,8 +1,9 @@
import { test } from '../baseFixtures.js';
import { baseUrl } from '../defaults.js';
import { clickButtonIfVisible, openFilterDrawer } from '../helpers.js';
import { doQuickLogin } from '../login.js';
test('Stock', async ({ page }) => {
test('Stock - Basic Tests', async ({ page }) => {
await doQuickLogin(page);
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();
});
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 }) => {
await doQuickLogin(page);

View File

@ -1,23 +1,23 @@
import { test } from './baseFixtures.js';
import { baseUrl } from './defaults.js';
import {
clearTableFilters,
closeFilterDrawer,
openFilterDrawer
} from './helpers.js';
import { doQuickLogin } from './login.js';
// Helper function to set the value of a specific table filter
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.getByPlaceholder('Select filter').click();
await page.getByRole('option', { name: name, exact: true }).click();
await page.getByPlaceholder('Select filter value').click();
await page.getByRole('option', { name: value, exact: true }).click();
await page.getByLabel('filter-drawer-close').click();
};
// Helper function to clear table filters
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();
await closeFilterDrawer(page);
};
test('Tables - Filters', async ({ page }) => {
@ -30,14 +30,14 @@ test('Tables - Filters', async ({ page }) => {
await setFilter(page, 'Responsible', 'allaccess');
await setFilter(page, 'Project Code', 'PRJ-NIM');
await clearFilters(page);
await clearTableFilters(page);
// Head to the "part list" page
await page.goto(`${baseUrl}/part/category/index/parts/`);
await setFilter(page, 'Assembly', 'Yes');
await clearFilters(page);
await clearTableFilters(page);
// Head to the "purchase order list" page
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, 'Project Code', 'PRO-ZEN');
await clearFilters(page);
await clearTableFilters(page);
});
test('Tables - Columns', async ({ page }) => {