Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Refactor] Custom states #8438

Merged
merged 54 commits into from
Dec 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
cf2dae0
Enhancements for "custom state" form
SchrodingersGat Nov 5, 2024
15f6930
Improve back-end validation
SchrodingersGat Nov 6, 2024
7ff81c4
Improve table rendering
SchrodingersGat Nov 6, 2024
989373d
Merge branch 'master' into custom-states
SchrodingersGat Nov 8, 2024
106a935
Merge branch 'master' into custom-states
SchrodingersGat Nov 8, 2024
e131343
Merge branch 'master' into custom-states
SchrodingersGat Nov 10, 2024
87f4245
Merge branch 'master' into custom-states
SchrodingersGat Nov 20, 2024
612d71a
Fix lookup for useStatusCodes
SchrodingersGat Nov 21, 2024
8cbe376
Fix status display for SockDetail page
SchrodingersGat Nov 21, 2024
dd6033b
Fix SalesOrder status display
SchrodingersGat Nov 21, 2024
80d6a97
Refactor get_custom_classes
SchrodingersGat Nov 21, 2024
d1e0450
Fix for status table filters
SchrodingersGat Nov 21, 2024
2fcc0bb
Cleanup (and note to self)
SchrodingersGat Nov 21, 2024
59a0113
Merge branch 'master' into custom-states
SchrodingersGat Nov 21, 2024
ec55c57
Merge branch 'master' into custom-states
SchrodingersGat Nov 24, 2024
063b5f1
Include custom state values in specific API endpoints
SchrodingersGat Nov 24, 2024
d1e73fb
Add serializer class definition
SchrodingersGat Nov 24, 2024
cd28459
Use same serializer for AllStatusView
SchrodingersGat Nov 24, 2024
9a10495
Fix API to match existing frontend type StatusCodeListInterface
SchrodingersGat Nov 24, 2024
c222eed
Enable filtering by reference status type
SchrodingersGat Nov 24, 2024
366a826
Add option to duplicate an existing custom state
SchrodingersGat Nov 24, 2024
9ebc008
Improved validation for the InvenTreeCustomUserStateModel class
SchrodingersGat Nov 24, 2024
6cf9bfc
Code cleanup
SchrodingersGat Nov 24, 2024
a8c6563
Merge branch 'master' into custom-states
SchrodingersGat Nov 27, 2024
1d69087
Merge remote-tracking branch 'origin/master' into custom-states
SchrodingersGat Dec 27, 2024
97ff73b
Fix default value in StockOperationsRow
SchrodingersGat Dec 27, 2024
b4955a9
Use custom status values in stock operations
SchrodingersGat Dec 27, 2024
0663b7a
Allow custom values
SchrodingersGat Dec 27, 2024
3cc280d
Fix migration
SchrodingersGat Dec 27, 2024
886bc17
Bump API version
SchrodingersGat Dec 27, 2024
31057ee
Fix filtering of stock items by "status"
SchrodingersGat Dec 27, 2024
d389aa2
Enhance status filter for orders
SchrodingersGat Dec 27, 2024
876e90a
Fix status code rendering
SchrodingersGat Dec 27, 2024
516a6d9
Build Order API filter
SchrodingersGat Dec 27, 2024
9928939
Update playwright tests for build filters
SchrodingersGat Dec 28, 2024
b260128
Additional playwright tests for stock table filters
SchrodingersGat Dec 28, 2024
cf9d71f
Add 'custom' attribute
SchrodingersGat Dec 28, 2024
5be8d7c
Fix unit tests
SchrodingersGat Dec 28, 2024
ad79c79
Add custom state field validation
SchrodingersGat Dec 28, 2024
7621052
Implement StatusCodeMixin for setting status code values
SchrodingersGat Dec 28, 2024
26f1792
Clear out 'custom key' if the base key does not match
SchrodingersGat Dec 28, 2024
1568dc2
Updated playwright testing
SchrodingersGat Dec 28, 2024
60a777f
Remove timeout
SchrodingersGat Dec 28, 2024
a731afc
Refactor detail pages which display status
SchrodingersGat Dec 28, 2024
08a1571
Update old migrations - add field validator
SchrodingersGat Dec 28, 2024
2ce17f6
Remove dead code
SchrodingersGat Dec 28, 2024
06c858a
Simplify API query filtering
SchrodingersGat Dec 28, 2024
5ddb279
Revert "Simplify API query filtering"
SchrodingersGat Dec 28, 2024
389b62e
Fix save method
SchrodingersGat Dec 28, 2024
98bc45a
Unit test fixes
SchrodingersGat Dec 28, 2024
89a3268
Fix for ReturnOrderLineItem
SchrodingersGat Dec 28, 2024
59ad097
Reorganize code
SchrodingersGat Dec 28, 2024
5f7ed1e
Merge remote-tracking branch 'origin/master' into custom-states
SchrodingersGat Dec 28, 2024
702209d
Adjust unit test
SchrodingersGat Dec 28, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/backend/InvenTree/InvenTree/api_version.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
"""InvenTree API version information."""

# InvenTree API version
INVENTREE_API_VERSION = 296
INVENTREE_API_VERSION = 297

"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""


INVENTREE_API_TEXT = """

v297 - 2024-12-29 - https://github.com/inventree/InvenTree/pull/8438
- Adjustments to the CustomUserState API endpoints and serializers

v296 - 2024-12-25 : https://github.com/inventree/InvenTree/pull/8732
- Adjust default "part_detail" behaviour for StockItem API endpoints

Expand Down
12 changes: 11 additions & 1 deletion src/backend/InvenTree/build/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,17 @@ class Meta:
model = Build
fields = ['sales_order']

status = rest_filters.NumberFilter(label='Status')
status = rest_filters.NumberFilter(label=_('Order Status'), method='filter_status')

def filter_status(self, queryset, name, value):
"""Filter by integer status code.

Note: Also account for the possibility of a custom status code
"""
q1 = Q(status=value, status_custom_key__isnull=True)
q2 = Q(status_custom_key=value)

return queryset.filter(q1 | q2).distinct()

active = rest_filters.BooleanFilter(label='Build is active', method='filter_active')

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from django.db import migrations

import generic.states.fields
import generic.states.validators
import InvenTree.status_codes


Expand All @@ -23,6 +24,11 @@ class Migration(migrations.Migration):
help_text="Additional status information for this item",
null=True,
verbose_name="Custom status key",
validators=[
generic.states.validators.CustomStatusCodeValidator(
status_class=InvenTree.status_codes.BuildStatus
),
]
),
),
migrations.AlterField(
Expand All @@ -32,7 +38,12 @@ class Migration(migrations.Migration):
choices=InvenTree.status_codes.BuildStatus.items(),
default=10,
help_text="Build status code",
validators=[django.core.validators.MinValueValidator(0)],
validators=[
django.core.validators.MinValueValidator(0),
generic.states.validators.CustomStatusCodeValidator(
status_class=InvenTree.status_codes.BuildStatus
),
],
verbose_name="Build Status",
),
),
Expand Down
6 changes: 5 additions & 1 deletion src/backend/InvenTree/build/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
get_global_setting,
prevent_build_output_complete_on_incompleted_tests,
)
from generic.states import StateTransitionMixin
from generic.states import StateTransitionMixin, StatusCodeMixin
from plugin.events import trigger_event
from stock.status_codes import StockHistoryCode, StockStatus

Expand All @@ -59,6 +59,7 @@ class Build(
InvenTree.models.PluginValidationMixin,
InvenTree.models.ReferenceIndexingMixin,
StateTransitionMixin,
StatusCodeMixin,
MPTTModel,
):
"""A Build object organises the creation of new StockItem objects from other existing StockItem objects.
Expand All @@ -84,6 +85,8 @@ class Build(
priority: Priority of the build
"""

STATUS_CLASS = BuildStatus

class Meta:
"""Metaclass options for the BuildOrder model."""

Expand Down Expand Up @@ -319,6 +322,7 @@ def get_absolute_url(self):
verbose_name=_('Build Status'),
default=BuildStatus.PENDING.value,
choices=BuildStatus.items(),
status_class=BuildStatus,
validators=[MinValueValidator(0)],
help_text=_('Build status code'),
)
Expand Down
4 changes: 3 additions & 1 deletion src/backend/InvenTree/build/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -574,7 +574,9 @@ class Meta:
)

status_custom_key = serializers.ChoiceField(
choices=StockStatus.items(), default=StockStatus.OK.value, label=_('Status')
choices=StockStatus.items(custom=True),
default=StockStatus.OK.value,
label=_('Status'),
)

accept_incomplete_allocation = serializers.BooleanField(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Generated by Django 4.2.17 on 2024-12-27 09:15

import common.validators
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('common', '0033_delete_colortheme'),
]

operations = [
migrations.AlterUniqueTogether(
name='inventreecustomuserstatemodel',
unique_together=set(),
),
migrations.AlterField(
model_name='inventreecustomuserstatemodel',
name='key',
field=models.IntegerField(help_text='Numerical value that will be saved in the models database', verbose_name='Value'),
),
migrations.AlterField(
model_name='inventreecustomuserstatemodel',
name='name',
field=models.CharField(help_text='Name of the state', max_length=250, validators=[common.validators.validate_uppercase, common.validators.validate_variable_string], verbose_name='Name'),
),
migrations.AlterUniqueTogether(
name='inventreecustomuserstatemodel',
unique_together={('reference_status', 'name'), ('reference_status', 'key')},
),
]
112 changes: 73 additions & 39 deletions src/backend/InvenTree/common/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
import users.models
from common.setting.type import InvenTreeSettingsKeyType, SettingsKeyType
from generic.states import ColorEnum
from generic.states.custom import get_custom_classes, state_color_mappings
from generic.states.custom import state_color_mappings
from InvenTree.sanitizer import sanitize_svg

logger = logging.getLogger('inventree')
Expand Down Expand Up @@ -1927,33 +1927,67 @@ def check_permission(self, permission, user):


class InvenTreeCustomUserStateModel(models.Model):
"""Custom model to extends any registered state with extra custom, user defined states."""
"""Custom model to extends any registered state with extra custom, user defined states.

Fields:
reference_status: Status set that is extended with this custom state
logical_key: State logical key that is equal to this custom state in business logic
key: Numerical value that will be saved in the models database
name: Name of the state (must be uppercase and a valid variable identifier)
label: Label that will be displayed in the frontend (human readable)
color: Color that will be displayed in the frontend

"""

class Meta:
"""Metaclass options for this mixin."""

verbose_name = _('Custom State')
verbose_name_plural = _('Custom States')
unique_together = [('reference_status', 'key'), ('reference_status', 'name')]

reference_status = models.CharField(
max_length=250,
verbose_name=_('Reference Status Set'),
help_text=_('Status set that is extended with this custom state'),
)

logical_key = models.IntegerField(
verbose_name=_('Logical Key'),
help_text=_(
'State logical key that is equal to this custom state in business logic'
),
)

key = models.IntegerField(
verbose_name=_('Key'),
help_text=_('Value that will be saved in the models database'),
verbose_name=_('Value'),
help_text=_('Numerical value that will be saved in the models database'),
)

name = models.CharField(
max_length=250, verbose_name=_('Name'), help_text=_('Name of the state')
max_length=250,
verbose_name=_('Name'),
help_text=_('Name of the state'),
validators=[
common.validators.validate_uppercase,
common.validators.validate_variable_string,
],
)

label = models.CharField(
max_length=250,
verbose_name=_('Label'),
help_text=_('Label that will be displayed in the frontend'),
)

color = models.CharField(
max_length=10,
choices=state_color_mappings(),
default=ColorEnum.secondary.value,
verbose_name=_('Color'),
help_text=_('Color that will be displayed in the frontend'),
)
logical_key = models.IntegerField(
verbose_name=_('Logical Key'),
help_text=_(
'State logical key that is equal to this custom state in business logic'
),
)

model = models.ForeignKey(
ContentType,
on_delete=models.SET_NULL,
Expand All @@ -1962,18 +1996,6 @@ class InvenTreeCustomUserStateModel(models.Model):
verbose_name=_('Model'),
help_text=_('Model this state is associated with'),
)
reference_status = models.CharField(
max_length=250,
verbose_name=_('Reference Status Set'),
help_text=_('Status set that is extended with this custom state'),
)

class Meta:
"""Metaclass options for this mixin."""

verbose_name = _('Custom State')
verbose_name_plural = _('Custom States')
unique_together = [['model', 'reference_status', 'key', 'logical_key']]

def __str__(self) -> str:
"""Return string representation of the custom state."""
Expand All @@ -1999,38 +2021,50 @@ def clean(self) -> None:
if self.key == self.logical_key:
raise ValidationError({'key': _('Key must be different from logical key')})

if self.reference_status is None or self.reference_status == '':
raise ValidationError({
'reference_status': _('Reference status must be selected')
})
# Check against the reference status class
status_class = self.get_status_class()

# Ensure that the key is not in the range of the logical keys of the reference status
ref_set = list(
filter(
lambda x: x.__name__ == self.reference_status,
get_custom_classes(include_custom=False),
)
)
if len(ref_set) == 0:
if not status_class:
raise ValidationError({
'reference_status': _('Reference status set not found')
'reference_status': _('Valid reference status class must be provided')
})
ref_set = ref_set[0]
if self.key in ref_set.keys(): # noqa: SIM118

if self.key in status_class.values():
raise ValidationError({
'key': _(
'Key must be different from the logical keys of the reference status'
)
})
if self.logical_key not in ref_set.keys(): # noqa: SIM118

if self.logical_key not in status_class.values():
raise ValidationError({
'logical_key': _(
'Logical key must be in the logical keys of the reference status'
)
})

if self.name in status_class.names():
raise ValidationError({
'name': _(
'Name must be different from the names of the reference status'
)
})

return super().clean()

def get_status_class(self):
"""Return the appropriate status class for this custom state."""
from generic.states import StatusCode
from InvenTree.helpers import inheritors

if not self.reference_status:
return None

# Return the first class that matches the reference status
for cls in inheritors(StatusCode):
if cls.__name__ == self.reference_status:
return cls


class SelectionList(InvenTree.models.MetadataMixin, InvenTree.models.InvenTreeModel):
"""Class which represents a list of selectable items for parameters.
Expand Down
1 change: 1 addition & 0 deletions src/backend/InvenTree/common/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,7 @@ class Meta:
]

model_name = serializers.CharField(read_only=True, source='model.name')

reference_status = serializers.ChoiceField(
choices=generic.states.custom.state_reference_mappings()
)
Expand Down
14 changes: 14 additions & 0 deletions src/backend/InvenTree/common/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,3 +113,17 @@ def validate_icon(name: Union[str, None]):
return

common.icons.validate_icon(name)


def validate_uppercase(value: str):
"""Ensure that the provided value is uppercase."""
value = str(value)

if value != value.upper():
raise ValidationError(_('Value must be uppercase'))


def validate_variable_string(value: str):
"""The passed value must be a valid variable identifier string."""
if not re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', value):
raise ValidationError(_('Value must be a valid variable identifier'))
3 changes: 2 additions & 1 deletion src/backend/InvenTree/generic/states/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@
States can be extended with custom options for each InvenTree instance - those options are stored in the database and need to link back to state values.
"""

from .states import ColorEnum, StatusCode
from .states import ColorEnum, StatusCode, StatusCodeMixin
from .transition import StateTransitionMixin, TransitionMethod, storage

__all__ = [
'ColorEnum',
'StateTransitionMixin',
'StatusCode',
'StatusCodeMixin',
'TransitionMethod',
'storage',
]
Loading
Loading