16783 Add status field to InventoryItem (#17627)

* 16783 Add status field to InventoryItem

* 16783 fix tests

* 16783 fix tests

* 16783 review changes
This commit is contained in:
Arthur Hanson 2024-09-30 15:26:14 -07:00 committed by GitHub
parent c60a0f4f56
commit 8cd0a3215c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 107 additions and 19 deletions

View File

@ -44,3 +44,7 @@ The serial number assigned by the manufacturer.
### Asset Tag ### Asset Tag
A unique, locally-administered label used to identify hardware resources. A unique, locally-administered label used to identify hardware resources.
### Status
The inventory item's operational status.

View File

@ -345,11 +345,12 @@ class InventoryItemSerializer(NetBoxModelSerializer):
) )
component = serializers.SerializerMethodField(read_only=True, allow_null=True) component = serializers.SerializerMethodField(read_only=True, allow_null=True)
_depth = serializers.IntegerField(source='level', read_only=True) _depth = serializers.IntegerField(source='level', read_only=True)
status = ChoiceField(choices=InventoryItemStatusChoices, required=False)
class Meta: class Meta:
model = InventoryItem model = InventoryItem
fields = [ 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', 'part_id', 'serial', 'asset_tag', 'discovered', 'description', 'component_type', 'component_id',
'component', 'tags', 'custom_fields', 'created', 'last_updated', '_depth', 'component', 'tags', 'custom_fields', 'created', 'last_updated', '_depth',
] ]

View File

@ -1648,3 +1648,27 @@ class VirtualDeviceContextStatusChoices(ChoiceSet):
(STATUS_PLANNED, _('Planned'), 'cyan'), (STATUS_PLANNED, _('Planned'), 'cyan'),
(STATUS_OFFLINE, _('Offline'), 'red'), (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'),
]

View File

@ -1860,10 +1860,14 @@ class InventoryItemFilterSet(DeviceComponentFilterSet, NetBoxModelFilterSet):
serial = MultiValueCharFilter( serial = MultiValueCharFilter(
lookup_expr='iexact' lookup_expr='iexact'
) )
status = django_filters.MultipleChoiceFilter(
choices=InventoryItemStatusChoices,
null_value=None
)
class Meta: class Meta:
model = InventoryItem 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): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():

View File

@ -1661,10 +1661,16 @@ class InventoryItemBulkEditForm(
queryset=Manufacturer.objects.all(), queryset=Manufacturer.objects.all(),
required=False required=False
) )
status = forms.ChoiceField(
label=_('Status'),
choices=add_blank_choice(InventoryItemStatusChoices),
required=False,
initial=''
)
model = InventoryItem model = InventoryItem
fieldsets = ( 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') nullable_fields = ('label', 'role', 'manufacturer', 'part_id', 'description')

View File

@ -1103,11 +1103,16 @@ class InventoryItemImportForm(NetBoxModelImportForm):
required=False, required=False,
help_text=_('Component Name') help_text=_('Component Name')
) )
status = CSVChoiceField(
label=_('Status'),
choices=InventoryItemStatusChoices,
help_text=_('Operational status')
)
class Meta: class Meta:
model = InventoryItem model = InventoryItem
fields = ( 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', 'description', 'tags', 'component_type', 'component_name',
) )

View File

@ -35,7 +35,6 @@ __all__ = (
'LocationFilterForm', 'LocationFilterForm',
'ManufacturerFilterForm', 'ManufacturerFilterForm',
'ModuleFilterForm', 'ModuleFilterForm',
'ModuleFilterForm',
'ModuleBayFilterForm', 'ModuleBayFilterForm',
'ModuleTypeFilterForm', 'ModuleTypeFilterForm',
'PlatformFilterForm', 'PlatformFilterForm',
@ -1553,6 +1552,11 @@ class InventoryItemFilterForm(DeviceComponentFilterForm):
choices=BOOLEAN_WITH_BLANK_CHOICES choices=BOOLEAN_WITH_BLANK_CHOICES
) )
) )
status = forms.MultipleChoiceField(
label=_('Status'),
choices=InventoryItemStatusChoices,
required=False
)
tag = TagFilterField(model) tag = TagFilterField(model)

View File

@ -1576,7 +1576,7 @@ class InventoryItemForm(DeviceComponentForm):
) )
fieldsets = ( 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('manufacturer', 'part_id', 'serial', 'asset_tag', name=_('Hardware')),
FieldSet( FieldSet(
TabbedGroups( TabbedGroups(
@ -1596,7 +1596,7 @@ class InventoryItemForm(DeviceComponentForm):
model = InventoryItem model = InventoryItem
fields = [ fields = [
'device', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'device', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag',
'description', 'tags', 'status', 'description', 'tags',
] ]
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):

View File

@ -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),
),
]

View File

@ -1244,6 +1244,12 @@ class InventoryItem(MPTTModel, ComponentModel, TrackingModelMixin):
ct_field='component_type', ct_field='component_type',
fk_field='component_id' fk_field='component_id'
) )
status = models.CharField(
verbose_name=_('status'),
max_length=50,
choices=InventoryItemStatusChoices,
default=InventoryItemStatusChoices.STATUS_ACTIVE
)
role = models.ForeignKey( role = models.ForeignKey(
to='dcim.InventoryItemRole', to='dcim.InventoryItemRole',
on_delete=models.PROTECT, on_delete=models.PROTECT,
@ -1285,7 +1291,7 @@ class InventoryItem(MPTTModel, ComponentModel, TrackingModelMixin):
objects = TreeManager() objects = TreeManager()
clone_fields = ('device', 'parent', 'role', 'manufacturer', 'part_id',) clone_fields = ('device', 'parent', 'role', 'manufacturer', 'status', 'part_id')
class Meta: class Meta:
ordering = ('device__id', 'parent__id', '_name') ordering = ('device__id', 'parent__id', '_name')
@ -1334,3 +1340,6 @@ class InventoryItem(MPTTModel, ComponentModel, TrackingModelMixin):
raise ValidationError({ raise ValidationError({
"device": _("Cannot assign inventory item to component on another device") "device": _("Cannot assign inventory item to component on another device")
}) })
def get_status_color(self):
return InventoryItemStatusChoices.colors.get(self.status)

View File

@ -946,6 +946,9 @@ class InventoryItemTable(DeviceComponentTable):
verbose_name=_('Discovered'), verbose_name=_('Discovered'),
false_mark=None false_mark=None
) )
status = columns.ChoiceFieldColumn(
verbose_name=_('Status'),
)
parent = tables.Column( parent = tables.Column(
linkify=True, linkify=True,
verbose_name=_('Parent'), verbose_name=_('Parent'),
@ -958,11 +961,11 @@ class InventoryItemTable(DeviceComponentTable):
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = models.InventoryItem model = models.InventoryItem
fields = ( 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', 'asset_tag', 'description', 'discovered', 'tags', 'created', 'last_updated',
) )
default_columns = ( 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): class Meta(NetBoxTable.Meta):
model = models.InventoryItem model = models.InventoryItem
fields = ( 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', 'description', 'discovered', 'tags', 'actions',
) )
default_columns = ( 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',
) )

View File

@ -4751,9 +4751,9 @@ class InventoryItemTestCase(TestCase, ChangeLoggedFilterSetTests):
) )
inventory_items = ( 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[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, description='Second', component=components[1]), 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, description='Third', component=components[2]), 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: for i in inventory_items:
i.save() i.save()
@ -4881,6 +4881,10 @@ class InventoryItemTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'component_type': 'dcim.interface'} params = {'component_type': 'dcim.interface'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) 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): class InventoryItemRoleTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = InventoryItemRole.objects.all() queryset = InventoryItemRole.objects.all()

View File

@ -2903,6 +2903,7 @@ class InventoryItemTestCase(ViewTestCases.DeviceComponentViewTestCase):
'part_id': '123456', 'part_id': '123456',
'serial': '123ABC', 'serial': '123ABC',
'asset_tag': 'ABC123', 'asset_tag': 'ABC123',
'status': InventoryItemStatusChoices.STATUS_ACTIVE,
'description': 'An inventory item', 'description': 'An inventory item',
'tags': [t.pk for t in tags], 'tags': [t.pk for t in tags],
} }
@ -2916,6 +2917,7 @@ class InventoryItemTestCase(ViewTestCases.DeviceComponentViewTestCase):
'discovered': False, 'discovered': False,
'part_id': '123456', 'part_id': '123456',
'serial': '123ABC', 'serial': '123ABC',
'status': InventoryItemStatusChoices.STATUS_ACTIVE,
'description': 'An inventory item', 'description': 'An inventory item',
'tags': [t.pk for t in tags], 'tags': [t.pk for t in tags],
} }
@ -2927,10 +2929,10 @@ class InventoryItemTestCase(ViewTestCases.DeviceComponentViewTestCase):
} }
cls.csv_data = ( cls.csv_data = (
"device,name,parent", "device,name,parent,status",
"Device 1,Inventory Item 4,Inventory Item 1", "Device 1,Inventory Item 4,Inventory Item 1,active",
"Device 1,Inventory Item 5,Inventory Item 2", "Device 1,Inventory Item 5,Inventory Item 2,planned",
"Device 1,Inventory Item 6,Inventory Item 3", "Device 1,Inventory Item 6,Inventory Item 3,failed",
) )
cls.csv_update_data = ( cls.csv_update_data = (

View File

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