2
0
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:
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 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

View File

@ -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."""

View File

@ -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',
] ]

View File

@ -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'
) )

View File

@ -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')

View File

@ -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" %}',
}, },

View File

@ -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}

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 { export function ShipmentDateColumn(props: TableColumnProps): TableColumn {
return DateColumn({ return DateColumn({
accessor: 'shipment_date', accessor: 'shipment_date',

View File

@ -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',

View File

@ -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>
); );
} }

View File

@ -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`,

View File

@ -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`,

View File

@ -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({}),
{ {

View File

@ -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`,

View File

@ -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,

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 { 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);
});

View File

@ -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,

View File

@ -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

View File

@ -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
*/ */

View File

@ -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);

View File

@ -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 }) => {