16783 Add status field to InventoryItem

This commit is contained in:
Arthur Hanson 2024-09-26 13:41:18 -07:00
parent 85396866bc
commit 741e9a82a1
13 changed files with 99 additions and 13 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,12 +345,13 @@ 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',
'part_id', 'serial', 'asset_tag', 'discovered', 'description', 'component_type', 'component_id',
'part_id', 'serial', 'asset_tag', 'discovered', 'description', 'component_type', 'component_id', 'status',
'component', 'tags', 'custom_fields', 'created', 'last_updated', '_depth',
]
brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', '_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,12 +1103,17 @@ 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',
'description', 'tags', 'component_type', 'component_name',
'status', 'description', 'tags', 'component_type', 'component_name',
)
def __init__(self, *args, **kwargs):

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

@ -1577,7 +1577,7 @@ class InventoryItemForm(DeviceComponentForm):
fieldsets = (
FieldSet('device', 'parent', 'name', 'label', 'role', 'description', 'tags', name=_('Inventory Item')),
FieldSet('manufacturer', 'part_id', 'serial', 'asset_tag', name=_('Hardware')),
FieldSet('manufacturer', 'part_id', 'serial', 'asset_tag', 'status', name=_('Hardware')),
FieldSet(
TabbedGroups(
FieldSet('interface', name=_('Interface')),
@ -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

@ -1282,10 +1282,16 @@ class InventoryItem(MPTTModel, ComponentModel, TrackingModelMixin):
default=False,
help_text=_('This item was automatically discovered')
)
status = models.CharField(
verbose_name=_('status'),
max_length=50,
choices=InventoryItemStatusChoices,
default=InventoryItemStatusChoices.STATUS_ACTIVE
)
objects = TreeManager()
clone_fields = ('device', 'parent', 'role', 'manufacturer', 'part_id',)
clone_fields = ('device', 'parent', 'role', 'manufacturer', 'part_id', 'status')
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'),
@ -959,10 +962,10 @@ class InventoryItemTable(DeviceComponentTable):
model = models.InventoryItem
fields = (
'pk', 'id', 'name', 'device', 'parent', 'component', 'label', 'role', 'manufacturer', 'part_id', 'serial',
'asset_tag', 'description', 'discovered', 'tags', 'created', 'last_updated',
'asset_tag', 'status', 'description', 'discovered', 'tags', 'created', 'last_updated',
)
default_columns = (
'pk', 'name', 'device', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag',
'pk', 'name', 'device', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'status',
)

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

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