From 8cd0a3215ca64ef57cfe550df23c73024feb9334 Mon Sep 17 00:00:00 2001 From: Arthur Hanson Date: Mon, 30 Sep 2024 15:26:14 -0700 Subject: [PATCH] 16783 Add status field to InventoryItem (#17627) * 16783 Add status field to InventoryItem * 16783 fix tests * 16783 fix tests * 16783 review changes --- docs/models/dcim/inventoryitem.md | 4 ++++ .../api/serializers_/device_components.py | 3 ++- netbox/dcim/choices.py | 24 +++++++++++++++++++ netbox/dcim/filtersets.py | 6 ++++- netbox/dcim/forms/bulk_edit.py | 8 ++++++- netbox/dcim/forms/bulk_import.py | 7 +++++- netbox/dcim/forms/filtersets.py | 6 ++++- netbox/dcim/forms/model_forms.py | 4 ++-- .../migrations/0191_inventoryitem_status.py | 18 ++++++++++++++ netbox/dcim/models/device_components.py | 11 ++++++++- netbox/dcim/tables/devices.py | 11 +++++---- netbox/dcim/tests/test_filtersets.py | 10 +++++--- netbox/dcim/tests/test_views.py | 10 ++++---- netbox/templates/dcim/inventoryitem.html | 4 ++++ 14 files changed, 107 insertions(+), 19 deletions(-) create mode 100644 netbox/dcim/migrations/0191_inventoryitem_status.py diff --git a/docs/models/dcim/inventoryitem.md b/docs/models/dcim/inventoryitem.md index b9029f75c..a6dfa32db 100644 --- a/docs/models/dcim/inventoryitem.md +++ b/docs/models/dcim/inventoryitem.md @@ -44,3 +44,7 @@ The serial number assigned by the manufacturer. ### Asset Tag A unique, locally-administered label used to identify hardware resources. + +### Status + +The inventory item's operational status. diff --git a/netbox/dcim/api/serializers_/device_components.py b/netbox/dcim/api/serializers_/device_components.py index 60d6561df..db029be7b 100644 --- a/netbox/dcim/api/serializers_/device_components.py +++ b/netbox/dcim/api/serializers_/device_components.py @@ -345,11 +345,12 @@ class InventoryItemSerializer(NetBoxModelSerializer): ) component = serializers.SerializerMethodField(read_only=True, allow_null=True) _depth = serializers.IntegerField(source='level', read_only=True) + status = ChoiceField(choices=InventoryItemStatusChoices, required=False) class Meta: model = InventoryItem fields = [ - 'id', 'url', 'display_url', 'display', 'device', 'parent', 'name', 'label', 'role', 'manufacturer', + 'id', 'url', 'display_url', 'display', 'device', 'parent', 'name', 'label', 'status', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered', 'description', 'component_type', 'component_id', 'component', 'tags', 'custom_fields', 'created', 'last_updated', '_depth', ] diff --git a/netbox/dcim/choices.py b/netbox/dcim/choices.py index 848f57d7e..89ca76870 100644 --- a/netbox/dcim/choices.py +++ b/netbox/dcim/choices.py @@ -1648,3 +1648,27 @@ class VirtualDeviceContextStatusChoices(ChoiceSet): (STATUS_PLANNED, _('Planned'), 'cyan'), (STATUS_OFFLINE, _('Offline'), 'red'), ] + + +# +# InventoryItem +# + +class InventoryItemStatusChoices(ChoiceSet): + key = 'InventoryItem.status' + + STATUS_OFFLINE = 'offline' + STATUS_ACTIVE = 'active' + STATUS_PLANNED = 'planned' + STATUS_STAGED = 'staged' + STATUS_FAILED = 'failed' + STATUS_DECOMMISSIONING = 'decommissioning' + + CHOICES = [ + (STATUS_OFFLINE, _('Offline'), 'gray'), + (STATUS_ACTIVE, _('Active'), 'green'), + (STATUS_PLANNED, _('Planned'), 'cyan'), + (STATUS_STAGED, _('Staged'), 'blue'), + (STATUS_FAILED, _('Failed'), 'red'), + (STATUS_DECOMMISSIONING, _('Decommissioning'), 'yellow'), + ] diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py index 6517aadb4..c51703c93 100644 --- a/netbox/dcim/filtersets.py +++ b/netbox/dcim/filtersets.py @@ -1860,10 +1860,14 @@ class InventoryItemFilterSet(DeviceComponentFilterSet, NetBoxModelFilterSet): serial = MultiValueCharFilter( lookup_expr='iexact' ) + status = django_filters.MultipleChoiceFilter( + choices=InventoryItemStatusChoices, + null_value=None + ) class Meta: model = InventoryItem - fields = ('id', 'name', 'label', 'part_id', 'asset_tag', 'description', 'discovered') + fields = ('id', 'name', 'label', 'part_id', 'asset_tag', 'status', 'description', 'discovered') def search(self, queryset, name, value): if not value.strip(): diff --git a/netbox/dcim/forms/bulk_edit.py b/netbox/dcim/forms/bulk_edit.py index 96036f4da..1224f6aea 100644 --- a/netbox/dcim/forms/bulk_edit.py +++ b/netbox/dcim/forms/bulk_edit.py @@ -1661,10 +1661,16 @@ class InventoryItemBulkEditForm( queryset=Manufacturer.objects.all(), required=False ) + status = forms.ChoiceField( + label=_('Status'), + choices=add_blank_choice(InventoryItemStatusChoices), + required=False, + initial='' + ) model = InventoryItem fieldsets = ( - FieldSet('device', 'label', 'role', 'manufacturer', 'part_id', 'description'), + FieldSet('device', 'label', 'role', 'manufacturer', 'part_id', 'status', 'description'), ) nullable_fields = ('label', 'role', 'manufacturer', 'part_id', 'description') diff --git a/netbox/dcim/forms/bulk_import.py b/netbox/dcim/forms/bulk_import.py index e9c8b362e..f1639099e 100644 --- a/netbox/dcim/forms/bulk_import.py +++ b/netbox/dcim/forms/bulk_import.py @@ -1103,11 +1103,16 @@ class InventoryItemImportForm(NetBoxModelImportForm): required=False, help_text=_('Component Name') ) + status = CSVChoiceField( + label=_('Status'), + choices=InventoryItemStatusChoices, + help_text=_('Operational status') + ) class Meta: model = InventoryItem fields = ( - 'device', 'name', 'label', 'role', 'manufacturer', 'parent', 'part_id', 'serial', 'asset_tag', 'discovered', + 'device', 'name', 'label', 'status', 'role', 'manufacturer', 'parent', 'part_id', 'serial', 'asset_tag', 'discovered', 'description', 'tags', 'component_type', 'component_name', ) diff --git a/netbox/dcim/forms/filtersets.py b/netbox/dcim/forms/filtersets.py index e2b6fda07..e6ffe0906 100644 --- a/netbox/dcim/forms/filtersets.py +++ b/netbox/dcim/forms/filtersets.py @@ -35,7 +35,6 @@ __all__ = ( 'LocationFilterForm', 'ManufacturerFilterForm', 'ModuleFilterForm', - 'ModuleFilterForm', 'ModuleBayFilterForm', 'ModuleTypeFilterForm', 'PlatformFilterForm', @@ -1553,6 +1552,11 @@ class InventoryItemFilterForm(DeviceComponentFilterForm): choices=BOOLEAN_WITH_BLANK_CHOICES ) ) + status = forms.MultipleChoiceField( + label=_('Status'), + choices=InventoryItemStatusChoices, + required=False + ) tag = TagFilterField(model) diff --git a/netbox/dcim/forms/model_forms.py b/netbox/dcim/forms/model_forms.py index 4f08ea896..df4520ad3 100644 --- a/netbox/dcim/forms/model_forms.py +++ b/netbox/dcim/forms/model_forms.py @@ -1576,7 +1576,7 @@ class InventoryItemForm(DeviceComponentForm): ) fieldsets = ( - FieldSet('device', 'parent', 'name', 'label', 'role', 'description', 'tags', name=_('Inventory Item')), + FieldSet('device', 'parent', 'name', 'label', 'status', 'role', 'description', 'tags', name=_('Inventory Item')), FieldSet('manufacturer', 'part_id', 'serial', 'asset_tag', name=_('Hardware')), FieldSet( TabbedGroups( @@ -1596,7 +1596,7 @@ class InventoryItemForm(DeviceComponentForm): model = InventoryItem fields = [ 'device', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', - 'description', 'tags', + 'status', 'description', 'tags', ] def __init__(self, *args, **kwargs): diff --git a/netbox/dcim/migrations/0191_inventoryitem_status.py b/netbox/dcim/migrations/0191_inventoryitem_status.py new file mode 100644 index 000000000..23daf903d --- /dev/null +++ b/netbox/dcim/migrations/0191_inventoryitem_status.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.9 on 2024-09-26 20:19 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dcim', '0190_nested_modules'), + ] + + operations = [ + migrations.AddField( + model_name='inventoryitem', + name='status', + field=models.CharField(default='active', max_length=50), + ), + ] diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index f5fbaa956..9f18032e5 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -1244,6 +1244,12 @@ class InventoryItem(MPTTModel, ComponentModel, TrackingModelMixin): ct_field='component_type', fk_field='component_id' ) + status = models.CharField( + verbose_name=_('status'), + max_length=50, + choices=InventoryItemStatusChoices, + default=InventoryItemStatusChoices.STATUS_ACTIVE + ) role = models.ForeignKey( to='dcim.InventoryItemRole', on_delete=models.PROTECT, @@ -1285,7 +1291,7 @@ class InventoryItem(MPTTModel, ComponentModel, TrackingModelMixin): objects = TreeManager() - clone_fields = ('device', 'parent', 'role', 'manufacturer', 'part_id',) + clone_fields = ('device', 'parent', 'role', 'manufacturer', 'status', 'part_id') class Meta: ordering = ('device__id', 'parent__id', '_name') @@ -1334,3 +1340,6 @@ class InventoryItem(MPTTModel, ComponentModel, TrackingModelMixin): raise ValidationError({ "device": _("Cannot assign inventory item to component on another device") }) + + def get_status_color(self): + return InventoryItemStatusChoices.colors.get(self.status) diff --git a/netbox/dcim/tables/devices.py b/netbox/dcim/tables/devices.py index 2fa82dc43..efa5efddb 100644 --- a/netbox/dcim/tables/devices.py +++ b/netbox/dcim/tables/devices.py @@ -946,6 +946,9 @@ class InventoryItemTable(DeviceComponentTable): verbose_name=_('Discovered'), false_mark=None ) + status = columns.ChoiceFieldColumn( + verbose_name=_('Status'), + ) parent = tables.Column( linkify=True, verbose_name=_('Parent'), @@ -958,11 +961,11 @@ class InventoryItemTable(DeviceComponentTable): class Meta(NetBoxTable.Meta): model = models.InventoryItem fields = ( - 'pk', 'id', 'name', 'device', 'parent', 'component', 'label', 'role', 'manufacturer', 'part_id', 'serial', + 'pk', 'id', 'name', 'device', 'parent', 'component', 'label', 'status', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description', 'discovered', 'tags', 'created', 'last_updated', ) default_columns = ( - 'pk', 'name', 'device', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', + 'pk', 'name', 'device', 'label', 'status', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', ) @@ -978,11 +981,11 @@ class DeviceInventoryItemTable(InventoryItemTable): class Meta(NetBoxTable.Meta): model = models.InventoryItem fields = ( - 'pk', 'id', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'component', + 'pk', 'id', 'name', 'label', 'status', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'component', 'description', 'discovered', 'tags', 'actions', ) default_columns = ( - 'pk', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'component', + 'pk', 'name', 'label', 'status', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'component', ) diff --git a/netbox/dcim/tests/test_filtersets.py b/netbox/dcim/tests/test_filtersets.py index afb360d76..714b1af9c 100644 --- a/netbox/dcim/tests/test_filtersets.py +++ b/netbox/dcim/tests/test_filtersets.py @@ -4751,9 +4751,9 @@ class InventoryItemTestCase(TestCase, ChangeLoggedFilterSetTests): ) inventory_items = ( - InventoryItem(device=devices[0], role=roles[0], manufacturer=manufacturers[0], name='Inventory Item 1', label='A', part_id='1001', serial='ABC', asset_tag='1001', discovered=True, description='First', component=components[0]), - InventoryItem(device=devices[1], role=roles[1], manufacturer=manufacturers[1], name='Inventory Item 2', label='B', part_id='1002', serial='DEF', asset_tag='1002', discovered=True, description='Second', component=components[1]), - InventoryItem(device=devices[2], role=roles[2], manufacturer=manufacturers[2], name='Inventory Item 3', label='C', part_id='1003', serial='GHI', asset_tag='1003', discovered=False, description='Third', component=components[2]), + InventoryItem(device=devices[0], role=roles[0], manufacturer=manufacturers[0], name='Inventory Item 1', label='A', part_id='1001', serial='ABC', asset_tag='1001', discovered=True, status=ModuleStatusChoices.STATUS_ACTIVE, description='First', component=components[0]), + InventoryItem(device=devices[1], role=roles[1], manufacturer=manufacturers[1], name='Inventory Item 2', label='B', part_id='1002', serial='DEF', asset_tag='1002', discovered=True, status=ModuleStatusChoices.STATUS_PLANNED, description='Second', component=components[1]), + InventoryItem(device=devices[2], role=roles[2], manufacturer=manufacturers[2], name='Inventory Item 3', label='C', part_id='1003', serial='GHI', asset_tag='1003', discovered=False, status=ModuleStatusChoices.STATUS_FAILED, description='Third', component=components[2]), ) for i in inventory_items: i.save() @@ -4881,6 +4881,10 @@ class InventoryItemTestCase(TestCase, ChangeLoggedFilterSetTests): params = {'component_type': 'dcim.interface'} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + def test_status(self): + params = {'status': [InventoryItemStatusChoices.STATUS_PLANNED, InventoryItemStatusChoices.STATUS_FAILED]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + class InventoryItemRoleTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = InventoryItemRole.objects.all() diff --git a/netbox/dcim/tests/test_views.py b/netbox/dcim/tests/test_views.py index 7d6c34337..a87d92642 100644 --- a/netbox/dcim/tests/test_views.py +++ b/netbox/dcim/tests/test_views.py @@ -2903,6 +2903,7 @@ class InventoryItemTestCase(ViewTestCases.DeviceComponentViewTestCase): 'part_id': '123456', 'serial': '123ABC', 'asset_tag': 'ABC123', + 'status': InventoryItemStatusChoices.STATUS_ACTIVE, 'description': 'An inventory item', 'tags': [t.pk for t in tags], } @@ -2916,6 +2917,7 @@ class InventoryItemTestCase(ViewTestCases.DeviceComponentViewTestCase): 'discovered': False, 'part_id': '123456', 'serial': '123ABC', + 'status': InventoryItemStatusChoices.STATUS_ACTIVE, 'description': 'An inventory item', 'tags': [t.pk for t in tags], } @@ -2927,10 +2929,10 @@ class InventoryItemTestCase(ViewTestCases.DeviceComponentViewTestCase): } cls.csv_data = ( - "device,name,parent", - "Device 1,Inventory Item 4,Inventory Item 1", - "Device 1,Inventory Item 5,Inventory Item 2", - "Device 1,Inventory Item 6,Inventory Item 3", + "device,name,parent,status", + "Device 1,Inventory Item 4,Inventory Item 1,active", + "Device 1,Inventory Item 5,Inventory Item 2,planned", + "Device 1,Inventory Item 6,Inventory Item 3,failed", ) cls.csv_update_data = ( diff --git a/netbox/templates/dcim/inventoryitem.html b/netbox/templates/dcim/inventoryitem.html index 44648d53e..f17bf2ade 100644 --- a/netbox/templates/dcim/inventoryitem.html +++ b/netbox/templates/dcim/inventoryitem.html @@ -56,6 +56,10 @@ {% trans "Asset Tag" %} {{ object.asset_tag|placeholder }} + + {% trans "Status" %} + {% badge object.get_status_display bg_color=object.get_status_color %} + {% trans "Description" %} {{ object.description|placeholder }}