2
0
mirror of https://github.com/inventree/InvenTree.git synced 2026-02-25 16:17:58 +00:00

[API] Enhanced filtering for generic parameters (#11383)

* Enhanced filtering for parameter templates

- Allow filtering by model ID as well as model type

* Enhanced filtering for ParameterTemplate

- Required for the parameteric data tables
- Enable filtering by base model ID
- Enable filtering by related model ID

* Bump API version

* Remove outdated comments

* Fix typo

* Remove debug statement

* Added unit tests

* Playwright tests

* Fix unit test

* Bump requirements

* Revert
This commit is contained in:
Oliver
2026-02-20 09:32:33 +11:00
committed by GitHub
parent fc1bfe876c
commit dd423dccd6
12 changed files with 524 additions and 88 deletions

View File

@@ -1,11 +1,16 @@
"""InvenTree API version information.""" """InvenTree API version information."""
# InvenTree API version # InvenTree API version
INVENTREE_API_VERSION = 454 INVENTREE_API_VERSION = 455
"""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 = """
v455 -> 2026-02-19 : https://github.com/inventree/InvenTree/pull/11383
- Adds "exists_for_model_id" filter to ParameterTemplate API endpoint
- Adds "exists_for_related_model" filter to ParameterTemplate API endpoint
- Adds "exists_for_related_model_id" filter to ParameterTemplate API endpoint
v454 -> 2026-02-19 : https://github.com/inventree/InvenTree/pull/11379 v454 -> 2026-02-19 : https://github.com/inventree/InvenTree/pull/11379
- Adds "purchase_price" ordering option to StockItem API endpoint - Adds "purchase_price" ordering option to StockItem API endpoint

View File

@@ -5,7 +5,7 @@ import json.decoder
from django.conf import settings from django.conf import settings
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError from django.core.exceptions import FieldDoesNotExist, ValidationError
from django.db.models import Q from django.db.models import Q
from django.http import JsonResponse from django.http import JsonResponse
from django.http.response import HttpResponse from django.http.response import HttpResponse
@@ -35,6 +35,7 @@ import common.filters
import common.models import common.models
import common.serializers import common.serializers
import InvenTree.conversion import InvenTree.conversion
import InvenTree.models
import InvenTree.ready import InvenTree.ready
from common.icons import get_icon_packs from common.icons import get_icon_packs
from common.settings import get_global_setting from common.settings import get_global_setting
@@ -839,7 +840,13 @@ class ParameterTemplateFilter(FilterSet):
content_type = common.filters.determine_content_type(value) content_type = common.filters.determine_content_type(value)
if not content_type: if not content_type:
return queryset.none() raise ValidationError({
'exists_for_model': 'Invalid model type provided - unable to determine content type'
})
# If the 'filter_exists_for_model_id' filter is applied, defer to that
if self.request.query_params.get('exists_for_model_id', None):
return queryset
queryset = queryset.prefetch_related('parameters') queryset = queryset.prefetch_related('parameters')
@@ -853,6 +860,160 @@ class ParameterTemplateFilter(FilterSet):
# Return only those ParameterTemplates which have at least one Parameter for the given model type # Return only those ParameterTemplates which have at least one Parameter for the given model type
return queryset.filter(parameter_count__gt=0) return queryset.filter(parameter_count__gt=0)
exists_for_model_id = rest_filters.NumberFilter(
method='filter_exists_for_model_id', label='Exists For Model ID'
)
def filter_exists_for_model_id(self, queryset, name, value):
"""Filter queryset to include only ParameterTemplates which have at least one Parameter for the given model type and model id.
Notes:
- This filter can only be applied if the 'exists_for_model' filter is also applied, as the model_id is only meaningful in the context of a particular model type.
Reference: https://github.com/inventree/InvenTree/issues/11381
"""
exists_for_model = self.request.query_params.get('exists_for_model', None)
if not exists_for_model:
raise ValidationError({
'exists_for_model': 'Invalid model type provided - unable to determine content type'
})
content_type = common.filters.determine_content_type(exists_for_model)
if not content_type:
raise ValidationError({
'exists_for_model': 'Invalid model type provided - unable to determine content type'
})
model_class = content_type.model_class()
# Try to find the model instance
try:
instance = model_class.objects.get(pk=value)
except (model_class.DoesNotExist, ValueError):
# If the model instance does not exist, then we can return an empty queryset
raise ValidationError({
'exists_for_model_id': 'Invalid model id provided - no such instance for the given model type'
})
# If the provided model is a "tree" structure, then we should also include any child objects in the filter
if isinstance(instance, InvenTree.models.InvenTreeTree):
id_values = list(
instance.get_descendants(include_self=True).values_list('pk', flat=True)
)
else:
id_values = [instance.pk]
# Now, filter against model type and model id
queryset = queryset.prefetch_related('parameters')
filters = {'model_type': content_type, 'model_id__in': id_values}
# Annotate the queryset to determine which ParameterTemplates have at least one Parameter defined
queryset = queryset.annotate(
parameter_count=SubqueryCount('parameters', filter=Q(**filters))
)
return queryset.filter(parameter_count__gt=0)
exists_for_related_model = rest_filters.CharFilter(
method='filter_exists_for_related_model', label='Exists For Related Model'
)
def filter_exists_for_related_model(self, queryset, name, value):
"""Filter applied to map parameter templates to a particular model relation against the target model.
For instance, specify 'category' to filter part parameters which exist for any part in that category.
Note:
- This filter has no effect on its own
- It requires the 'exists_for_model' filter to be applied (to specify the base model)
- It requires the 'exists_for_related_model_id' filter to be applied also (to specify the related model id)
"""
return queryset
exists_for_related_model_id = rest_filters.NumberFilter(
method='filter_exists_for_related_model_id', label='Exists For Model ID'
)
def filter_exists_for_related_model_id(self, queryset, name, value):
"""Filter queryset to include only ParameterTemplates which have at least one Parameter for the given related model type and model id.
Notes:
- This filter can only be applied if the 'exists_for_model' filter is also applied, as the model_id is only meaningful in the context of a base model
- This filter can only be applied if the 'exists_for_related_model' filter is also applied, as the related model id is only meaningful in the context of a particular model relation
Example: To filter part parameters which have at least one parameter defined for any part in category 5, you could apply the following filters:
- exists_for_model=part
- exists_for_related_model=category
- exists_for_related_model_id=5
"""
model = self.request.query_params.get('exists_for_model', None)
related_model = self.request.query_params.get('exists_for_related_model', None)
if not model or not related_model:
raise ValidationError({
'exists_for_model': 'Invalid model type provided - unable to determine content type'
})
# Determine content type for the base model, to ensure they are valid
model_type = common.filters.determine_content_type(model)
if not model_type:
return queryset.none()
# Determine the model class for the 'related' model
try:
related_model_field = model_type.model_class()._meta.get_field(
related_model
)
except FieldDoesNotExist:
raise ValidationError({
'exists_for_related_model': 'Invalid related model - no such field on the base model'
})
if related_model_field := model_type.model_class()._meta.get_field(
related_model
):
related_model_class = related_model_field.related_model
else:
# Return an empty queryset if the provided related model is invalid
return queryset.none()
# Find all instances of the related model which match the provided related model id
try:
related_instance = related_model_class.objects.get(pk=value)
except (related_model_class.DoesNotExist, ValueError):
return queryset.none()
# Account for potential tree structure in the related model
if isinstance(related_instance, InvenTree.models.InvenTreeTree):
related_instances = list(
related_instance.get_descendants(include_self=True).values_list(
'pk', flat=True
)
)
else:
related_instances = [related_instance.pk]
# Next, find all instances of the base model which are related to the related model instances
model_instances = model_type.model_class().objects.filter(**{
f'{related_model}__in': related_instances
})
model_instance_ids = list(model_instances.values_list('pk', flat=True))
# Now, filter against model type and model id
queryset = queryset.prefetch_related('parameters')
filters = {'model_type': model_type, 'model_id__in': model_instance_ids}
# Annotate the queryset to determine which ParameterTemplates have at least one Parameter defined
queryset = queryset.annotate(
parameter_count=SubqueryCount('parameters', filter=Q(**filters))
)
return queryset.filter(parameter_count__gt=0)
class ParameterTemplateMixin: class ParameterTemplateMixin:
"""Mixin class for ParameterTemplate views.""" """Mixin class for ParameterTemplate views."""

View File

@@ -237,6 +237,217 @@ class ParameterAPITests(InvenTreeAPITestCase):
f'Incorrect number of templates for model "{model_name}"', f'Incorrect number of templates for model "{model_name}"',
) )
def test_template_extended_filters(self):
"""Unit testing for more complex filters on the ParameterTemplate endpoint.
Ref: https://github.com/inventree/InvenTree/pull/11383
In these tests we will filter by complex model relations.
"""
from part.models import Part, PartCategory
# Create some part categories
cat_mech = PartCategory.objects.create(
name='Mechanical', description='Mechanical components'
)
cat_elec = PartCategory.objects.create(
name='Electronics', description='Electronic components'
)
cat_pass = PartCategory.objects.create(
name='Passive', description='Passive electronic components', parent=cat_elec
)
cat_res = PartCategory.objects.create(
name='Resistors', description='Resistor components', parent=cat_pass
)
cat_cap = PartCategory.objects.create(
name='Capacitors', description='Capacitor components', parent=cat_pass
)
# Create some parts
capacitors = [
Part.objects.create(
name=f'Capacitor {ii}', description='A capacitor', category=cat_cap
)
for ii in range(5)
]
resistors = [
Part.objects.create(
name=f'Resistor {ii}', description='A resistor', category=cat_res
)
for ii in range(5)
]
# Create some ParameterTemplates which relate to the category of the part
resistance = common.models.ParameterTemplate.objects.create(
name='Resistance', description='The resistance of a part', units='Ohms'
)
capacitance = common.models.ParameterTemplate.objects.create(
name='Capacitance', description='The capacitance of a part', units='Farads'
)
tolerance = common.models.ParameterTemplate.objects.create(
name='Tolerance', description='The tolerance of a part', units='%'
)
for idx, resistor in enumerate(resistors):
common.models.Parameter.objects.create(
template=resistance,
model_type=resistor.get_content_type(),
model_id=resistor.pk,
data=f'{10 * (idx + 1)}k',
)
common.models.Parameter.objects.create(
template=tolerance,
model_type=resistor.get_content_type(),
model_id=resistor.pk,
data=f'{idx + 1}%',
)
for idx, capacitor in enumerate(capacitors):
common.models.Parameter.objects.create(
template=capacitance,
model_type=capacitor.get_content_type(),
model_id=capacitor.pk,
data=f'{10 * (idx + 1)}uF',
)
common.models.Parameter.objects.create(
template=tolerance,
model_type=capacitor.get_content_type(),
model_id=capacitor.pk,
data=f'{5 * (idx + 1)}%',
)
# Ensure that we have the expected number of templates and parameters created for testing
self.assertEqual(common.models.ParameterTemplate.objects.count(), 3)
self.assertEqual(common.models.Parameter.objects.count(), 20)
# Now, we have some data - let's apply some filtering
url = reverse('api-parameter-template-list')
# Return *all* results, without filters
response = self.get(url)
self.assertEqual(len(response.data), 3)
# Filter by 'exists_for_model'
for model_name, count in {
'part.part': 3,
'part': 3,
'company': 0,
'build': 0,
}.items():
response = self.get(url, data={'exists_for_model': model_name})
n = len(response.data)
self.assertEqual(
n,
count,
f'Incorrect number of templates ({n}) for model "{model_name}"',
)
# Filter by 'exists_for_model' and 'exists_for_model_id'
res = resistors[0]
response = self.get(
url, data={'exists_for_model': 'part.part', 'exists_for_model_id': res.pk}
)
self.assertEqual(len(response.data), 2)
pk_list = [t['pk'] for t in response.data]
self.assertIn(resistance.pk, pk_list)
self.assertIn(tolerance.pk, pk_list)
cap = capacitors[0]
response = self.get(
url, data={'exists_for_model': 'part.part', 'exists_for_model_id': cap.pk}
)
self.assertEqual(len(response.data), 2)
pk_list = [t['pk'] for t in response.data]
self.assertIn(capacitance.pk, pk_list)
self.assertIn(tolerance.pk, pk_list)
# Filter by 'exists_for_related_model' (test the "capacitor" relationship)
# Check the 'capacitor' category
response = self.get(
url,
data={
'exists_for_model': 'part.part',
'exists_for_related_model': 'category',
'exists_for_related_model_id': cat_cap.pk,
},
)
self.assertEqual(len(response.data), 2)
pk_list = [t['pk'] for t in response.data]
self.assertIn(capacitance.pk, pk_list)
self.assertIn(tolerance.pk, pk_list)
# Check the 'electronics' category - this should return all parameters
response = self.get(
url,
data={
'exists_for_model': 'part.part',
'exists_for_related_model': 'category',
'exists_for_related_model_id': cat_elec.pk,
},
)
self.assertEqual(len(response.data), 3)
pk_list = [t['pk'] for t in response.data]
self.assertIn(resistance.pk, pk_list)
self.assertIn(capacitance.pk, pk_list)
self.assertIn(tolerance.pk, pk_list)
# Check the 'mechanical' category - this should return no parameters
response = self.get(
url,
data={
'exists_for_model': 'part.part',
'exists_for_related_model': 'category',
'exists_for_related_model_id': cat_mech.pk,
},
)
self.assertEqual(len(response.data), 0)
def test_invalid_filters(self):
"""Test error messages for invalid filter combinations."""
url = reverse('api-parameter-template-list')
# Invalid 'exists_for_model' value
response = self.get(
url, {'exists_for_model': 'asdf---invalid---model'}, expected_code=400
)
self.assertIn(
'Invalid model type provided', str(response.data['exists_for_model'])
)
# Invalid 'exists_for_model_id' value
for model_id in ['not_an_integer', -1, 9999]:
response = self.get(
url,
{'exists_for_model': 'part.part', 'exists_for_model_id': model_id},
expected_code=400,
)
# Invalid 'exists_for_related_model' value
response = self.get(
url,
{
'exists_for_model': 'part',
'exists_for_related_model': 'invalid_field',
'exists_for_related_model_id': 1,
},
expected_code=400,
)
self.assertIn(
'no such field on the base model',
str(response.data['exists_for_related_model']),
)
def test_parameter_api(self): def test_parameter_api(self):
"""Test Parameter API functionality.""" """Test Parameter API functionality."""
# Create a simple part to test with # Create a simple part to test with

View File

@@ -1659,9 +1659,9 @@ pynacl==1.6.2 \
# via # via
# -c src/backend/requirements.txt # -c src/backend/requirements.txt
# paramiko # paramiko
pypdf==6.7.1 \ pypdf==6.6.2 \
--hash=sha256:6b7a63be5563a0a35d54c6d6b550d75c00b8ccf36384be96365355e296e6b3b0 \ --hash=sha256:0a3ea3b3303982333404e22d8f75d7b3144f9cf4b2970b96856391a516f9f016 \
--hash=sha256:a02ccbb06463f7c334ce1612e91b3e68a8e827f3cee100b9941771e6066b094e --hash=sha256:44c0c9811cfb3b83b28f1c3d054531d5b8b81abaedee0d8cb403650d023832ba
# via # via
# -c src/backend/requirements.txt # -c src/backend/requirements.txt
# -r src/backend/requirements.in # -r src/backend/requirements.in

View File

@@ -101,12 +101,18 @@ function ParameterCell({
*/ */
export default function ParametricDataTable({ export default function ParametricDataTable({
modelType, modelType,
modelId,
relatedModel,
relatedModelId,
endpoint, endpoint,
queryParams, queryParams,
customFilters, customFilters,
customColumns customColumns
}: { }: {
modelType: ModelType; modelType: ModelType;
modelId?: number;
relatedModel?: string;
relatedModelId?: number;
endpoint: ApiEndpoints | string; endpoint: ApiEndpoints | string;
queryParams?: Record<string, any>; queryParams?: Record<string, any>;
customFilters?: TableFilter[]; customFilters?: TableFilter[];
@@ -125,8 +131,12 @@ export default function ParametricDataTable({
.get(apiUrl(ApiEndpoints.parameter_template_list), { .get(apiUrl(ApiEndpoints.parameter_template_list), {
params: { params: {
active: true, active: true,
ordering: 'name',
for_model: modelType, for_model: modelType,
exists_for_model: modelType exists_for_model: modelType,
exists_for_model_id: modelId ?? undefined,
exists_for_related_model: relatedModel ?? undefined,
exists_for_related_model_id: relatedModelId ?? undefined
} }
}) })
.then((response) => response.data); .then((response) => response.data);

View File

@@ -56,6 +56,8 @@ export default function ParametricPartTable({
return ( return (
<ParametricDataTable <ParametricDataTable
modelType={ModelType.part} modelType={ModelType.part}
relatedModel={'category'}
relatedModelId={categoryId}
endpoint={ApiEndpoints.part_list} endpoint={ApiEndpoints.part_list}
customColumns={customColumns} customColumns={customColumns}
customFilters={customFilters} customFilters={customFilters}

View File

@@ -180,3 +180,33 @@ export const toggleColumnSorting = async (page: Page, columnName: string) => {
await page.waitForTimeout(50); await page.waitForTimeout(50);
await page.waitForLoadState('networkidle'); await page.waitForLoadState('networkidle');
}; };
// Display the 'table' view
export const showTableView = async (page: Page) => {
await page
.getByRole('button', { name: 'segmented-icon-control-table' })
.click();
await page.waitForLoadState('networkidle');
};
// Display the 'parameteric' view
export const showParametricView = async (page: Page) => {
await page
.getByRole('button', { name: 'segmented-icon-control-parametric' })
.click();
await page.waitForLoadState('networkidle');
};
// Display the 'calendar' view
export const showCalendarView = async (page: Page) => {
await page
.getByRole('button', { name: 'segmented-icon-control-calendar' })
.click();
await page.waitForLoadState('networkidle');
};
// Check for an expected number of columns in the visible table
export const expectTableColumnCount = async (page: Page, count: number) => {
const columns = page.locator('table thead tr th');
await expect(columns).toHaveCount(count);
};

View File

@@ -7,7 +7,10 @@ import {
getRowFromCell, getRowFromCell,
loadTab, loadTab,
navigate, navigate,
setTableChoiceFilter setTableChoiceFilter,
showCalendarView,
showParametricView,
showTableView
} from '../helpers.ts'; } from '../helpers.ts';
import { doCachedLogin } from '../login.ts'; import { doCachedLogin } from '../login.ts';
@@ -17,18 +20,10 @@ test('Build - Index', async ({ browser }) => {
await loadTab(page, 'Build Orders'); await loadTab(page, 'Build Orders');
// Ensure all data views are available // Ensure all data views are available
await page await showParametricView(page);
.getByRole('button', { name: 'segmented-icon-control-parametric' }) await showCalendarView(page);
.click();
await page
.getByRole('button', { name: 'segmented-icon-control-calendar' })
.click();
await page.getByRole('button', { name: 'action-button-next-month' }).click(); await page.getByRole('button', { name: 'action-button-next-month' }).click();
await showTableView(page);
await page
.getByRole('button', { name: 'segmented-icon-control-table' })
.click();
}); });
test('Build Order - Basic Tests', async ({ browser }) => { test('Build Order - Basic Tests', async ({ browser }) => {

View File

@@ -1,5 +1,10 @@
import { test } from '../baseFixtures.js'; import { test } from '../baseFixtures.js';
import { clickOnParamFilter, loadTab, navigate } from '../helpers.js'; import {
clickOnParamFilter,
loadTab,
navigate,
showParametricView
} from '../helpers.js';
import { doCachedLogin } from '../login.js'; import { doCachedLogin } from '../login.js';
test('Company', async ({ browser }) => { test('Company', async ({ browser }) => {
@@ -49,9 +54,7 @@ test('Company - Parameters', async ({ browser }) => {
}); });
// Show parametric view // Show parametric view
await page await showParametricView(page);
.getByRole('button', { name: 'segmented-icon-control-parametric' })
.click();
// Filter by "payment terms" parameter value // Filter by "payment terms" parameter value
await clickOnParamFilter(page, 'Payment Terms'); await clickOnParamFilter(page, 'Payment Terms');

View File

@@ -4,10 +4,13 @@ import {
clickOnParamFilter, clickOnParamFilter,
clickOnRowMenu, clickOnRowMenu,
deletePart, deletePart,
expectTableColumnCount,
getRowFromCell, getRowFromCell,
loadTab, loadTab,
navigate, navigate,
setTableChoiceFilter setTableChoiceFilter,
showParametricView,
showTableView
} from '../helpers'; } from '../helpers';
import { doCachedLogin } from '../login'; import { doCachedLogin } from '../login';
import { setPluginState, setSettingState } from '../settings'; import { setPluginState, setSettingState } from '../settings';
@@ -499,6 +502,58 @@ test('Parts - Attachments', async ({ browser }) => {
await page.getByRole('button', { name: 'Cancel' }).click(); await page.getByRole('button', { name: 'Cancel' }).click();
}); });
test('Parts - Parameters by Category', async ({ browser }) => {
const page = await doCachedLogin(browser, { url: 'part/category/4/parts' });
await showParametricView(page);
// Check for expected parameter columns
for (const col of [
'Total Stock',
'Capacitance [F]',
'Power [W]',
'Resistance [ohms]'
]) {
await page.getByRole('button', { name: col }).waitFor();
}
await expectTableColumnCount(page, 9);
// Now let's go to the "resistors" category
await navigate(page, 'part/category/5/parts');
await showParametricView(page);
// Fewer parameter templates displayed here
await expectTableColumnCount(page, 7);
// Check for expected parameter columns
for (const col of [
'Total Stock',
'Tolerance [percent]',
'Power [W]',
'Resistance [ohms]'
]) {
await page.getByRole('button', { name: col }).waitFor();
}
// Finally, let's go to the "capacitors" category, which has a different set of parameter templates
await navigate(page, 'part/category/6/parts');
await showParametricView(page);
await expectTableColumnCount(page, 7);
for (const col of [
'Total Stock',
'Tolerance [percent]',
'Polarized',
'Capacitance [F]'
]) {
await page.getByRole('button', { name: col }).waitFor();
}
// Reset to the table view
await showTableView(page);
});
test('Parts - Parameters', async ({ browser }) => { test('Parts - Parameters', async ({ browser }) => {
const page = await doCachedLogin(browser, { url: 'part/69/parameters' }); const page = await doCachedLogin(browser, { url: 'part/69/parameters' });
@@ -562,10 +617,8 @@ test('Parts - Parameter Filtering', async ({ browser }) => {
const page = await doCachedLogin(browser, { url: 'part/' }); const page = await doCachedLogin(browser, { url: 'part/' });
await loadTab(page, 'Parts', true); await loadTab(page, 'Parts', true);
await page
.getByRole('button', { name: 'segmented-icon-control-parametric' })
.click();
await showParametricView(page);
await clearTableFilters(page); await clearTableFilters(page);
// All parts should be available (no filters applied) // All parts should be available (no filters applied)

View File

@@ -9,7 +9,10 @@ import {
loadTab, loadTab,
navigate, navigate,
openFilterDrawer, openFilterDrawer,
setTableChoiceFilter setTableChoiceFilter,
showCalendarView,
showParametricView,
showTableView
} from '../helpers.ts'; } from '../helpers.ts';
import { doCachedLogin } from '../login.ts'; import { doCachedLogin } from '../login.ts';
@@ -18,26 +21,14 @@ test('Purchasing - Index', async ({ browser }) => {
// Purchase Orders tab // Purchase Orders tab
await loadTab(page, 'Purchase Orders'); await loadTab(page, 'Purchase Orders');
await page await showParametricView(page);
.getByRole('button', { name: 'segmented-icon-control-parametric' }) await showCalendarView(page);
.click(); await showTableView(page);
await page
.getByRole('button', { name: 'segmented-icon-control-calendar' })
.click();
await page.getByRole('button', { name: 'calendar-select-month' }).waitFor();
await page
.getByRole('button', { name: 'segmented-icon-control-table' })
.click();
// Suppliers tab // Suppliers tab
await loadTab(page, 'Suppliers'); await loadTab(page, 'Suppliers');
await page await showParametricView(page);
.getByRole('button', { name: 'segmented-icon-control-parametric' }) await showTableView(page);
.click();
await page
.getByRole('button', { name: 'segmented-icon-control-table' })
.click();
// Check for expected values // Check for expected values
await clearTableFilters(page); await clearTableFilters(page);
@@ -45,12 +36,8 @@ test('Purchasing - Index', async ({ browser }) => {
// Supplier parts tab // Supplier parts tab
await loadTab(page, 'Supplier Parts'); await loadTab(page, 'Supplier Parts');
await page await showParametricView(page);
.getByRole('button', { name: 'segmented-icon-control-parametric' }) await showTableView(page);
.click();
await page
.getByRole('button', { name: 'segmented-icon-control-table' })
.click();
// Check for expected values // Check for expected values
await clearTableFilters(page); await clearTableFilters(page);
@@ -60,12 +47,8 @@ test('Purchasing - Index', async ({ browser }) => {
// Manufacturers tab // Manufacturers tab
await loadTab(page, 'Manufacturers'); await loadTab(page, 'Manufacturers');
await page await showParametricView(page);
.getByRole('button', { name: 'segmented-icon-control-parametric' }) await showTableView(page);
.click();
await page
.getByRole('button', { name: 'segmented-icon-control-table' })
.click();
// Check for expected values // Check for expected values
await clearTableFilters(page); await clearTableFilters(page);
@@ -76,12 +59,8 @@ test('Purchasing - Index', async ({ browser }) => {
// Manufacturer parts tab // Manufacturer parts tab
await loadTab(page, 'Manufacturer Parts'); await loadTab(page, 'Manufacturer Parts');
await page await showParametricView(page);
.getByRole('button', { name: 'segmented-icon-control-parametric' }) await showTableView(page);
.click();
await page
.getByRole('button', { name: 'segmented-icon-control-table' })
.click();
// Check for expected values // Check for expected values
await clearTableFilters(page); await clearTableFilters(page);

View File

@@ -5,7 +5,10 @@ import {
clickOnRowMenu, clickOnRowMenu,
globalSearch, globalSearch,
loadTab, loadTab,
setTableChoiceFilter setTableChoiceFilter,
showCalendarView,
showParametricView,
showTableView
} from '../helpers.ts'; } from '../helpers.ts';
import { doCachedLogin } from '../login.ts'; import { doCachedLogin } from '../login.ts';
@@ -18,15 +21,9 @@ test('Sales Orders - Tabs', async ({ browser }) => {
await loadTab(page, 'Sales Orders'); await loadTab(page, 'Sales Orders');
await page.waitForURL('**/web/sales/index/salesorders'); await page.waitForURL('**/web/sales/index/salesorders');
await page await showParametricView(page);
.getByRole('button', { name: 'segmented-icon-control-parametric' }) await showCalendarView(page);
.click(); await showTableView(page);
await page
.getByRole('button', { name: 'segmented-icon-control-calendar' })
.click();
await page
.getByRole('button', { name: 'segmented-icon-control-table' })
.click();
// Pending Shipments panel // Pending Shipments panel
await loadTab(page, 'Pending Shipments'); await loadTab(page, 'Pending Shipments');
@@ -37,25 +34,15 @@ test('Sales Orders - Tabs', async ({ browser }) => {
await loadTab(page, 'Return Orders'); await loadTab(page, 'Return Orders');
await page.getByRole('cell', { name: 'NOISE-COMPLAINT' }).waitFor(); await page.getByRole('cell', { name: 'NOISE-COMPLAINT' }).waitFor();
await page await showParametricView(page);
.getByRole('button', { name: 'segmented-icon-control-parametric' }) await showCalendarView(page);
.click(); await showTableView(page);
await page
.getByRole('button', { name: 'segmented-icon-control-calendar' })
.click();
await page
.getByRole('button', { name: 'segmented-icon-control-table' })
.click();
// Customers // Customers
await loadTab(page, 'Customers'); await loadTab(page, 'Customers');
await page await showParametricView(page);
.getByRole('button', { name: 'segmented-icon-control-parametric' }) await showTableView(page);
.click();
await page
.getByRole('button', { name: 'segmented-icon-control-table' })
.click();
await page.getByText('Customer A').click(); await page.getByText('Customer A').click();
await loadTab(page, 'Notes'); await loadTab(page, 'Notes');