diff --git a/docs/models/dcim/inventoryitem.md b/docs/models/dcim/inventoryitem.md index 237bad92cdf..f9837183392 100644 --- a/docs/models/dcim/inventoryitem.md +++ b/docs/models/dcim/inventoryitem.md @@ -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. diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index fe84874111a..5e07ea3fd0e 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -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', ] diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py index 5f4840fdee9..01c0a278d21 100644 --- a/netbox/dcim/filtersets.py +++ b/netbox/dcim/filtersets.py @@ -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' ) diff --git a/netbox/dcim/forms/bulk_create.py b/netbox/dcim/forms/bulk_create.py index 8eae46111e0..e78e0ee19af 100644 --- a/netbox/dcim/forms/bulk_create.py +++ b/netbox/dcim/forms/bulk_create.py @@ -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', ) diff --git a/netbox/dcim/forms/bulk_edit.py b/netbox/dcim/forms/bulk_edit.py index 8fc8835cbc0..93a90a1cb6a 100644 --- a/netbox/dcim/forms/bulk_edit.py +++ b/netbox/dcim/forms/bulk_edit.py @@ -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'] # diff --git a/netbox/dcim/forms/bulk_import.py b/netbox/dcim/forms/bulk_import.py index 40838c60ca1..1297fc98024 100644 --- a/netbox/dcim/forms/bulk_import.py +++ b/netbox/dcim/forms/bulk_import.py @@ -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): diff --git a/netbox/dcim/forms/filtersets.py b/netbox/dcim/forms/filtersets.py index ae58e1d2f15..c12891dc368 100644 --- a/netbox/dcim/forms/filtersets.py +++ b/netbox/dcim/forms/filtersets.py @@ -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, diff --git a/netbox/dcim/forms/models.py b/netbox/dcim/forms/models.py index b5b15e731b8..2be571f71eb 100644 --- a/netbox/dcim/forms/models.py +++ b/netbox/dcim/forms/models.py @@ -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', ] diff --git a/netbox/dcim/forms/object_create.py b/netbox/dcim/forms/object_create.py index 681a17a5aa9..9e208300b2d 100644 --- a/netbox/dcim/forms/object_create.py +++ b/netbox/dcim/forms/object_create.py @@ -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', ) diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index 5329c9e0120..cb38d86830a 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -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') diff --git a/netbox/dcim/tables/devices.py b/netbox/dcim/tables/devices.py index f889d52ec37..9472be541cb 100644 --- a/netbox/dcim/tables/devices.py +++ b/netbox/dcim/tables/devices.py @@ -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='' + '{{ value }}', + 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='' - '{{ value }}', - 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 # diff --git a/netbox/dcim/tests/test_api.py b/netbox/dcim/tests/test_api.py index a6c7760f4e8..e6ea1049975 100644 --- a/netbox/dcim/tests/test_api.py +++ b/netbox/dcim/tests/test_api.py @@ -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, }, ] diff --git a/netbox/dcim/tests/test_filtersets.py b/netbox/dcim/tests/test_filtersets.py index f93e9164d22..a808aeda2a0 100644 --- a/netbox/dcim/tests/test_filtersets.py +++ b/netbox/dcim/tests/test_filtersets.py @@ -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]} diff --git a/netbox/dcim/tests/test_views.py b/netbox/dcim/tests/test_views.py index 3ac7b9c7211..8f077df92b0 100644 --- a/netbox/dcim/tests/test_views.py +++ b/netbox/dcim/tests/test_views.py @@ -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', } diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 8e3d35b3e6b..bfa2fecae82 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -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' diff --git a/netbox/templates/dcim/inventoryitem.html b/netbox/templates/dcim/inventoryitem.html index 36ba0469f89..7de30365602 100644 --- a/netbox/templates/dcim/inventoryitem.html +++ b/netbox/templates/dcim/inventoryitem.html @@ -13,9 +13,7 @@
| Label | {{ object.label|placeholder }} |
|---|---|
| Role | ++ {% if object.role %} + {{ object.role }} + {% else %} + — + {% endif %} + | +
| Manufacturer |