diff --git a/src/backend/InvenTree/InvenTree/api_version.py b/src/backend/InvenTree/InvenTree/api_version.py index 0ad025b8a7..195b1631c3 100644 --- a/src/backend/InvenTree/InvenTree/api_version.py +++ b/src/backend/InvenTree/InvenTree/api_version.py @@ -1,12 +1,15 @@ """InvenTree API version information.""" # InvenTree API version -INVENTREE_API_VERSION = 201 +INVENTREE_API_VERSION = 202 """Increment this API version number whenever there is a significant change to the API that any clients need to know about.""" INVENTREE_API_TEXT = """ +v202 - 2024-05-27 : https://github.com/inventree/InvenTree/pull/7343 + - Adjust "required" attribute of Part.category field to be optional + v201 - 2024-05-21 : https://github.com/inventree/InvenTree/pull/7074 - Major refactor of the report template / report printing interface - This is a *breaking change* to the report template API diff --git a/src/backend/InvenTree/InvenTree/metadata.py b/src/backend/InvenTree/InvenTree/metadata.py index 242516f59b..36edbde6b8 100644 --- a/src/backend/InvenTree/InvenTree/metadata.py +++ b/src/backend/InvenTree/InvenTree/metadata.py @@ -115,6 +115,31 @@ class InvenTreeMetadata(SimpleMetadata): return metadata + def override_value(self, field_name, field_value, model_value): + """Override a value on the serializer with a matching value for the model. + + This is used to override the serializer values with model values, + if (and *only* if) the model value should take precedence. + + The values are overridden under the following conditions: + - field_value is None + - model_value is callable, and field_value is not (this indicates that the model value is translated) + - model_value is not a string, and field_value is a string (this indicates that the model value is translated) + """ + if model_value and not field_value: + return model_value + + if field_value and not model_value: + return field_value + + if callable(model_value) and not callable(field_value): + return model_value + + if type(model_value) is not str and type(field_value) is str: + return model_value + + return field_value + def get_serializer_info(self, serializer): """Override get_serializer_info so that we can add 'default' values to any fields whose Meta.model specifies a default value.""" self.serializer = serializer @@ -134,7 +159,12 @@ class InvenTreeMetadata(SimpleMetadata): model_class = None # Attributes to copy extra attributes from the model to the field (if they don't exist) - extra_attributes = ['help_text', 'max_length'] + # Note that the attributes may be named differently on the underlying model! + extra_attributes = { + 'help_text': 'help_text', + 'max_length': 'max_length', + 'label': 'verbose_name', + } try: model_class = serializer.Meta.model @@ -165,10 +195,12 @@ class InvenTreeMetadata(SimpleMetadata): elif name in model_default_values: serializer_info[name]['default'] = model_default_values[name] - for attr in extra_attributes: - if attr not in serializer_info[name]: - if hasattr(field, attr): - serializer_info[name][attr] = getattr(field, attr) + for field_key, model_key in extra_attributes.items(): + field_value = serializer_info[name].get(field_key, None) + model_value = getattr(field, model_key, None) + + if value := self.override_value(name, field_value, model_value): + serializer_info[name][field_key] = value # Iterate through relations for name, relation in model_fields.relations.items(): @@ -186,13 +218,12 @@ class InvenTreeMetadata(SimpleMetadata): relation.model_field.get_limit_choices_to() ) - for attr in extra_attributes: - if attr not in serializer_info[name] and hasattr( - relation.model_field, attr - ): - serializer_info[name][attr] = getattr( - relation.model_field, attr - ) + for field_key, model_key in extra_attributes.items(): + field_value = serializer_info[name].get(field_key, None) + model_value = getattr(relation.model_field, model_key, None) + + if value := self.override_value(name, field_value, model_value): + serializer_info[name][field_key] = value if name in model_default_values: serializer_info[name]['default'] = model_default_values[name] diff --git a/src/backend/InvenTree/part/serializers.py b/src/backend/InvenTree/part/serializers.py index e2182b1635..3df52b22ca 100644 --- a/src/backend/InvenTree/part/serializers.py +++ b/src/backend/InvenTree/part/serializers.py @@ -851,7 +851,9 @@ class PartSerializer( starred = serializers.SerializerMethodField() # PrimaryKeyRelated fields (Note: enforcing field type here results in much faster queries, somehow...) - category = serializers.PrimaryKeyRelatedField(queryset=PartCategory.objects.all()) + category = serializers.PrimaryKeyRelatedField( + queryset=PartCategory.objects.all(), required=False, allow_null=True + ) # Pricing fields pricing_min = InvenTree.serializers.InvenTreeMoneySerializer( diff --git a/src/backend/InvenTree/part/test_api.py b/src/backend/InvenTree/part/test_api.py index e4372f379b..faaacfb0a5 100644 --- a/src/backend/InvenTree/part/test_api.py +++ b/src/backend/InvenTree/part/test_api.py @@ -541,13 +541,58 @@ class PartOptionsAPITest(InvenTreeAPITestCase): category = actions['category'] self.assertEqual(category['type'], 'related field') - self.assertTrue(category['required']) + self.assertFalse(category['required']) self.assertFalse(category['read_only']) self.assertEqual(category['label'], 'Category') self.assertEqual(category['model'], 'partcategory') self.assertEqual(category['api_url'], reverse('api-part-category-list')) self.assertEqual(category['help_text'], 'Part category') + def test_part_label_translation(self): + """Test that 'label' values are correctly translated.""" + response = self.options(reverse('api-part-list')) + + labels = { + 'IPN': 'IPN', + 'category': 'Category', + 'assembly': 'Assembly', + 'ordering': 'On Order', + 'stock_item_count': 'Stock Items', + } + + help_text = { + 'IPN': 'Internal Part Number', + 'active': 'Is this part active?', + 'barcode_hash': 'Unique hash of barcode data', + 'category': 'Part category', + } + + # Check basic return values + for field, value in labels.items(): + self.assertEqual(response.data['actions']['POST'][field]['label'], value) + + for field, value in help_text.items(): + self.assertEqual( + response.data['actions']['POST'][field]['help_text'], value + ) + + # Check again, with a different locale + response = self.options( + reverse('api-part-list'), headers={'Accept-Language': 'de'} + ) + + translated = { + 'IPN': 'IPN (Interne Produktnummer)', + 'category': 'Kategorie', + 'assembly': 'Baugruppe', + 'ordering': 'Bestellt', + 'stock_item_count': 'Lagerartikel', + } + + for field, value in translated.items(): + label = response.data['actions']['POST'][field]['label'] + self.assertEqual(label, value) + def test_category(self): """Test the PartCategory API OPTIONS endpoint.""" actions = self.getActions(reverse('api-part-category-list'))