From 809a978f7df74ab39b8555679a60b0159f2bfdfc Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 25 Nov 2024 21:36:31 +1100 Subject: [PATCH] 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 --- .../InvenTree/InvenTree/api_version.py | 9 +- src/backend/InvenTree/build/api.py | 32 ++++- src/backend/InvenTree/order/api.py | 101 +++++++++++----- src/backend/InvenTree/part/api.py | 4 +- src/backend/InvenTree/stock/api.py | 21 +++- .../templates/js/translated/table_filters.js | 4 +- .../src/pages/stock/LocationDetail.tsx | 8 -- src/frontend/src/tables/ColumnRenderers.tsx | 8 ++ src/frontend/src/tables/Filter.tsx | 57 ++++++++- .../src/tables/FilterSelectDrawer.tsx | 114 +++++++++++++----- .../src/tables/build/BuildOrderTable.tsx | 14 ++- .../tables/purchasing/PurchaseOrderTable.tsx | 18 ++- .../src/tables/sales/ReturnOrderTable.tsx | 21 +++- .../src/tables/sales/SalesOrderTable.tsx | 14 ++- .../src/tables/stock/StockItemTable.tsx | 107 ++++++++++++++-- src/frontend/tests/helpers.ts | 46 +++++++ src/frontend/tests/pages/pui_build.spec.ts | 30 +++-- .../tests/pages/pui_dashboard.spec.ts | 4 +- src/frontend/tests/pages/pui_part.spec.ts | 9 +- .../tests/pages/pui_purchase_order.spec.ts | 25 ++++ src/frontend/tests/pages/pui_stock.spec.ts | 42 ++++++- src/frontend/tests/pui_tables.spec.ts | 22 ++-- 22 files changed, 586 insertions(+), 124 deletions(-) create mode 100644 src/frontend/tests/helpers.ts diff --git a/src/backend/InvenTree/InvenTree/api_version.py b/src/backend/InvenTree/InvenTree/api_version.py index 76907a626a..df82fc361e 100644 --- a/src/backend/InvenTree/InvenTree/api_version.py +++ b/src/backend/InvenTree/InvenTree/api_version.py @@ -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 diff --git a/src/backend/InvenTree/build/api.py b/src/backend/InvenTree/build/api.py index 096a42683e..0eeb17ed61 100644 --- a/src/backend/InvenTree/build/api.py +++ b/src/backend/InvenTree/build/api.py @@ -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.""" diff --git a/src/backend/InvenTree/order/api.py b/src/backend/InvenTree/order/api.py index b5671c5952..dbbe60c0b8 100644 --- a/src/backend/InvenTree/order/api.py +++ b/src/backend/InvenTree/order/api.py @@ -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', ] diff --git a/src/backend/InvenTree/part/api.py b/src/backend/InvenTree/part/api.py index 36663a7847..4f62be6d1d 100644 --- a/src/backend/InvenTree/part/api.py +++ b/src/backend/InvenTree/part/api.py @@ -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' ) diff --git a/src/backend/InvenTree/stock/api.py b/src/backend/InvenTree/stock/api.py index 7734e101ab..42bc628d96 100644 --- a/src/backend/InvenTree/stock/api.py +++ b/src/backend/InvenTree/stock/api.py @@ -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') diff --git a/src/backend/InvenTree/templates/js/translated/table_filters.js b/src/backend/InvenTree/templates/js/translated/table_filters.js index 4706b493b5..a74ceaee30 100644 --- a/src/backend/InvenTree/templates/js/translated/table_filters.js +++ b/src/backend/InvenTree/templates/js/translated/table_filters.js @@ -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" %}', }, diff --git a/src/frontend/src/pages/stock/LocationDetail.tsx b/src/frontend/src/pages/stock/LocationDetail.tsx index 9aaf256c2a..739ad8ba1f 100644 --- a/src/frontend/src/pages/stock/LocationDetail.tsx +++ b/src/frontend/src/pages/stock/LocationDetail.tsx @@ -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() { () => [ , , - } - onClick={notYetImplemented} - variant='outline' - size='lg' - />, location.pk ? ( 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(''); + + switch (filterType) { + case 'text': + return ( + onValueChange(textValue)} + > + + + } + onChange={(e) => setTextValue(e.currentTarget.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + onValueChange(textValue); + } + }} + /> + ); + case 'date': + return ( + + ); + case 'choice': + case 'boolean': + default: + return ( + setSelectedValue(value)} - maxDropdownHeight={800} - /> - ))} + {selectedFilter && ( + + )} ); } diff --git a/src/frontend/src/tables/build/BuildOrderTable.tsx b/src/frontend/src/tables/build/BuildOrderTable.tsx index 6e3d25ba87..01531866f0 100644 --- a/src/frontend/src/tables/build/BuildOrderTable.tsx +++ b/src/frontend/src/tables/build/BuildOrderTable.tsx @@ -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`, diff --git a/src/frontend/src/tables/purchasing/PurchaseOrderTable.tsx b/src/frontend/src/tables/purchasing/PurchaseOrderTable.tsx index 0f33d6e571..72c763b35d 100644 --- a/src/frontend/src/tables/purchasing/PurchaseOrderTable.tsx +++ b/src/frontend/src/tables/purchasing/PurchaseOrderTable.tsx @@ -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`, diff --git a/src/frontend/src/tables/sales/ReturnOrderTable.tsx b/src/frontend/src/tables/sales/ReturnOrderTable.tsx index 8c62811ba9..d086b7ad6f 100644 --- a/src/frontend/src/tables/sales/ReturnOrderTable.tsx +++ b/src/frontend/src/tables/sales/ReturnOrderTable.tsx @@ -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({}), { diff --git a/src/frontend/src/tables/sales/SalesOrderTable.tsx b/src/frontend/src/tables/sales/SalesOrderTable.tsx index 220eadb9a9..265ec9a214 100644 --- a/src/frontend/src/tables/sales/SalesOrderTable.tsx +++ b/src/frontend/src/tables/sales/SalesOrderTable.tsx @@ -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`, diff --git a/src/frontend/src/tables/stock/StockItemTable.tsx b/src/frontend/src/tables/stock/StockItemTable.tsx index 276978cac0..62c0ff115e 100644 --- a/src/frontend/src/tables/stock/StockItemTable.tsx +++ b/src/frontend/src/tables/stock/StockItemTable.tsx @@ -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, diff --git a/src/frontend/tests/helpers.ts b/src/frontend/tests/helpers.ts new file mode 100644 index 0000000000..e352881fe9 --- /dev/null +++ b/src/frontend/tests/helpers.ts @@ -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(); +}; diff --git a/src/frontend/tests/pages/pui_build.spec.ts b/src/frontend/tests/pages/pui_build.spec.ts index 6d3126c910..ad2e7e38a6 100644 --- a/src/frontend/tests/pages/pui_build.spec.ts +++ b/src/frontend/tests/pages/pui_build.spec.ts @@ -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); +}); diff --git a/src/frontend/tests/pages/pui_dashboard.spec.ts b/src/frontend/tests/pages/pui_dashboard.spec.ts index 3dddb14b48..3b0e376bf7 100644 --- a/src/frontend/tests/pages/pui_dashboard.spec.ts +++ b/src/frontend/tests/pages/pui_dashboard.spec.ts @@ -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, diff --git a/src/frontend/tests/pages/pui_part.spec.ts b/src/frontend/tests/pages/pui_part.spec.ts index 9629e43ec8..9f19892f03 100644 --- a/src/frontend/tests/pages/pui_part.spec.ts +++ b/src/frontend/tests/pages/pui_part.spec.ts @@ -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 diff --git a/src/frontend/tests/pages/pui_purchase_order.spec.ts b/src/frontend/tests/pages/pui_purchase_order.spec.ts index 2b94c80ac9..ff8207346d 100644 --- a/src/frontend/tests/pages/pui_purchase_order.spec.ts +++ b/src/frontend/tests/pages/pui_purchase_order.spec.ts @@ -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 */ diff --git a/src/frontend/tests/pages/pui_stock.spec.ts b/src/frontend/tests/pages/pui_stock.spec.ts index df72be3887..3737250b87 100644 --- a/src/frontend/tests/pages/pui_stock.spec.ts +++ b/src/frontend/tests/pages/pui_stock.spec.ts @@ -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); diff --git a/src/frontend/tests/pui_tables.spec.ts b/src/frontend/tests/pui_tables.spec.ts index 7c5bbaa41a..015023c443 100644 --- a/src/frontend/tests/pui_tables.spec.ts +++ b/src/frontend/tests/pui_tables.spec.ts @@ -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 }) => {