Просмотр исходного кода

16783 Add status field to InventoryItem (#17627)

* 16783 Add status field to InventoryItem

* 16783 fix tests

* 16783 fix tests

* 16783 review changes
Arthur Hanson 1 год назад
Родитель
Сommit
8cd0a3215c

+ 4 - 0
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.

+ 2 - 1
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',
         ]

+ 24 - 0
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'),
+    ]

+ 5 - 1
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():

+ 7 - 1
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')
 

+ 6 - 1
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',
         )
 

+ 5 - 1
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)
 
 

+ 2 - 2
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):

+ 18 - 0
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),
+        ),
+    ]

+ 10 - 1
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)

+ 7 - 4
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',
         )
 
 

+ 7 - 3
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()

+ 6 - 4
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 = (

+ 4 - 0
netbox/templates/dcim/inventoryitem.html

@@ -56,6 +56,10 @@
                         <th scope="row">{% trans "Asset Tag" %}</th>
                         <td>{{ object.asset_tag|placeholder }}</td>
                     </tr>
+                    <tr>
+                      <th scope="row">{% trans "Status" %}</th>
+                      <td>{% badge object.get_status_display bg_color=object.get_status_color %}</td>
+                    </tr>
                     <tr>
                         <th scope="row">{% trans "Description" %}</th>
                         <td>{{ object.description|placeholder }}</td>