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
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)
_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',
]

View File

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

View File

@ -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():

View File

@ -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')

View File

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

View File

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

View File

@ -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):

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

View File

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

View File

@ -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()

View File

@ -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 = (

View File

@ -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>