#8037: Add role field to InventoryItem

This commit is contained in:
jeremystretch 2021-12-27 10:45:33 -05:00
parent 04fb5e544d
commit 6e9afccfd7
16 changed files with 141 additions and 65 deletions

View File

@ -2,6 +2,6 @@
Inventory items represent hardware components installed within a device, such as a power supply or CPU or line card. Inventory items are distinct from other device components in that they cannot be templatized on a device type, and cannot be connected by cables. They are intended to be used primarily for inventory purposes.
Each inventory item can be assigned a manufacturer, part ID, serial number, and asset tag (all optional). A boolean toggle is also provided to indicate whether each item was entered manually or discovered automatically (by some process outside of NetBox).
Each inventory item can be assigned a functional role, manufacturer, part ID, serial number, and asset tag (all optional). A boolean toggle is also provided to indicate whether each item was entered manually or discovered automatically (by some process outside of NetBox).
Inventory items are hierarchical in nature, such that any individual item may be designated as the parent for other items. For example, an inventory item might be created to represent a line card which houses several SFP optics, each of which exists as a child item within the device.

View File

@ -811,12 +811,13 @@ class InventoryItemSerializer(PrimaryModelSerializer):
device = NestedDeviceSerializer()
parent = serializers.PrimaryKeyRelatedField(queryset=InventoryItem.objects.all(), allow_null=True, default=None)
manufacturer = NestedManufacturerSerializer(required=False, allow_null=True, default=None)
role = NestedInventoryItemRoleSerializer(required=False, allow_null=True)
_depth = serializers.IntegerField(source='level', read_only=True)
class Meta:
model = InventoryItem
fields = [
'id', 'url', 'display', 'device', 'parent', 'name', 'label', 'manufacturer', 'part_id', 'serial',
'id', 'url', 'display', 'device', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial',
'asset_tag', 'discovered', 'description', 'tags', 'custom_fields', 'created', 'last_updated', '_depth',
]

View File

@ -1284,6 +1284,16 @@ class InventoryItemFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet):
to_field_name='slug',
label='Manufacturer (slug)',
)
role_id = django_filters.ModelMultipleChoiceFilter(
queryset=InventoryItemRole.objects.all(),
label='Role (ID)',
)
role = django_filters.ModelMultipleChoiceFilter(
field_name='role__slug',
queryset=InventoryItemRole.objects.all(),
to_field_name='slug',
label='Role (slug)',
)
serial = django_filters.CharFilter(
lookup_expr='iexact'
)

View File

@ -107,11 +107,11 @@ class DeviceBayBulkCreateForm(DeviceBulkAddComponentForm):
class InventoryItemBulkCreateForm(
form_from_model(InventoryItem, ['manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered']),
form_from_model(InventoryItem, ['role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered']),
DeviceBulkAddComponentForm
):
model = InventoryItem
field_order = (
'name_pattern', 'label_pattern', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered', 'description',
'tags',
'name_pattern', 'label_pattern', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered',
'description', 'tags',
)

View File

@ -1172,7 +1172,7 @@ class DeviceBayBulkEditForm(
class InventoryItemBulkEditForm(
form_from_model(InventoryItem, ['label', 'manufacturer', 'part_id', 'description']),
form_from_model(InventoryItem, ['label', 'role', 'manufacturer', 'part_id', 'description']),
AddRemoveTagsForm,
CustomFieldModelBulkEditForm
):
@ -1180,13 +1180,17 @@ class InventoryItemBulkEditForm(
queryset=InventoryItem.objects.all(),
widget=forms.MultipleHiddenInput()
)
role = DynamicModelChoiceField(
queryset=InventoryItemRole.objects.all(),
required=False
)
manufacturer = DynamicModelChoiceField(
queryset=Manufacturer.objects.all(),
required=False
)
class Meta:
nullable_fields = ['label', 'manufacturer', 'part_id', 'description']
nullable_fields = ['label', 'role', 'manufacturer', 'part_id', 'description']
#

View File

@ -772,6 +772,11 @@ class InventoryItemCSVForm(CustomFieldModelCSVForm):
queryset=Device.objects.all(),
to_field_name='name'
)
role = CSVModelChoiceField(
queryset=InventoryItemRole.objects.all(),
to_field_name='name',
required=False
)
manufacturer = CSVModelChoiceField(
queryset=Manufacturer.objects.all(),
to_field_name='name',
@ -787,7 +792,8 @@ class InventoryItemCSVForm(CustomFieldModelCSVForm):
class Meta:
model = InventoryItem
fields = (
'device', 'name', 'label', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered', 'description',
'device', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered',
'description',
)
def __init__(self, *args, **kwargs):

View File

@ -1100,6 +1100,12 @@ class InventoryItemFilterForm(DeviceComponentFilterForm):
['name', 'label', 'manufacturer_id', 'serial', 'asset_tag', 'discovered'],
['region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id'],
]
role_id = DynamicModelMultipleChoiceField(
queryset=InventoryItemRole.objects.all(),
required=False,
label=_('Role'),
fetch_trigger='open'
)
manufacturer_id = DynamicModelMultipleChoiceField(
queryset=Manufacturer.objects.all(),
required=False,

View File

@ -1368,6 +1368,10 @@ class InventoryItemForm(CustomFieldModelForm):
'device_id': '$device'
}
)
role = DynamicModelChoiceField(
queryset=InventoryItemRole.objects.all(),
required=False
)
manufacturer = DynamicModelChoiceField(
queryset=Manufacturer.objects.all(),
required=False
@ -1380,8 +1384,8 @@ class InventoryItemForm(CustomFieldModelForm):
class Meta:
model = InventoryItem
fields = [
'device', 'parent', 'name', 'label', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description',
'tags',
'device', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag',
'description', 'tags',
]

View File

@ -652,10 +652,6 @@ class DeviceBayCreateForm(ComponentCreateForm):
class InventoryItemCreateForm(ComponentCreateForm):
model = InventoryItem
manufacturer = DynamicModelChoiceField(
queryset=Manufacturer.objects.all(),
required=False
)
parent = DynamicModelChoiceField(
queryset=InventoryItem.objects.all(),
required=False,
@ -663,6 +659,14 @@ class InventoryItemCreateForm(ComponentCreateForm):
'device_id': '$device'
}
)
role = DynamicModelChoiceField(
queryset=InventoryItemRole.objects.all(),
required=False
)
manufacturer = DynamicModelChoiceField(
queryset=Manufacturer.objects.all(),
required=False
)
part_id = forms.CharField(
max_length=50,
required=False,
@ -677,6 +681,6 @@ class InventoryItemCreateForm(ComponentCreateForm):
required=False,
)
field_order = (
'device', 'parent', 'name_pattern', 'label_pattern', 'manufacturer', 'part_id', 'serial', 'asset_tag',
'device', 'parent', 'name_pattern', 'label_pattern', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag',
'description', 'tags',
)

View File

@ -994,6 +994,13 @@ class InventoryItem(MPTTModel, ComponentModel):
null=True,
db_index=True
)
role = models.ForeignKey(
to='dcim.InventoryItemRole',
on_delete=models.PROTECT,
related_name='inventory_items',
blank=True,
null=True
)
manufacturer = models.ForeignKey(
to='dcim.Manufacturer',
on_delete=models.PROTECT,
@ -1007,13 +1014,6 @@ class InventoryItem(MPTTModel, ComponentModel):
blank=True,
help_text='Manufacturer-assigned part identifier'
)
role = models.ForeignKey(
to='dcim.InventoryItemRole',
on_delete=models.PROTECT,
related_name='inventory_items',
blank=True,
null=True
)
serial = models.CharField(
max_length=50,
verbose_name='Serial number',
@ -1034,7 +1034,7 @@ class InventoryItem(MPTTModel, ComponentModel):
objects = TreeManager()
clone_fields = ['device', 'parent', 'manufacturer', 'part_id', 'role']
clone_fields = ['device', 'parent', 'role', 'manufacturer', 'part_id']
class Meta:
ordering = ('device__id', 'parent__id', '_name')

View File

@ -774,6 +774,9 @@ class InventoryItemTable(DeviceComponentTable):
'args': [Accessor('device_id')],
}
)
role = tables.Column(
linkify=True
)
manufacturer = tables.Column(
linkify=True
)
@ -786,10 +789,33 @@ class InventoryItemTable(DeviceComponentTable):
class Meta(BaseTable.Meta):
model = InventoryItem
fields = (
'pk', 'id', 'name', 'device', 'label', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description',
'discovered', 'tags',
'pk', 'id', 'name', 'device', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag',
'description', 'discovered', 'tags',
)
default_columns = ('pk', 'name', 'device', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag')
class DeviceInventoryItemTable(InventoryItemTable):
name = tables.TemplateColumn(
template_code='<a href="{{ record.get_absolute_url }}" style="padding-left: {{ record.level }}0px">'
'{{ value }}</a>',
order_by=Accessor('_name'),
attrs={'td': {'class': 'text-nowrap'}}
)
actions = ButtonsColumn(
model=InventoryItem,
buttons=('edit', 'delete')
)
class Meta(BaseTable.Meta):
model = InventoryItem
fields = (
'pk', 'id', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description',
'discovered', 'tags', 'actions',
)
default_columns = (
'pk', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description', 'actions',
)
default_columns = ('pk', 'name', 'device', 'label', 'manufacturer', 'part_id', 'serial', 'asset_tag')
class InventoryItemRoleTable(BaseTable):
@ -816,30 +842,6 @@ class InventoryItemRoleTable(BaseTable):
default_columns = ('pk', 'name', 'inventoryitem_count', 'color', 'description', 'actions')
class DeviceInventoryItemTable(InventoryItemTable):
name = tables.TemplateColumn(
template_code='<a href="{{ record.get_absolute_url }}" style="padding-left: {{ record.level }}0px">'
'{{ value }}</a>',
order_by=Accessor('_name'),
attrs={'td': {'class': 'text-nowrap'}}
)
actions = ButtonsColumn(
model=InventoryItem,
buttons=('edit', 'delete')
)
class Meta(BaseTable.Meta):
model = InventoryItem
fields = (
'pk', 'id', 'name', 'label', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description', 'discovered',
'tags', 'actions',
)
default_columns = (
'pk', 'name', 'label', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description', 'discovered',
'actions',
)
#
# Virtual chassis
#

View File

@ -1626,24 +1626,33 @@ class InventoryItemTest(APIViewTestCases.APIViewTestCase):
devicerole = DeviceRole.objects.create(name='Test Device Role 1', slug='test-device-role-1', color='ff0000')
device = Device.objects.create(device_type=devicetype, device_role=devicerole, name='Device 1', site=site)
InventoryItem.objects.create(device=device, name='Inventory Item 1', manufacturer=manufacturer)
InventoryItem.objects.create(device=device, name='Inventory Item 2', manufacturer=manufacturer)
InventoryItem.objects.create(device=device, name='Inventory Item 3', manufacturer=manufacturer)
roles = (
InventoryItemRole(name='Inventory Item Role 1', slug='inventory-item-role-1'),
InventoryItemRole(name='Inventory Item Role 2', slug='inventory-item-role-2'),
)
InventoryItemRole.objects.bulk_create(roles)
InventoryItem.objects.create(device=device, name='Inventory Item 1', role=roles[0], manufacturer=manufacturer)
InventoryItem.objects.create(device=device, name='Inventory Item 2', role=roles[0], manufacturer=manufacturer)
InventoryItem.objects.create(device=device, name='Inventory Item 3', role=roles[0], manufacturer=manufacturer)
cls.create_data = [
{
'device': device.pk,
'name': 'Inventory Item 4',
'role': roles[1].pk,
'manufacturer': manufacturer.pk,
},
{
'device': device.pk,
'name': 'Inventory Item 5',
'role': roles[1].pk,
'manufacturer': manufacturer.pk,
},
{
'device': device.pk,
'name': 'Inventory Item 6',
'role': roles[1].pk,
'manufacturer': manufacturer.pk,
},
]

View File

@ -2949,7 +2949,6 @@ class InventoryItemTestCase(TestCase, ChangeLoggedFilterSetTests):
@classmethod
def setUpTestData(cls):
manufacturers = (
Manufacturer(name='Manufacturer 1', slug='manufacturer-1'),
Manufacturer(name='Manufacturer 2', slug='manufacturer-2'),
@ -2998,10 +2997,17 @@ class InventoryItemTestCase(TestCase, ChangeLoggedFilterSetTests):
)
Device.objects.bulk_create(devices)
roles = (
InventoryItemRole(name='Inventory Item Role 1', slug='inventory-item-role-1'),
InventoryItemRole(name='Inventory Item Role 2', slug='inventory-item-role-2'),
InventoryItemRole(name='Inventory Item Role 3', slug='inventory-item-role-3'),
)
InventoryItemRole.objects.bulk_create(roles)
inventory_items = (
InventoryItem(device=devices[0], manufacturer=manufacturers[0], name='Inventory Item 1', label='A', part_id='1001', serial='ABC', asset_tag='1001', discovered=True, description='First'),
InventoryItem(device=devices[1], manufacturer=manufacturers[1], name='Inventory Item 2', label='B', part_id='1002', serial='DEF', asset_tag='1002', discovered=True, description='Second'),
InventoryItem(device=devices[2], manufacturer=manufacturers[2], name='Inventory Item 3', label='C', part_id='1003', serial='GHI', asset_tag='1003', discovered=False, description='Third'),
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'),
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'),
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'),
)
for i in inventory_items:
i.save()
@ -3077,6 +3083,13 @@ class InventoryItemTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'parent_id': [parent_items[0].pk, parent_items[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_role(self):
roles = InventoryItemRole.objects.all()[:2]
params = {'role_id': [roles[0].pk, roles[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'role': [roles[0].slug, roles[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_manufacturer(self):
manufacturers = Manufacturer.objects.all()[:2]
params = {'manufacturer_id': [manufacturers[0].pk, manufacturers[1].pk]}

View File

@ -2331,14 +2331,21 @@ class InventoryItemTestCase(ViewTestCases.DeviceComponentViewTestCase):
device = create_test_device('Device 1')
manufacturer, _ = Manufacturer.objects.get_or_create(name='Manufacturer 1', slug='manufacturer-1')
InventoryItem.objects.create(device=device, name='Inventory Item 1')
InventoryItem.objects.create(device=device, name='Inventory Item 2')
InventoryItem.objects.create(device=device, name='Inventory Item 3')
roles = (
InventoryItemRole(name='Inventory Item Role 1', slug='inventory-item-role-1'),
InventoryItemRole(name='Inventory Item Role 2', slug='inventory-item-role-2'),
)
InventoryItemRole.objects.bulk_create(roles)
InventoryItem.objects.create(device=device, name='Inventory Item 1', role=roles[0], manufacturer=manufacturer)
InventoryItem.objects.create(device=device, name='Inventory Item 2', role=roles[0], manufacturer=manufacturer)
InventoryItem.objects.create(device=device, name='Inventory Item 3', role=roles[0], manufacturer=manufacturer)
tags = create_tags('Alpha', 'Bravo', 'Charlie')
cls.form_data = {
'device': device.pk,
'role': roles[1].pk,
'manufacturer': manufacturer.pk,
'name': 'Inventory Item X',
'parent': None,
@ -2353,6 +2360,7 @@ class InventoryItemTestCase(ViewTestCases.DeviceComponentViewTestCase):
cls.bulk_create_data = {
'device': device.pk,
'name_pattern': 'Inventory Item [4-6]',
'role': roles[1].pk,
'manufacturer': manufacturer.pk,
'parent': None,
'discovered': False,
@ -2363,6 +2371,7 @@ class InventoryItemTestCase(ViewTestCases.DeviceComponentViewTestCase):
}
cls.bulk_edit_data = {
'role': roles[1].pk,
'part_id': '123456',
'description': 'New description',
}

View File

@ -2412,7 +2412,7 @@ class InventoryItemBulkImportView(generic.BulkImportView):
class InventoryItemBulkEditView(generic.BulkEditView):
queryset = InventoryItem.objects.prefetch_related('device', 'manufacturer')
queryset = InventoryItem.objects.prefetch_related('device', 'manufacturer', 'role')
filterset = filtersets.InventoryItemFilterSet
table = tables.InventoryItemTable
form = forms.InventoryItemBulkEditForm
@ -2423,7 +2423,7 @@ class InventoryItemBulkRenameView(generic.BulkRenameView):
class InventoryItemBulkDeleteView(generic.BulkDeleteView):
queryset = InventoryItem.objects.prefetch_related('device', 'manufacturer')
queryset = InventoryItem.objects.prefetch_related('device', 'manufacturer', 'role')
table = tables.InventoryItemTable
template_name = 'dcim/inventoryitem_bulk_delete.html'

View File

@ -13,9 +13,7 @@
<div class="row mb-3">
<div class="col col-md-6">
<div class="card">
<h5 class="card-header">
Inventory Item
</h5>
<h5 class="card-header">Inventory Item</h5>
<div class="card-body">
<table class="table table-hover attr-table">
<tr>
@ -42,6 +40,16 @@
<th scope="row">Label</th>
<td>{{ object.label|placeholder }}</td>
</tr>
<tr>
<th scope="row">Role</th>
<td>
{% if object.role %}
<a href="{{ object.role.get_absolute_url }}">{{ object.role }}</a>
{% else %}
<span class="text-muted">&mdash;</span>
{% endif %}
</td>
</tr>
<tr>
<th scope="row">Manufacturer</th>
<td>