[feature] Unique parameters (#12388)
* Add uniqueness requirement for parameter values * Add documentation * Do not copy unique parameters * Updated playwright tests * Enhanced validation checks for uniqueness * Update documentation * Update CHANGELOG * Bump API version
This commit is contained in:
parent
183e6c37b1
commit
61dcc04205
|
|
@ -15,6 +15,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
### Added
|
||||
|
||||
|
||||
- [#12388](https://github.com/inventree/InvenTree/pull/12388) adds uniqueness requirements options for the Parameter and ParameterTemplate models. This allows users to specify whether a parameter value should be unique for a given model type, or globally unique across all models.
|
||||
- [#12310](https://github.com/inventree/InvenTree/pull/12310) adds the ability to disassemble (or break apart) assembled stock items into their component parts, based on the Bill of Materials (BOM) associated with the stock item. This allows users to easily break down assembled items into their constituent parts, which can be useful for inventory management and tracking purposes.
|
||||
- [#12117](https://github.com/inventree/InvenTree/pull/12117) adds a "preview" drawer to the InvenTree table component, allowing users to preview the details of a selected row without navigating away from the table view. This feature is optional and can be enabled or disabled via the `PREVIEW_DRAWER_ENABLED` system setting.
|
||||
- [#12341](https://github.com/inventree/InvenTree/pull/12341) adds support for importing internal part prices.
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ Parameter templates are used to define the different types of parameters which a
|
|||
| Choices | A comma-separated list of valid choices for parameter values linked to this template. |
|
||||
| Checkbox | If set, parameters linked to this template can only be assigned values *true* or *false* |
|
||||
| Selection List | If set, parameters linked to this template can only be assigned values from the linked [selection list](#selection-lists) |
|
||||
| Unique | Enforce a [uniqueness requirement](#parameter-uniqueness) on parameter values linked to this template |
|
||||
|
||||
{{ image("concepts/parameter-template.png", "Parameters Template") }}
|
||||
|
||||
|
|
@ -59,6 +60,45 @@ To add a parameter, navigate to a specific part detail page, click on the "Param
|
|||
|
||||
Select the parameter `Template` you would like to use for this parameter, fill-out the `Data` field (value of this specific parameter) and click the "Submit" button.
|
||||
|
||||
### Parameter Uniqueness
|
||||
|
||||
A parameter template can be configured to enforce a uniqueness requirement on the values of any parameters linked to it. This is useful for parameters which are expected to act as a unique identifier and prevents duplicate values from being entered by mistake.
|
||||
|
||||
The `Unique` attribute on a parameter template supports the following options:
|
||||
|
||||
| Option | Description |
|
||||
| --- | --- |
|
||||
| No uniqueness required | The default option - no restriction is placed on parameter values |
|
||||
| Unique for model type | A parameter value must be unique amongst all other parameters (linked to this template) which are assigned to the *same* model type. For example, a template with this option enabled could be used to enforce unique serial numbers across all `Part` instances, without preventing the same value from also being used against a `Company` instance |
|
||||
| Globally unique | A parameter value must be unique amongst *all* other parameters linked to this template, regardless of the model type to which they are assigned |
|
||||
|
||||
!!! info "Case Insensitive"
|
||||
Uniqueness checks are case-insensitive - for example, the values `ABC123` and `abc123` are considered to be duplicates of each other.
|
||||
|
||||
If a parameter value is entered which does not satisfy the uniqueness requirement of its template, it will be rejected and an error message displayed.
|
||||
|
||||
#### Validation Against Numeric Value
|
||||
|
||||
If a parameter template defines a set of [units](#parameter-units), uniqueness is checked against the *normalized numeric value* of the parameter, rather than the raw text entered. This ensures that equivalent values expressed in different (but compatible) units are correctly detected as duplicates.
|
||||
|
||||
For example, if a *Resistance* template (with base units of `ohm`) enforces uniqueness, then a value of `1k` would be rejected as a duplicate of an existing value of `1000`, as they both represent the same physical quantity.
|
||||
|
||||
Templates which do not define a set of units are compared using a direct (case-insensitive) text comparison of the raw parameter value.
|
||||
|
||||
#### Copying Caveats
|
||||
|
||||
Some workflows in InvenTree allow parameters to be copied from one object to another - for example, when duplicating a part, build order, or other object which supports parameters.
|
||||
|
||||
Any parameter which is linked to a template with a uniqueness requirement is *skipped* when copying parameters in this way. This prevents the copy operation from creating a duplicate (and therefore invalid) value on the new object. If this occurs, the new object simply will not have a parameter created against that particular template - it can be added manually (with a distinct value) afterwards.
|
||||
|
||||
#### Category Parameter Caveats
|
||||
|
||||
A parameter template can be linked to a part category, along with a default value which is automatically applied to any new parts created within that category (or a sub-category).
|
||||
|
||||
This default value system is not compatible with a template which enforces a uniqueness requirement - applying the *same* default value to every part in a category would immediately conflict with the "unique" requirement, as soon as more than one part exists in that category.
|
||||
|
||||
For this reason, a category-based default value is *not* applied for any parameter template which has a uniqueness requirement configured. If you need to assign values for such parameters, this must be done manually (or via the API) on a per-part basis.
|
||||
|
||||
## Parametric Tables
|
||||
|
||||
Parametric tables gather all parameters from all objects of a particular type, to be sorted and filtered.
|
||||
|
|
|
|||
|
|
@ -1,11 +1,14 @@
|
|||
"""InvenTree API version information."""
|
||||
|
||||
# InvenTree API version
|
||||
INVENTREE_API_VERSION = 521
|
||||
INVENTREE_API_VERSION = 522
|
||||
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
||||
|
||||
INVENTREE_API_TEXT = """
|
||||
|
||||
v522 -> 2026-07-14 : https://github.com/inventree/InvenTree/pull/12388
|
||||
- Adds "unique" field to the ParameterTemplate model
|
||||
|
||||
v521 -> 2026-07-12 : https://github.com/inventree/InvenTree/pull/12360
|
||||
- Removes the MPTT mixin from the StockItem model, and removes the self-referential tree structure from the database.
|
||||
|
||||
|
|
|
|||
|
|
@ -597,12 +597,21 @@ class InvenTreeParameterMixin(InvenTreePermissionCheckMixin, models.Model):
|
|||
|
||||
content_type = ContentType.objects.get_for_model(self.__class__)
|
||||
|
||||
template_ids = [parameter.template.pk for parameter in other.parameters.all()]
|
||||
# Skip any parameters which are linked to a template with a uniqueness requirement,
|
||||
# as copying these values would create conflicting (duplicate) values
|
||||
copyable_parameters = [
|
||||
parameter
|
||||
for parameter in other.parameters.all().select_related('template')
|
||||
if parameter.template.unique
|
||||
== common.models.ParameterTemplate.UniqueOptions.NONE
|
||||
]
|
||||
|
||||
template_ids = [parameter.template.pk for parameter in copyable_parameters]
|
||||
|
||||
# Remove all conflicting parameters first
|
||||
self.parameters_list.filter(template__pk__in=template_ids).delete()
|
||||
|
||||
for parameter in other.parameters.all():
|
||||
for parameter in copyable_parameters:
|
||||
parameter.pk = None
|
||||
parameter.model_id = self.pk
|
||||
parameter.model_type = content_type
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import common.validators
|
|||
class ParameterTemplateAdmin(admin.ModelAdmin):
|
||||
"""Admin interface for ParameterTemplate objects."""
|
||||
|
||||
list_display = ('name', 'description', 'model_type', 'units')
|
||||
list_display = ('name', 'description', 'model_type', 'units', 'unique')
|
||||
search_fields = ('name', 'description')
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -877,7 +877,7 @@ class ParameterTemplateFilter(FilterSet):
|
|||
"""Metaclass options."""
|
||||
|
||||
model = common.models.ParameterTemplate
|
||||
fields = ['name', 'units', 'checkbox', 'enabled']
|
||||
fields = ['name', 'units', 'checkbox', 'enabled', 'unique']
|
||||
|
||||
has_choices = rest_filters.BooleanFilter(
|
||||
method='filter_has_choices', label='Has Choice'
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
# Generated by Django 5.2.15 on 2026-07-14 00:05
|
||||
|
||||
from django.db import migrations, models
|
||||
from common.models import ParameterTemplate
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("common", "0046_alter_emailmessage_global_id_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="parametertemplate",
|
||||
name="unique",
|
||||
field=models.PositiveIntegerField(
|
||||
choices=ParameterTemplate.UniqueOptions.choices,
|
||||
default=0,
|
||||
help_text="Enforce uniqueness of linked parameter values against this template",
|
||||
verbose_name="Uniqueness",
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
@ -2640,6 +2640,19 @@ class ParameterTemplate(
|
|||
|
||||
choice_fnc = common.validators.parameter_template_model_options
|
||||
|
||||
class UniqueOptions(models.IntegerChoices):
|
||||
"""Enumeration of uniqueness options for a ParameterTemplate.
|
||||
|
||||
Attributes:
|
||||
NONE: No uniqueness requirement is enforced (default)
|
||||
MODEL_TYPE: Linked parameter values must be unique for a given model type
|
||||
GLOBAL: Linked parameter values must be unique across all model types
|
||||
"""
|
||||
|
||||
NONE = 0, _('No uniqueness required')
|
||||
MODEL_TYPE = 1, _('Unique for model type')
|
||||
GLOBAL = 2, _('Globally unique')
|
||||
|
||||
@staticmethod
|
||||
def get_api_url() -> str:
|
||||
"""Return the API URL associated with the ParameterTemplate model."""
|
||||
|
|
@ -2783,6 +2796,15 @@ class ParameterTemplate(
|
|||
help_text=_('Is this parameter template enabled?'),
|
||||
)
|
||||
|
||||
unique = models.PositiveIntegerField(
|
||||
default=UniqueOptions.NONE,
|
||||
choices=UniqueOptions.choices,
|
||||
verbose_name=_('Uniqueness'),
|
||||
help_text=_(
|
||||
'Enforce uniqueness of linked parameter values against this template'
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@receiver(
|
||||
post_save, sender=ParameterTemplate, dispatch_uid='post_save_parameter_template'
|
||||
|
|
@ -2888,6 +2910,9 @@ class Parameter(
|
|||
except ValidationError as e:
|
||||
raise ValidationError({'data': e.message})
|
||||
|
||||
# Validate the parameter data against any uniqueness requirements imposed by the template
|
||||
self.validate_uniqueness()
|
||||
|
||||
if InvenTree.ready.isReadOnlyCommand():
|
||||
# Skip plugin validation checks during read-only management commands
|
||||
return
|
||||
|
|
@ -2935,6 +2960,42 @@ class Parameter(
|
|||
if math.isnan(self.data_numeric) or math.isinf(self.data_numeric):
|
||||
self.data_numeric = None
|
||||
|
||||
def validate_uniqueness(self):
|
||||
"""Ensure that this Parameter satisfies any uniqueness requirements imposed by its template.
|
||||
|
||||
The ParameterTemplate.unique field determines the scope of the uniqueness check:
|
||||
|
||||
- NONE: No uniqueness check is performed
|
||||
- MODEL_TYPE: The value must be unique amongst other parameters (for this template) linked to the same model type
|
||||
- GLOBAL: The value must be unique amongst all other parameters linked to this template
|
||||
|
||||
Note: If the template defines a set of 'units', the comparison is performed against the
|
||||
normalized 'data_numeric' value, so that equivalent values expressed in different
|
||||
(but compatible) units are correctly detected as duplicates (e.g. '1k' and '1000' ohms).
|
||||
"""
|
||||
uniqueness = self.template.unique
|
||||
|
||||
if uniqueness == ParameterTemplate.UniqueOptions.NONE:
|
||||
return
|
||||
|
||||
if self.template.units and self.data_numeric is not None:
|
||||
query = Parameter.objects.filter(
|
||||
template=self.template, data_numeric=self.data_numeric
|
||||
)
|
||||
else:
|
||||
query = Parameter.objects.filter(
|
||||
template=self.template, data__iexact=self.data
|
||||
)
|
||||
|
||||
if self.pk:
|
||||
query = query.exclude(pk=self.pk)
|
||||
|
||||
if uniqueness == ParameterTemplate.UniqueOptions.MODEL_TYPE:
|
||||
query = query.filter(model_type=self.model_type)
|
||||
|
||||
if query.exists():
|
||||
raise ValidationError({'data': _('Parameter value must be unique')})
|
||||
|
||||
def check_permission(self, permission, user):
|
||||
"""Check if the user has the required permission for this parameter."""
|
||||
from InvenTree.models import InvenTreeParameterMixin
|
||||
|
|
|
|||
|
|
@ -874,6 +874,7 @@ class ParameterTemplateSerializer(
|
|||
'choices',
|
||||
'selectionlist',
|
||||
'enabled',
|
||||
'unique',
|
||||
]
|
||||
|
||||
# Note: The choices are overridden at run-time on class initialization
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import io
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.files.base import ContentFile
|
||||
from django.core.files.storage import default_storage
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
|
|
@ -73,6 +74,7 @@ class ParameterAPITests(InvenTreeAPITestCase):
|
|||
'model_type',
|
||||
'selectionlist',
|
||||
'enabled',
|
||||
'unique',
|
||||
]:
|
||||
self.assertIn(
|
||||
field,
|
||||
|
|
@ -567,6 +569,179 @@ class ParameterAPITests(InvenTreeAPITestCase):
|
|||
common.models.Parameter.objects.filter(pk=parameter.pk).exists()
|
||||
)
|
||||
|
||||
def test_parameter_uniqueness(self):
|
||||
"""Test the uniqueness options which can be applied to a ParameterTemplate."""
|
||||
from company.models import Company
|
||||
from part.models import Part
|
||||
|
||||
part_a = Part.objects.create(name='Part A', description='A part for testing')
|
||||
part_b = Part.objects.create(name='Part B', description='A part for testing')
|
||||
part_c = Part.objects.create(name='Part C', description='A part for testing')
|
||||
company = Company.objects.create(
|
||||
name='Test Company', description='A company for testing'
|
||||
)
|
||||
|
||||
template = common.models.ParameterTemplate.objects.create(
|
||||
name='Serial Number', description='A serial number parameter'
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
template.unique, common.models.ParameterTemplate.UniqueOptions.NONE
|
||||
)
|
||||
|
||||
param_a = common.models.Parameter(
|
||||
template=template,
|
||||
model_type=part_a.get_content_type(),
|
||||
model_id=part_a.pk,
|
||||
data='ABC123',
|
||||
)
|
||||
param_a.full_clean()
|
||||
param_a.save()
|
||||
|
||||
# No uniqueness requirement - a duplicate value against a different part is fine
|
||||
param_b = common.models.Parameter(
|
||||
template=template,
|
||||
model_type=part_b.get_content_type(),
|
||||
model_id=part_b.pk,
|
||||
data='ABC123',
|
||||
)
|
||||
param_b.full_clean()
|
||||
param_b.save()
|
||||
|
||||
# Re-saving the existing instance (unchanged) should not raise any errors
|
||||
param_a.full_clean()
|
||||
param_a.save()
|
||||
|
||||
# Now, require uniqueness *per model type*
|
||||
template.unique = common.models.ParameterTemplate.UniqueOptions.MODEL_TYPE
|
||||
template.save()
|
||||
|
||||
# A new Part with the same value should be rejected
|
||||
with self.assertRaises(ValidationError):
|
||||
common.models.Parameter(
|
||||
template=template,
|
||||
model_type=part_c.get_content_type(),
|
||||
model_id=part_c.pk,
|
||||
data='ABC123',
|
||||
).full_clean()
|
||||
|
||||
# A case-insensitive match should also be rejected
|
||||
with self.assertRaises(ValidationError):
|
||||
common.models.Parameter(
|
||||
template=template,
|
||||
model_type=part_c.get_content_type(),
|
||||
model_id=part_c.pk,
|
||||
data='abc123',
|
||||
).full_clean()
|
||||
|
||||
# A different model type entirely is not affected by the 'model type' restriction
|
||||
param_company = common.models.Parameter(
|
||||
template=template,
|
||||
model_type=company.get_content_type(),
|
||||
model_id=company.pk,
|
||||
data='ABC123',
|
||||
)
|
||||
param_company.full_clean()
|
||||
param_company.save()
|
||||
|
||||
# Finally, require the value to be *globally* unique
|
||||
template.unique = common.models.ParameterTemplate.UniqueOptions.GLOBAL
|
||||
template.save()
|
||||
|
||||
with self.assertRaises(ValidationError):
|
||||
common.models.Parameter(
|
||||
template=template,
|
||||
model_type=part_c.get_content_type(),
|
||||
model_id=part_c.pk,
|
||||
data='ABC123',
|
||||
).full_clean()
|
||||
|
||||
def test_parameter_uniqueness_units(self):
|
||||
"""Test that uniqueness checks are unit-aware for templates which define units.
|
||||
|
||||
Values expressed in different (but compatible) units which represent the
|
||||
same physical quantity must be detected as duplicates.
|
||||
"""
|
||||
from part.models import Part
|
||||
|
||||
part_a = Part.objects.create(name='Part A', description='A part for testing')
|
||||
part_b = Part.objects.create(name='Part B', description='A part for testing')
|
||||
|
||||
template = common.models.ParameterTemplate.objects.create(
|
||||
name='Resistance',
|
||||
units='ohm',
|
||||
description='A globally unique resistance parameter',
|
||||
unique=common.models.ParameterTemplate.UniqueOptions.GLOBAL,
|
||||
)
|
||||
|
||||
param_a = common.models.Parameter(
|
||||
template=template,
|
||||
model_type=part_a.get_content_type(),
|
||||
model_id=part_a.pk,
|
||||
data='1000',
|
||||
)
|
||||
param_a.full_clean()
|
||||
param_a.save()
|
||||
|
||||
# A value expressed as '1k' ohms is numerically identical to '1000' ohms
|
||||
with self.assertRaises(ValidationError):
|
||||
common.models.Parameter(
|
||||
template=template,
|
||||
model_type=part_b.get_content_type(),
|
||||
model_id=part_b.pk,
|
||||
data='1k',
|
||||
).full_clean()
|
||||
|
||||
# A distinct value (in different units) is not a duplicate
|
||||
param_b = common.models.Parameter(
|
||||
template=template,
|
||||
model_type=part_b.get_content_type(),
|
||||
model_id=part_b.pk,
|
||||
data='2k',
|
||||
)
|
||||
param_b.full_clean()
|
||||
param_b.save()
|
||||
|
||||
def test_copy_unique_parameters(self):
|
||||
"""Test that 'unique' parameters are skipped when copying parameters between model instances."""
|
||||
from part.models import Part
|
||||
|
||||
part_a = Part.objects.create(name='Part A', description='A part for testing')
|
||||
part_b = Part.objects.create(name='Part B', description='A part for testing')
|
||||
|
||||
normal_template = common.models.ParameterTemplate.objects.create(
|
||||
name='Color', description='A normal (non-unique) parameter'
|
||||
)
|
||||
|
||||
unique_template = common.models.ParameterTemplate.objects.create(
|
||||
name='Serial Number',
|
||||
description='A globally unique parameter',
|
||||
unique=common.models.ParameterTemplate.UniqueOptions.GLOBAL,
|
||||
)
|
||||
|
||||
common.models.Parameter.objects.create(
|
||||
template=normal_template,
|
||||
model_type=part_a.get_content_type(),
|
||||
model_id=part_a.pk,
|
||||
data='Red',
|
||||
)
|
||||
|
||||
common.models.Parameter.objects.create(
|
||||
template=unique_template,
|
||||
model_type=part_a.get_content_type(),
|
||||
model_id=part_a.pk,
|
||||
data='ABC123',
|
||||
)
|
||||
|
||||
# Copy parameters from part_a to part_b
|
||||
part_b.copy_parameters_from(part_a)
|
||||
|
||||
# The non-unique parameter should have been copied
|
||||
self.assertEqual(part_b.get_parameter('Color').data, 'Red')
|
||||
|
||||
# The unique parameter should *not* have been copied, to avoid a conflicting value
|
||||
self.assertIsNone(part_b.get_parameter('Serial Number'))
|
||||
|
||||
def test_parameter_annotation(self):
|
||||
"""Test that we can annotate parameters against a queryset."""
|
||||
from company.models import Company
|
||||
|
|
|
|||
|
|
@ -2348,6 +2348,14 @@ class Part(
|
|||
if category_template.template.pk in template_ids:
|
||||
continue
|
||||
|
||||
# Skip templates which enforce a uniqueness requirement - applying the same
|
||||
# default value to every part in the category would create conflicting values
|
||||
if (
|
||||
category_template.template.unique
|
||||
!= common.models.ParameterTemplate.UniqueOptions.NONE
|
||||
):
|
||||
continue
|
||||
|
||||
template_ids.add(category_template.template.pk)
|
||||
|
||||
parameters.append(
|
||||
|
|
|
|||
|
|
@ -1746,6 +1746,51 @@ class PartCreationTests(PartAPITestBase):
|
|||
prt = Part.objects.get(pk=data['pk'])
|
||||
self.assertEqual(prt.parameters.count(), 3)
|
||||
|
||||
def test_category_parameters_unique(self):
|
||||
"""Test that category parameters with a uniqueness requirement are not applied.
|
||||
|
||||
Applying the same default value to every part created in a category
|
||||
would immediately conflict with a 'unique' parameter template.
|
||||
"""
|
||||
cat = PartCategory.objects.get(pk=1)
|
||||
|
||||
normal_template = ParameterTemplate.objects.get(pk=1)
|
||||
|
||||
unique_template = ParameterTemplate.objects.create(
|
||||
name='Serial Number',
|
||||
description='A globally unique parameter',
|
||||
unique=ParameterTemplate.UniqueOptions.GLOBAL,
|
||||
)
|
||||
|
||||
PartCategoryParameterTemplate.objects.create(
|
||||
template=normal_template, category=cat, default_value='Normal Value'
|
||||
)
|
||||
|
||||
PartCategoryParameterTemplate.objects.create(
|
||||
template=unique_template, category=cat, default_value='Fixed Value'
|
||||
)
|
||||
|
||||
# Create two parts in this category, copying category parameters
|
||||
for name in ['Part A', 'Part B']:
|
||||
data = self.post(
|
||||
reverse('api-part-list'),
|
||||
{
|
||||
'category': cat.pk,
|
||||
'name': name,
|
||||
'description': 'A part for testing unique category parameters',
|
||||
'copy_category_parameters': True,
|
||||
},
|
||||
expected_code=201,
|
||||
).data
|
||||
|
||||
prt = Part.objects.get(pk=data['pk'])
|
||||
|
||||
# The 'normal' parameter should have been copied for each part
|
||||
self.assertIsNotNone(prt.get_parameter(normal_template.name))
|
||||
|
||||
# The 'unique' parameter template should *not* have been applied
|
||||
self.assertIsNone(prt.get_parameter(unique_template.name))
|
||||
|
||||
|
||||
class PartDetailTests(PartImageTestMixin, PartAPITestBase):
|
||||
"""Test that we can create / edit / delete Part objects via the API."""
|
||||
|
|
|
|||
|
|
@ -111,7 +111,8 @@ export function useParameterTemplateFields(): ApiFormFieldSet {
|
|||
active: true
|
||||
}
|
||||
},
|
||||
enabled: {}
|
||||
enabled: {},
|
||||
unique: {}
|
||||
};
|
||||
}, []);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,7 +70,43 @@ test('Company - Parameters', async ({ browser }) => {
|
|||
await page.getByRole('option', { name: 'NET-30' }).click();
|
||||
|
||||
await page.getByRole('cell', { name: 'Arrow Electronics' }).waitFor();
|
||||
await page.getByRole('cell', { name: 'PCB assembly house' }).waitFor();
|
||||
await page.getByRole('cell', { name: 'SUP-012' }).waitFor();
|
||||
|
||||
await page.getByRole('cell', { name: 'PCBA+' }).click();
|
||||
await page.getByRole('link', { name: 'details-company-45' }).click();
|
||||
|
||||
// Let's duplicate this company
|
||||
await openDetailAction(page, 'company', 'duplicate');
|
||||
await page
|
||||
.getByRole('textbox', { name: 'text-field-name' })
|
||||
.fill(`PCBA Duplicate ${Math.floor(Math.random() * 1000)}`);
|
||||
await page.getByRole('button', { name: 'Submit' }).click();
|
||||
|
||||
await page.waitForLoadState('networkidle');
|
||||
await loadTab(page, 'Parameters');
|
||||
|
||||
// Only one parameter should be visible (unique parameters not copied)
|
||||
await page.getByRole('cell', { name: 'NET-30' }).waitFor();
|
||||
await page.getByText('1 - 1 / 1').waitFor();
|
||||
|
||||
// Try to create a duplicate parameter
|
||||
await page
|
||||
.getByRole('button', { name: 'action-menu-add-parameters' })
|
||||
.click();
|
||||
await page
|
||||
.getByRole('menuitem', {
|
||||
name: 'action-menu-add-parameters-create-parameter'
|
||||
})
|
||||
.click();
|
||||
|
||||
await page
|
||||
.getByRole('combobox', { name: 'related-field-template' })
|
||||
.fill('supplier');
|
||||
await page.getByRole('option', { name: 'Supplier ID' }).click();
|
||||
|
||||
await page.getByRole('textbox', { name: 'text-field-data' }).fill('SUP-012');
|
||||
await page.getByRole('button', { name: 'Submit' }).click();
|
||||
await page.getByText('Parameter value must be unique').waitFor();
|
||||
});
|
||||
|
||||
test('Company - Supplier Parts', async ({ browser }) => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue