mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-23 04:22:01 -06:00
Merge pull request #8172 from netbox-community/3087-inventory-item-roles
Closes #3087: Add inventory item roles
This commit is contained in:
commit
a58f1c6a7d
@ -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.
|
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.
|
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.
|
||||||
|
3
docs/models/dcim/inventoryitemrole.md
Normal file
3
docs/models/dcim/inventoryitemrole.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# Inventory Item Roles
|
||||||
|
|
||||||
|
Inventory items can be organized by functional roles, which are fully customizable by the user. For example, you might create roles for power supplies, fans, interface optics, etc.
|
@ -18,6 +18,10 @@
|
|||||||
|
|
||||||
A new REST API endpoint has been added at `/api/ipam/vlan-groups/<pk>/available-vlans/`. A GET request to this endpoint will return a list of available VLANs within the group. A POST request can be made to this endpoint specifying the name(s) of one or more VLANs to create within the group, and their VLAN IDs will be assigned automatically.
|
A new REST API endpoint has been added at `/api/ipam/vlan-groups/<pk>/available-vlans/`. A GET request to this endpoint will return a list of available VLANs within the group. A POST request can be made to this endpoint specifying the name(s) of one or more VLANs to create within the group, and their VLAN IDs will be assigned automatically.
|
||||||
|
|
||||||
|
#### Inventory Item Roles ([#3087](https://github.com/netbox-community/netbox/issues/3087))
|
||||||
|
|
||||||
|
A new model has been introduced to represent function roles for inventory items, similar to device roles. The assignment of roles to inventory items is optional.
|
||||||
|
|
||||||
#### Modules & Module Types ([#7844](https://github.com/netbox-community/netbox/issues/7844))
|
#### Modules & Module Types ([#7844](https://github.com/netbox-community/netbox/issues/7844))
|
||||||
|
|
||||||
Several new models have been added to support field-replaceable device modules, such as those within a chassis-based switch or router. Similar to devices, each module is instantiated from a user-defined module type, and can have components associated with it. These components become available to the parent device once the module has been installed within a module bay. This makes it very convenient to replicate the addition and deletion of device components as modules are installed and removed.
|
Several new models have been added to support field-replaceable device modules, such as those within a chassis-based switch or router. Similar to devices, each module is instantiated from a user-defined module type, and can have components associated with it. These components become available to the parent device once the module has been installed within a module bay. This makes it very convenient to replicate the addition and deletion of device components as modules are installed and removed.
|
||||||
@ -55,7 +59,8 @@ FIELD_CHOICES = {
|
|||||||
|
|
||||||
### REST API Changes
|
### REST API Changes
|
||||||
|
|
||||||
* Added the following endpoints for modules & module types:
|
* Added the following endpoints:
|
||||||
|
* `/api/dcim/inventory-item-roles/`
|
||||||
* `/api/dcim/modules/`
|
* `/api/dcim/modules/`
|
||||||
* `/api/dcim/module-bays/`
|
* `/api/dcim/module-bays/`
|
||||||
* `/api/dcim/module-bay-templates/`
|
* `/api/dcim/module-bay-templates/`
|
||||||
@ -70,6 +75,8 @@ FIELD_CHOICES = {
|
|||||||
* Added `module` field
|
* Added `module` field
|
||||||
* dcim.Interface
|
* dcim.Interface
|
||||||
* Added `module` field
|
* Added `module` field
|
||||||
|
* dcim.InventoryItem
|
||||||
|
* Added `role` field
|
||||||
* dcim.PowerPort
|
* dcim.PowerPort
|
||||||
* Added `module` field
|
* Added `module` field
|
||||||
* dcim.PowerOutlet
|
* dcim.PowerOutlet
|
||||||
|
@ -20,6 +20,7 @@ __all__ = [
|
|||||||
'NestedInterfaceSerializer',
|
'NestedInterfaceSerializer',
|
||||||
'NestedInterfaceTemplateSerializer',
|
'NestedInterfaceTemplateSerializer',
|
||||||
'NestedInventoryItemSerializer',
|
'NestedInventoryItemSerializer',
|
||||||
|
'NestedInventoryItemRoleSerializer',
|
||||||
'NestedManufacturerSerializer',
|
'NestedManufacturerSerializer',
|
||||||
'NestedModuleBaySerializer',
|
'NestedModuleBaySerializer',
|
||||||
'NestedModuleBayTemplateSerializer',
|
'NestedModuleBayTemplateSerializer',
|
||||||
@ -384,6 +385,15 @@ class NestedInventoryItemSerializer(WritableNestedSerializer):
|
|||||||
fields = ['id', 'url', 'display', 'device', 'name', '_depth']
|
fields = ['id', 'url', 'display', 'device', 'name', '_depth']
|
||||||
|
|
||||||
|
|
||||||
|
class NestedInventoryItemRoleSerializer(WritableNestedSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitemrole-detail')
|
||||||
|
inventoryitem_count = serializers.IntegerField(read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.InventoryItemRole
|
||||||
|
fields = ['id', 'url', 'display', 'name', 'slug', 'inventoryitem_count']
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Cables
|
# Cables
|
||||||
#
|
#
|
||||||
|
@ -806,25 +806,38 @@ class DeviceBaySerializer(PrimaryModelSerializer):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
#
|
|
||||||
# Inventory items
|
|
||||||
#
|
|
||||||
|
|
||||||
class InventoryItemSerializer(PrimaryModelSerializer):
|
class InventoryItemSerializer(PrimaryModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitem-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitem-detail')
|
||||||
device = NestedDeviceSerializer()
|
device = NestedDeviceSerializer()
|
||||||
parent = serializers.PrimaryKeyRelatedField(queryset=InventoryItem.objects.all(), allow_null=True, default=None)
|
parent = serializers.PrimaryKeyRelatedField(queryset=InventoryItem.objects.all(), allow_null=True, default=None)
|
||||||
manufacturer = NestedManufacturerSerializer(required=False, 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)
|
_depth = serializers.IntegerField(source='level', read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = InventoryItem
|
model = InventoryItem
|
||||||
fields = [
|
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',
|
'asset_tag', 'discovered', 'description', 'tags', 'custom_fields', 'created', 'last_updated', '_depth',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Device component roles
|
||||||
|
#
|
||||||
|
|
||||||
|
class InventoryItemRoleSerializer(PrimaryModelSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitemrole-detail')
|
||||||
|
inventoryitem_count = serializers.IntegerField(read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = InventoryItemRole
|
||||||
|
fields = [
|
||||||
|
'id', 'url', 'display', 'name', 'slug', 'color', 'description', 'tags', 'custom_fields', 'created',
|
||||||
|
'last_updated', 'inventoryitem_count',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Cables
|
# Cables
|
||||||
#
|
#
|
||||||
|
@ -50,6 +50,9 @@ router.register('module-bays', views.ModuleBayViewSet)
|
|||||||
router.register('device-bays', views.DeviceBayViewSet)
|
router.register('device-bays', views.DeviceBayViewSet)
|
||||||
router.register('inventory-items', views.InventoryItemViewSet)
|
router.register('inventory-items', views.InventoryItemViewSet)
|
||||||
|
|
||||||
|
# Device component roles
|
||||||
|
router.register('inventory-item-roles', views.InventoryItemRoleViewSet)
|
||||||
|
|
||||||
# Cables
|
# Cables
|
||||||
router.register('cables', views.CableViewSet)
|
router.register('cables', views.CableViewSet)
|
||||||
|
|
||||||
|
@ -623,6 +623,18 @@ class InventoryItemViewSet(ModelViewSet):
|
|||||||
brief_prefetch_fields = ['device']
|
brief_prefetch_fields = ['device']
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Device component roles
|
||||||
|
#
|
||||||
|
|
||||||
|
class InventoryItemRoleViewSet(CustomFieldModelViewSet):
|
||||||
|
queryset = InventoryItemRole.objects.prefetch_related('tags').annotate(
|
||||||
|
inventoryitem_count=count_related(InventoryItem, 'role')
|
||||||
|
)
|
||||||
|
serializer_class = serializers.InventoryItemRoleSerializer
|
||||||
|
filterset_class = filtersets.InventoryItemRoleFilterSet
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Cables
|
# Cables
|
||||||
#
|
#
|
||||||
|
@ -39,6 +39,7 @@ __all__ = (
|
|||||||
'InterfaceFilterSet',
|
'InterfaceFilterSet',
|
||||||
'InterfaceTemplateFilterSet',
|
'InterfaceTemplateFilterSet',
|
||||||
'InventoryItemFilterSet',
|
'InventoryItemFilterSet',
|
||||||
|
'InventoryItemRoleFilterSet',
|
||||||
'LocationFilterSet',
|
'LocationFilterSet',
|
||||||
'ManufacturerFilterSet',
|
'ManufacturerFilterSet',
|
||||||
'ModuleBayFilterSet',
|
'ModuleBayFilterSet',
|
||||||
@ -1283,6 +1284,16 @@ class InventoryItemFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet):
|
|||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Manufacturer (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(
|
serial = django_filters.CharFilter(
|
||||||
lookup_expr='iexact'
|
lookup_expr='iexact'
|
||||||
)
|
)
|
||||||
@ -1304,6 +1315,14 @@ class InventoryItemFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet):
|
|||||||
return queryset.filter(qs_filter)
|
return queryset.filter(qs_filter)
|
||||||
|
|
||||||
|
|
||||||
|
class InventoryItemRoleFilterSet(OrganizationalModelFilterSet):
|
||||||
|
tag = TagFilter()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = InventoryItemRole
|
||||||
|
fields = ['id', 'name', 'slug', 'color']
|
||||||
|
|
||||||
|
|
||||||
class VirtualChassisFilterSet(PrimaryModelFilterSet):
|
class VirtualChassisFilterSet(PrimaryModelFilterSet):
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
method='search',
|
method='search',
|
||||||
|
@ -107,11 +107,11 @@ class DeviceBayBulkCreateForm(DeviceBulkAddComponentForm):
|
|||||||
|
|
||||||
|
|
||||||
class InventoryItemBulkCreateForm(
|
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
|
DeviceBulkAddComponentForm
|
||||||
):
|
):
|
||||||
model = InventoryItem
|
model = InventoryItem
|
||||||
field_order = (
|
field_order = (
|
||||||
'name_pattern', 'label_pattern', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered', 'description',
|
'name_pattern', 'label_pattern', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered',
|
||||||
'tags',
|
'description', 'tags',
|
||||||
)
|
)
|
||||||
|
@ -30,6 +30,7 @@ __all__ = (
|
|||||||
'InterfaceBulkEditForm',
|
'InterfaceBulkEditForm',
|
||||||
'InterfaceTemplateBulkEditForm',
|
'InterfaceTemplateBulkEditForm',
|
||||||
'InventoryItemBulkEditForm',
|
'InventoryItemBulkEditForm',
|
||||||
|
'InventoryItemRoleBulkEditForm',
|
||||||
'LocationBulkEditForm',
|
'LocationBulkEditForm',
|
||||||
'ManufacturerBulkEditForm',
|
'ManufacturerBulkEditForm',
|
||||||
'ModuleBulkEditForm',
|
'ModuleBulkEditForm',
|
||||||
@ -1171,7 +1172,7 @@ class DeviceBayBulkEditForm(
|
|||||||
|
|
||||||
|
|
||||||
class InventoryItemBulkEditForm(
|
class InventoryItemBulkEditForm(
|
||||||
form_from_model(InventoryItem, ['label', 'manufacturer', 'part_id', 'description']),
|
form_from_model(InventoryItem, ['label', 'role', 'manufacturer', 'part_id', 'description']),
|
||||||
AddRemoveTagsForm,
|
AddRemoveTagsForm,
|
||||||
CustomFieldModelBulkEditForm
|
CustomFieldModelBulkEditForm
|
||||||
):
|
):
|
||||||
@ -1179,10 +1180,35 @@ class InventoryItemBulkEditForm(
|
|||||||
queryset=InventoryItem.objects.all(),
|
queryset=InventoryItem.objects.all(),
|
||||||
widget=forms.MultipleHiddenInput()
|
widget=forms.MultipleHiddenInput()
|
||||||
)
|
)
|
||||||
|
role = DynamicModelChoiceField(
|
||||||
|
queryset=InventoryItemRole.objects.all(),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
manufacturer = DynamicModelChoiceField(
|
manufacturer = DynamicModelChoiceField(
|
||||||
queryset=Manufacturer.objects.all(),
|
queryset=Manufacturer.objects.all(),
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
nullable_fields = ['label', 'manufacturer', 'part_id', 'description']
|
nullable_fields = ['label', 'role', 'manufacturer', 'part_id', 'description']
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Device component roles
|
||||||
|
#
|
||||||
|
|
||||||
|
class InventoryItemRoleBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||||
|
pk = forms.ModelMultipleChoiceField(
|
||||||
|
queryset=InventoryItemRole.objects.all(),
|
||||||
|
widget=forms.MultipleHiddenInput
|
||||||
|
)
|
||||||
|
color = ColorField(
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
description = forms.CharField(
|
||||||
|
max_length=200,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
nullable_fields = ['color', 'description']
|
||||||
|
@ -24,6 +24,7 @@ __all__ = (
|
|||||||
'FrontPortCSVForm',
|
'FrontPortCSVForm',
|
||||||
'InterfaceCSVForm',
|
'InterfaceCSVForm',
|
||||||
'InventoryItemCSVForm',
|
'InventoryItemCSVForm',
|
||||||
|
'InventoryItemRoleCSVForm',
|
||||||
'LocationCSVForm',
|
'LocationCSVForm',
|
||||||
'ManufacturerCSVForm',
|
'ManufacturerCSVForm',
|
||||||
'ModuleCSVForm',
|
'ModuleCSVForm',
|
||||||
@ -771,6 +772,11 @@ class InventoryItemCSVForm(CustomFieldModelCSVForm):
|
|||||||
queryset=Device.objects.all(),
|
queryset=Device.objects.all(),
|
||||||
to_field_name='name'
|
to_field_name='name'
|
||||||
)
|
)
|
||||||
|
role = CSVModelChoiceField(
|
||||||
|
queryset=InventoryItemRole.objects.all(),
|
||||||
|
to_field_name='name',
|
||||||
|
required=False
|
||||||
|
)
|
||||||
manufacturer = CSVModelChoiceField(
|
manufacturer = CSVModelChoiceField(
|
||||||
queryset=Manufacturer.objects.all(),
|
queryset=Manufacturer.objects.all(),
|
||||||
to_field_name='name',
|
to_field_name='name',
|
||||||
@ -786,7 +792,8 @@ class InventoryItemCSVForm(CustomFieldModelCSVForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = InventoryItem
|
model = InventoryItem
|
||||||
fields = (
|
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):
|
def __init__(self, *args, **kwargs):
|
||||||
@ -805,6 +812,25 @@ class InventoryItemCSVForm(CustomFieldModelCSVForm):
|
|||||||
self.fields['parent'].queryset = InventoryItem.objects.none()
|
self.fields['parent'].queryset = InventoryItem.objects.none()
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Device component roles
|
||||||
|
#
|
||||||
|
|
||||||
|
class InventoryItemRoleCSVForm(CustomFieldModelCSVForm):
|
||||||
|
slug = SlugField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = InventoryItemRole
|
||||||
|
fields = ('name', 'slug', 'color', 'description')
|
||||||
|
help_texts = {
|
||||||
|
'color': mark_safe('RGB color in hexadecimal (e.g. <code>00ff00</code>)'),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Cables
|
||||||
|
#
|
||||||
|
|
||||||
class CableCSVForm(CustomFieldModelCSVForm):
|
class CableCSVForm(CustomFieldModelCSVForm):
|
||||||
# Termination A
|
# Termination A
|
||||||
side_a_device = CSVModelChoiceField(
|
side_a_device = CSVModelChoiceField(
|
||||||
@ -906,6 +932,10 @@ class CableCSVForm(CustomFieldModelCSVForm):
|
|||||||
return length_unit if length_unit is not None else ''
|
return length_unit if length_unit is not None else ''
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Virtual chassis
|
||||||
|
#
|
||||||
|
|
||||||
class VirtualChassisCSVForm(CustomFieldModelCSVForm):
|
class VirtualChassisCSVForm(CustomFieldModelCSVForm):
|
||||||
master = CSVModelChoiceField(
|
master = CSVModelChoiceField(
|
||||||
queryset=Device.objects.all(),
|
queryset=Device.objects.all(),
|
||||||
@ -919,6 +949,10 @@ class VirtualChassisCSVForm(CustomFieldModelCSVForm):
|
|||||||
fields = ('name', 'domain', 'master')
|
fields = ('name', 'domain', 'master')
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Power
|
||||||
|
#
|
||||||
|
|
||||||
class PowerPanelCSVForm(CustomFieldModelCSVForm):
|
class PowerPanelCSVForm(CustomFieldModelCSVForm):
|
||||||
site = CSVModelChoiceField(
|
site = CSVModelChoiceField(
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
|
@ -27,6 +27,7 @@ __all__ = (
|
|||||||
'InterfaceConnectionFilterForm',
|
'InterfaceConnectionFilterForm',
|
||||||
'InterfaceFilterForm',
|
'InterfaceFilterForm',
|
||||||
'InventoryItemFilterForm',
|
'InventoryItemFilterForm',
|
||||||
|
'InventoryItemRoleFilterForm',
|
||||||
'LocationFilterForm',
|
'LocationFilterForm',
|
||||||
'ManufacturerFilterForm',
|
'ManufacturerFilterForm',
|
||||||
'ModuleFilterForm',
|
'ModuleFilterForm',
|
||||||
@ -1099,6 +1100,12 @@ class InventoryItemFilterForm(DeviceComponentFilterForm):
|
|||||||
['name', 'label', 'manufacturer_id', 'serial', 'asset_tag', 'discovered'],
|
['name', 'label', 'manufacturer_id', 'serial', 'asset_tag', 'discovered'],
|
||||||
['region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id'],
|
['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(
|
manufacturer_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=Manufacturer.objects.all(),
|
queryset=Manufacturer.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
@ -1120,6 +1127,15 @@ class InventoryItemFilterForm(DeviceComponentFilterForm):
|
|||||||
tag = TagFilterField(model)
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Device component roles
|
||||||
|
#
|
||||||
|
|
||||||
|
class InventoryItemRoleFilterForm(CustomFieldModelFilterForm):
|
||||||
|
model = InventoryItemRole
|
||||||
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Connections
|
# Connections
|
||||||
#
|
#
|
||||||
|
@ -37,6 +37,7 @@ __all__ = (
|
|||||||
'InterfaceForm',
|
'InterfaceForm',
|
||||||
'InterfaceTemplateForm',
|
'InterfaceTemplateForm',
|
||||||
'InventoryItemForm',
|
'InventoryItemForm',
|
||||||
|
'InventoryItemRoleForm',
|
||||||
'LocationForm',
|
'LocationForm',
|
||||||
'ManufacturerForm',
|
'ManufacturerForm',
|
||||||
'ModuleForm',
|
'ModuleForm',
|
||||||
@ -1367,6 +1368,10 @@ class InventoryItemForm(CustomFieldModelForm):
|
|||||||
'device_id': '$device'
|
'device_id': '$device'
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
role = DynamicModelChoiceField(
|
||||||
|
queryset=InventoryItemRole.objects.all(),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
manufacturer = DynamicModelChoiceField(
|
manufacturer = DynamicModelChoiceField(
|
||||||
queryset=Manufacturer.objects.all(),
|
queryset=Manufacturer.objects.all(),
|
||||||
required=False
|
required=False
|
||||||
@ -1379,6 +1384,24 @@ class InventoryItemForm(CustomFieldModelForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = InventoryItem
|
model = InventoryItem
|
||||||
fields = [
|
fields = [
|
||||||
'device', 'parent', 'name', 'label', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description',
|
'device', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag',
|
||||||
'tags',
|
'description', 'tags',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Device component roles
|
||||||
|
#
|
||||||
|
|
||||||
|
class InventoryItemRoleForm(CustomFieldModelForm):
|
||||||
|
slug = SlugField()
|
||||||
|
tags = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=Tag.objects.all(),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = InventoryItemRole
|
||||||
|
fields = [
|
||||||
|
'name', 'slug', 'color', 'description', 'tags',
|
||||||
]
|
]
|
||||||
|
@ -652,10 +652,6 @@ class DeviceBayCreateForm(ComponentCreateForm):
|
|||||||
|
|
||||||
class InventoryItemCreateForm(ComponentCreateForm):
|
class InventoryItemCreateForm(ComponentCreateForm):
|
||||||
model = InventoryItem
|
model = InventoryItem
|
||||||
manufacturer = DynamicModelChoiceField(
|
|
||||||
queryset=Manufacturer.objects.all(),
|
|
||||||
required=False
|
|
||||||
)
|
|
||||||
parent = DynamicModelChoiceField(
|
parent = DynamicModelChoiceField(
|
||||||
queryset=InventoryItem.objects.all(),
|
queryset=InventoryItem.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
@ -663,6 +659,14 @@ class InventoryItemCreateForm(ComponentCreateForm):
|
|||||||
'device_id': '$device'
|
'device_id': '$device'
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
role = DynamicModelChoiceField(
|
||||||
|
queryset=InventoryItemRole.objects.all(),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
manufacturer = DynamicModelChoiceField(
|
||||||
|
queryset=Manufacturer.objects.all(),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
part_id = forms.CharField(
|
part_id = forms.CharField(
|
||||||
max_length=50,
|
max_length=50,
|
||||||
required=False,
|
required=False,
|
||||||
@ -677,6 +681,6 @@ class InventoryItemCreateForm(ComponentCreateForm):
|
|||||||
required=False,
|
required=False,
|
||||||
)
|
)
|
||||||
field_order = (
|
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',
|
'description', 'tags',
|
||||||
)
|
)
|
||||||
|
@ -50,6 +50,9 @@ class DCIMQuery(graphene.ObjectType):
|
|||||||
inventory_item = ObjectField(InventoryItemType)
|
inventory_item = ObjectField(InventoryItemType)
|
||||||
inventory_item_list = ObjectListField(InventoryItemType)
|
inventory_item_list = ObjectListField(InventoryItemType)
|
||||||
|
|
||||||
|
inventory_item_role = ObjectField(InventoryItemRoleType)
|
||||||
|
inventory_item_role_list = ObjectListField(InventoryItemRoleType)
|
||||||
|
|
||||||
location = ObjectField(LocationType)
|
location = ObjectField(LocationType)
|
||||||
location_list = ObjectListField(LocationType)
|
location_list = ObjectListField(LocationType)
|
||||||
|
|
||||||
|
@ -25,6 +25,7 @@ __all__ = (
|
|||||||
'InterfaceType',
|
'InterfaceType',
|
||||||
'InterfaceTemplateType',
|
'InterfaceTemplateType',
|
||||||
'InventoryItemType',
|
'InventoryItemType',
|
||||||
|
'InventoryItemRoleType',
|
||||||
'LocationType',
|
'LocationType',
|
||||||
'ManufacturerType',
|
'ManufacturerType',
|
||||||
'ModuleType',
|
'ModuleType',
|
||||||
@ -242,6 +243,14 @@ class InventoryItemType(ComponentObjectType):
|
|||||||
filterset_class = filtersets.InventoryItemFilterSet
|
filterset_class = filtersets.InventoryItemFilterSet
|
||||||
|
|
||||||
|
|
||||||
|
class InventoryItemRoleType(OrganizationalObjectType):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.InventoryItemRole
|
||||||
|
fields = '__all__'
|
||||||
|
filterset_class = filtersets.InventoryItemRoleFilterSet
|
||||||
|
|
||||||
|
|
||||||
class LocationType(VLANGroupsMixin, ImageAttachmentsMixin, OrganizationalObjectType):
|
class LocationType(VLANGroupsMixin, ImageAttachmentsMixin, OrganizationalObjectType):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
38
netbox/dcim/migrations/0146_inventoryitemrole.py
Normal file
38
netbox/dcim/migrations/0146_inventoryitemrole.py
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import django.core.serializers.json
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import taggit.managers
|
||||||
|
import utilities.fields
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('extras', '0067_configcontext_cluster_types'),
|
||||||
|
('dcim', '0145_modules'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='InventoryItemRole',
|
||||||
|
fields=[
|
||||||
|
('created', models.DateField(auto_now_add=True, null=True)),
|
||||||
|
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||||
|
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
|
||||||
|
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||||
|
('name', models.CharField(max_length=100, unique=True)),
|
||||||
|
('slug', models.SlugField(max_length=100, unique=True)),
|
||||||
|
('color', utilities.fields.ColorField(default='9e9e9e', max_length=6)),
|
||||||
|
('description', models.CharField(blank=True, max_length=200)),
|
||||||
|
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'ordering': ['name'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='inventoryitem',
|
||||||
|
name='role',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='inventory_items', to='dcim.inventoryitemrole'),
|
||||||
|
),
|
||||||
|
]
|
@ -12,7 +12,8 @@ from dcim.constants import *
|
|||||||
from dcim.fields import MACAddressField, WWNField
|
from dcim.fields import MACAddressField, WWNField
|
||||||
from dcim.svg import CableTraceSVG
|
from dcim.svg import CableTraceSVG
|
||||||
from extras.utils import extras_features
|
from extras.utils import extras_features
|
||||||
from netbox.models import PrimaryModel
|
from netbox.models import OrganizationalModel, PrimaryModel
|
||||||
|
from utilities.choices import ColorChoices
|
||||||
from utilities.fields import ColorField, NaturalOrderingField
|
from utilities.fields import ColorField, NaturalOrderingField
|
||||||
from utilities.mptt import TreeManager
|
from utilities.mptt import TreeManager
|
||||||
from utilities.ordering import naturalize_interface
|
from utilities.ordering import naturalize_interface
|
||||||
@ -30,6 +31,7 @@ __all__ = (
|
|||||||
'FrontPort',
|
'FrontPort',
|
||||||
'Interface',
|
'Interface',
|
||||||
'InventoryItem',
|
'InventoryItem',
|
||||||
|
'InventoryItemRole',
|
||||||
'ModuleBay',
|
'ModuleBay',
|
||||||
'PathEndpoint',
|
'PathEndpoint',
|
||||||
'PowerOutlet',
|
'PowerOutlet',
|
||||||
@ -946,6 +948,38 @@ class DeviceBay(ComponentModel):
|
|||||||
# Inventory items
|
# Inventory items
|
||||||
#
|
#
|
||||||
|
|
||||||
|
|
||||||
|
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||||
|
class InventoryItemRole(OrganizationalModel):
|
||||||
|
"""
|
||||||
|
Inventory items may optionally be assigned a functional role.
|
||||||
|
"""
|
||||||
|
name = models.CharField(
|
||||||
|
max_length=100,
|
||||||
|
unique=True
|
||||||
|
)
|
||||||
|
slug = models.SlugField(
|
||||||
|
max_length=100,
|
||||||
|
unique=True
|
||||||
|
)
|
||||||
|
color = ColorField(
|
||||||
|
default=ColorChoices.COLOR_GREY
|
||||||
|
)
|
||||||
|
description = models.CharField(
|
||||||
|
max_length=200,
|
||||||
|
blank=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['name']
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse('dcim:inventoryitemrole', args=[self.pk])
|
||||||
|
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||||
class InventoryItem(MPTTModel, ComponentModel):
|
class InventoryItem(MPTTModel, ComponentModel):
|
||||||
"""
|
"""
|
||||||
@ -960,6 +994,13 @@ class InventoryItem(MPTTModel, ComponentModel):
|
|||||||
null=True,
|
null=True,
|
||||||
db_index=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(
|
manufacturer = models.ForeignKey(
|
||||||
to='dcim.Manufacturer',
|
to='dcim.Manufacturer',
|
||||||
on_delete=models.PROTECT,
|
on_delete=models.PROTECT,
|
||||||
@ -993,7 +1034,7 @@ class InventoryItem(MPTTModel, ComponentModel):
|
|||||||
|
|
||||||
objects = TreeManager()
|
objects = TreeManager()
|
||||||
|
|
||||||
clone_fields = ['device', 'parent', 'manufacturer', 'part_id']
|
clone_fields = ['device', 'parent', 'role', 'manufacturer', 'part_id']
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ('device__id', 'parent__id', '_name')
|
ordering = ('device__id', 'parent__id', '_name')
|
||||||
|
@ -2,8 +2,8 @@ import django_tables2 as tables
|
|||||||
from django_tables2.utils import Accessor
|
from django_tables2.utils import Accessor
|
||||||
|
|
||||||
from dcim.models import (
|
from dcim.models import (
|
||||||
ConsolePort, ConsoleServerPort, Device, DeviceBay, DeviceRole, FrontPort, Interface, InventoryItem, ModuleBay,
|
ConsolePort, ConsoleServerPort, Device, DeviceBay, DeviceRole, FrontPort, Interface, InventoryItem,
|
||||||
Platform, PowerOutlet, PowerPort, RearPort, VirtualChassis,
|
InventoryItemRole, ModuleBay, Platform, PowerOutlet, PowerPort, RearPort, VirtualChassis,
|
||||||
)
|
)
|
||||||
from tenancy.tables import TenantColumn
|
from tenancy.tables import TenantColumn
|
||||||
from utilities.tables import (
|
from utilities.tables import (
|
||||||
@ -33,6 +33,7 @@ __all__ = (
|
|||||||
'DeviceTable',
|
'DeviceTable',
|
||||||
'FrontPortTable',
|
'FrontPortTable',
|
||||||
'InterfaceTable',
|
'InterfaceTable',
|
||||||
|
'InventoryItemRoleTable',
|
||||||
'InventoryItemTable',
|
'InventoryItemTable',
|
||||||
'ModuleBayTable',
|
'ModuleBayTable',
|
||||||
'PlatformTable',
|
'PlatformTable',
|
||||||
@ -68,11 +69,11 @@ def get_interface_state_attribute(record):
|
|||||||
else:
|
else:
|
||||||
return "disabled"
|
return "disabled"
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Device roles
|
# Device roles
|
||||||
#
|
#
|
||||||
|
|
||||||
|
|
||||||
class DeviceRoleTable(BaseTable):
|
class DeviceRoleTable(BaseTable):
|
||||||
pk = ToggleColumn()
|
pk = ToggleColumn()
|
||||||
name = tables.Column(
|
name = tables.Column(
|
||||||
@ -773,6 +774,9 @@ class InventoryItemTable(DeviceComponentTable):
|
|||||||
'args': [Accessor('device_id')],
|
'args': [Accessor('device_id')],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
role = tables.Column(
|
||||||
|
linkify=True
|
||||||
|
)
|
||||||
manufacturer = tables.Column(
|
manufacturer = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
@ -785,10 +789,10 @@ class InventoryItemTable(DeviceComponentTable):
|
|||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = InventoryItem
|
model = InventoryItem
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'name', 'device', 'label', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description',
|
'pk', 'id', 'name', 'device', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag',
|
||||||
'discovered', 'tags',
|
'description', 'discovered', 'tags',
|
||||||
)
|
)
|
||||||
default_columns = ('pk', 'name', 'device', 'label', 'manufacturer', 'part_id', 'serial', 'asset_tag')
|
default_columns = ('pk', 'name', 'device', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag')
|
||||||
|
|
||||||
|
|
||||||
class DeviceInventoryItemTable(InventoryItemTable):
|
class DeviceInventoryItemTable(InventoryItemTable):
|
||||||
@ -806,15 +810,38 @@ class DeviceInventoryItemTable(InventoryItemTable):
|
|||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = InventoryItem
|
model = InventoryItem
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'name', 'label', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description', 'discovered',
|
'pk', 'id', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description',
|
||||||
'tags', 'actions',
|
'discovered', 'tags', 'actions',
|
||||||
)
|
)
|
||||||
default_columns = (
|
default_columns = (
|
||||||
'pk', 'name', 'label', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description', 'discovered',
|
'pk', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description', 'actions',
|
||||||
'actions',
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class InventoryItemRoleTable(BaseTable):
|
||||||
|
pk = ToggleColumn()
|
||||||
|
name = tables.Column(
|
||||||
|
linkify=True
|
||||||
|
)
|
||||||
|
inventoryitem_count = LinkedCountColumn(
|
||||||
|
viewname='dcim:inventoryitem_list',
|
||||||
|
url_params={'role_id': 'pk'},
|
||||||
|
verbose_name='Items'
|
||||||
|
)
|
||||||
|
color = ColorColumn()
|
||||||
|
tags = TagColumn(
|
||||||
|
url_name='dcim:inventoryitemrole_list'
|
||||||
|
)
|
||||||
|
actions = ButtonsColumn(InventoryItemRole)
|
||||||
|
|
||||||
|
class Meta(BaseTable.Meta):
|
||||||
|
model = InventoryItemRole
|
||||||
|
fields = (
|
||||||
|
'pk', 'id', 'name', 'inventoryitem_count', 'color', 'description', 'slug', 'tags', 'actions',
|
||||||
|
)
|
||||||
|
default_columns = ('pk', 'name', 'inventoryitem_count', 'color', 'description', 'actions')
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Virtual chassis
|
# Virtual chassis
|
||||||
#
|
#
|
||||||
|
@ -1626,29 +1626,73 @@ class InventoryItemTest(APIViewTestCases.APIViewTestCase):
|
|||||||
devicerole = DeviceRole.objects.create(name='Test Device Role 1', slug='test-device-role-1', color='ff0000')
|
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)
|
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)
|
roles = (
|
||||||
InventoryItem.objects.create(device=device, name='Inventory Item 2', manufacturer=manufacturer)
|
InventoryItemRole(name='Inventory Item Role 1', slug='inventory-item-role-1'),
|
||||||
InventoryItem.objects.create(device=device, name='Inventory Item 3', manufacturer=manufacturer)
|
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 = [
|
cls.create_data = [
|
||||||
{
|
{
|
||||||
'device': device.pk,
|
'device': device.pk,
|
||||||
'name': 'Inventory Item 4',
|
'name': 'Inventory Item 4',
|
||||||
|
'role': roles[1].pk,
|
||||||
'manufacturer': manufacturer.pk,
|
'manufacturer': manufacturer.pk,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'device': device.pk,
|
'device': device.pk,
|
||||||
'name': 'Inventory Item 5',
|
'name': 'Inventory Item 5',
|
||||||
|
'role': roles[1].pk,
|
||||||
'manufacturer': manufacturer.pk,
|
'manufacturer': manufacturer.pk,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'device': device.pk,
|
'device': device.pk,
|
||||||
'name': 'Inventory Item 6',
|
'name': 'Inventory Item 6',
|
||||||
|
'role': roles[1].pk,
|
||||||
'manufacturer': manufacturer.pk,
|
'manufacturer': manufacturer.pk,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class InventoryItemRoleTest(APIViewTestCases.APIViewTestCase):
|
||||||
|
model = InventoryItemRole
|
||||||
|
brief_fields = ['display', 'id', 'inventoryitem_count', 'name', 'slug', 'url']
|
||||||
|
create_data = [
|
||||||
|
{
|
||||||
|
'name': 'Inventory Item Role 4',
|
||||||
|
'slug': 'inventory-item-role-4',
|
||||||
|
'color': 'ffff00',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'Inventory Item Role 5',
|
||||||
|
'slug': 'inventory-item-role-5',
|
||||||
|
'color': 'ffff00',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'Inventory Item Role 6',
|
||||||
|
'slug': 'inventory-item-role-6',
|
||||||
|
'color': 'ffff00',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
bulk_update_data = {
|
||||||
|
'description': 'New description',
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
|
||||||
|
roles = (
|
||||||
|
InventoryItemRole(name='Inventory Item Role 1', slug='inventory-item-role-1', color='ff0000'),
|
||||||
|
InventoryItemRole(name='Inventory Item Role 2', slug='inventory-item-role-2', color='00ff00'),
|
||||||
|
InventoryItemRole(name='Inventory Item Role 3', slug='inventory-item-role-3', color='0000ff'),
|
||||||
|
)
|
||||||
|
InventoryItemRole.objects.bulk_create(roles)
|
||||||
|
|
||||||
|
|
||||||
class CableTest(APIViewTestCases.APIViewTestCase):
|
class CableTest(APIViewTestCases.APIViewTestCase):
|
||||||
model = Cable
|
model = Cable
|
||||||
brief_fields = ['display', 'id', 'label', 'url']
|
brief_fields = ['display', 'id', 'label', 'url']
|
||||||
|
@ -2949,7 +2949,6 @@ class InventoryItemTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
|
|
||||||
manufacturers = (
|
manufacturers = (
|
||||||
Manufacturer(name='Manufacturer 1', slug='manufacturer-1'),
|
Manufacturer(name='Manufacturer 1', slug='manufacturer-1'),
|
||||||
Manufacturer(name='Manufacturer 2', slug='manufacturer-2'),
|
Manufacturer(name='Manufacturer 2', slug='manufacturer-2'),
|
||||||
@ -2998,10 +2997,17 @@ class InventoryItemTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
)
|
)
|
||||||
Device.objects.bulk_create(devices)
|
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 = (
|
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[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], manufacturer=manufacturers[1], name='Inventory Item 2', label='B', part_id='1002', serial='DEF', asset_tag='1002', discovered=True, description='Second'),
|
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], manufacturer=manufacturers[2], name='Inventory Item 3', label='C', part_id='1003', serial='GHI', asset_tag='1003', discovered=False, description='Third'),
|
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:
|
for i in inventory_items:
|
||||||
i.save()
|
i.save()
|
||||||
@ -3077,6 +3083,13 @@ class InventoryItemTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
params = {'parent_id': [parent_items[0].pk, parent_items[1].pk]}
|
params = {'parent_id': [parent_items[0].pk, parent_items[1].pk]}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
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):
|
def test_manufacturer(self):
|
||||||
manufacturers = Manufacturer.objects.all()[:2]
|
manufacturers = Manufacturer.objects.all()[:2]
|
||||||
params = {'manufacturer_id': [manufacturers[0].pk, manufacturers[1].pk]}
|
params = {'manufacturer_id': [manufacturers[0].pk, manufacturers[1].pk]}
|
||||||
@ -3091,6 +3104,33 @@ class InventoryItemTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||||
|
|
||||||
|
|
||||||
|
class InventoryItemRoleTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||||
|
queryset = InventoryItemRole.objects.all()
|
||||||
|
filterset = InventoryItemRoleFilterSet
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
|
||||||
|
roles = (
|
||||||
|
InventoryItemRole(name='Inventory Item Role 1', slug='inventory-item-role-1', color='ff0000'),
|
||||||
|
InventoryItemRole(name='Inventory Item Role 2', slug='inventory-item-role-2', color='00ff00'),
|
||||||
|
InventoryItemRole(name='Inventory Item Role 3', slug='inventory-item-role-3', color='0000ff'),
|
||||||
|
)
|
||||||
|
InventoryItemRole.objects.bulk_create(roles)
|
||||||
|
|
||||||
|
def test_name(self):
|
||||||
|
params = {'name': ['Inventory Item Role 1', 'Inventory Item Role 2']}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_slug(self):
|
||||||
|
params = {'slug': ['inventory-item-role-1', 'inventory-item-role-2']}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_color(self):
|
||||||
|
params = {'color': ['ff0000', '00ff00']}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
|
||||||
class VirtualChassisTestCase(TestCase, ChangeLoggedFilterSetTests):
|
class VirtualChassisTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||||
queryset = VirtualChassis.objects.all()
|
queryset = VirtualChassis.objects.all()
|
||||||
filterset = VirtualChassisFilterSet
|
filterset = VirtualChassisFilterSet
|
||||||
|
@ -1408,7 +1408,7 @@ class DeviceRoleTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
|||||||
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||||
|
|
||||||
cls.form_data = {
|
cls.form_data = {
|
||||||
'name': 'Devie Role X',
|
'name': 'Device Role X',
|
||||||
'slug': 'device-role-x',
|
'slug': 'device-role-x',
|
||||||
'color': 'c0c0c0',
|
'color': 'c0c0c0',
|
||||||
'vm_role': False,
|
'vm_role': False,
|
||||||
@ -2331,14 +2331,21 @@ class InventoryItemTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
|||||||
device = create_test_device('Device 1')
|
device = create_test_device('Device 1')
|
||||||
manufacturer, _ = Manufacturer.objects.get_or_create(name='Manufacturer 1', slug='manufacturer-1')
|
manufacturer, _ = Manufacturer.objects.get_or_create(name='Manufacturer 1', slug='manufacturer-1')
|
||||||
|
|
||||||
InventoryItem.objects.create(device=device, name='Inventory Item 1')
|
roles = (
|
||||||
InventoryItem.objects.create(device=device, name='Inventory Item 2')
|
InventoryItemRole(name='Inventory Item Role 1', slug='inventory-item-role-1'),
|
||||||
InventoryItem.objects.create(device=device, name='Inventory Item 3')
|
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')
|
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||||
|
|
||||||
cls.form_data = {
|
cls.form_data = {
|
||||||
'device': device.pk,
|
'device': device.pk,
|
||||||
|
'role': roles[1].pk,
|
||||||
'manufacturer': manufacturer.pk,
|
'manufacturer': manufacturer.pk,
|
||||||
'name': 'Inventory Item X',
|
'name': 'Inventory Item X',
|
||||||
'parent': None,
|
'parent': None,
|
||||||
@ -2353,6 +2360,7 @@ class InventoryItemTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
|||||||
cls.bulk_create_data = {
|
cls.bulk_create_data = {
|
||||||
'device': device.pk,
|
'device': device.pk,
|
||||||
'name_pattern': 'Inventory Item [4-6]',
|
'name_pattern': 'Inventory Item [4-6]',
|
||||||
|
'role': roles[1].pk,
|
||||||
'manufacturer': manufacturer.pk,
|
'manufacturer': manufacturer.pk,
|
||||||
'parent': None,
|
'parent': None,
|
||||||
'discovered': False,
|
'discovered': False,
|
||||||
@ -2363,6 +2371,7 @@ class InventoryItemTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
|||||||
}
|
}
|
||||||
|
|
||||||
cls.bulk_edit_data = {
|
cls.bulk_edit_data = {
|
||||||
|
'role': roles[1].pk,
|
||||||
'part_id': '123456',
|
'part_id': '123456',
|
||||||
'description': 'New description',
|
'description': 'New description',
|
||||||
}
|
}
|
||||||
@ -2375,6 +2384,41 @@ class InventoryItemTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class InventoryItemRoleTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
||||||
|
model = InventoryItemRole
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
|
||||||
|
InventoryItemRole.objects.bulk_create([
|
||||||
|
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'),
|
||||||
|
])
|
||||||
|
|
||||||
|
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||||
|
|
||||||
|
cls.form_data = {
|
||||||
|
'name': 'Inventory Item Role X',
|
||||||
|
'slug': 'inventory-item-role-x',
|
||||||
|
'color': 'c0c0c0',
|
||||||
|
'description': 'New inventory item role',
|
||||||
|
'tags': [t.pk for t in tags],
|
||||||
|
}
|
||||||
|
|
||||||
|
cls.csv_data = (
|
||||||
|
"name,slug,color",
|
||||||
|
"Inventory Item Role 4,inventory-item-role-4,ff0000",
|
||||||
|
"Inventory Item Role 5,inventory-item-role-5,00ff00",
|
||||||
|
"Inventory Item Role 6,inventory-item-role-6,0000ff",
|
||||||
|
)
|
||||||
|
|
||||||
|
cls.bulk_edit_data = {
|
||||||
|
'color': '00ff00',
|
||||||
|
'description': 'New description',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# TODO: Change base class to PrimaryObjectViewTestCase
|
# TODO: Change base class to PrimaryObjectViewTestCase
|
||||||
# Blocked by lack of common creation view for cables (termination A must be initialized)
|
# Blocked by lack of common creation view for cables (termination A must be initialized)
|
||||||
class CableTestCase(
|
class CableTestCase(
|
||||||
|
@ -425,6 +425,17 @@ urlpatterns = [
|
|||||||
path('inventory-items/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='inventoryitem_changelog', kwargs={'model': InventoryItem}),
|
path('inventory-items/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='inventoryitem_changelog', kwargs={'model': InventoryItem}),
|
||||||
path('devices/inventory-items/add/', views.DeviceBulkAddInventoryItemView.as_view(), name='device_bulk_add_inventoryitem'),
|
path('devices/inventory-items/add/', views.DeviceBulkAddInventoryItemView.as_view(), name='device_bulk_add_inventoryitem'),
|
||||||
|
|
||||||
|
# Device roles
|
||||||
|
path('inventory-item-roles/', views.InventoryItemRoleListView.as_view(), name='inventoryitemrole_list'),
|
||||||
|
path('inventory-item-roles/add/', views.InventoryItemRoleEditView.as_view(), name='inventoryitemrole_add'),
|
||||||
|
path('inventory-item-roles/import/', views.InventoryItemRoleBulkImportView.as_view(), name='inventoryitemrole_import'),
|
||||||
|
path('inventory-item-roles/edit/', views.InventoryItemRoleBulkEditView.as_view(), name='inventoryitemrole_bulk_edit'),
|
||||||
|
path('inventory-item-roles/delete/', views.InventoryItemRoleBulkDeleteView.as_view(), name='inventoryitemrole_bulk_delete'),
|
||||||
|
path('inventory-item-roles/<int:pk>/', views.InventoryItemRoleView.as_view(), name='inventoryitemrole'),
|
||||||
|
path('inventory-item-roles/<int:pk>/edit/', views.InventoryItemRoleEditView.as_view(), name='inventoryitemrole_edit'),
|
||||||
|
path('inventory-item-roles/<int:pk>/delete/', views.InventoryItemRoleDeleteView.as_view(), name='inventoryitemrole_delete'),
|
||||||
|
path('inventory-item-roles/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='inventoryitemrole_changelog', kwargs={'model': InventoryItemRole}),
|
||||||
|
|
||||||
# Cables
|
# Cables
|
||||||
path('cables/', views.CableListView.as_view(), name='cable_list'),
|
path('cables/', views.CableListView.as_view(), name='cable_list'),
|
||||||
path('cables/import/', views.CableBulkImportView.as_view(), name='cable_import'),
|
path('cables/import/', views.CableBulkImportView.as_view(), name='cable_import'),
|
||||||
|
@ -2412,7 +2412,7 @@ class InventoryItemBulkImportView(generic.BulkImportView):
|
|||||||
|
|
||||||
|
|
||||||
class InventoryItemBulkEditView(generic.BulkEditView):
|
class InventoryItemBulkEditView(generic.BulkEditView):
|
||||||
queryset = InventoryItem.objects.prefetch_related('device', 'manufacturer')
|
queryset = InventoryItem.objects.prefetch_related('device', 'manufacturer', 'role')
|
||||||
filterset = filtersets.InventoryItemFilterSet
|
filterset = filtersets.InventoryItemFilterSet
|
||||||
table = tables.InventoryItemTable
|
table = tables.InventoryItemTable
|
||||||
form = forms.InventoryItemBulkEditForm
|
form = forms.InventoryItemBulkEditForm
|
||||||
@ -2423,11 +2423,64 @@ class InventoryItemBulkRenameView(generic.BulkRenameView):
|
|||||||
|
|
||||||
|
|
||||||
class InventoryItemBulkDeleteView(generic.BulkDeleteView):
|
class InventoryItemBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = InventoryItem.objects.prefetch_related('device', 'manufacturer')
|
queryset = InventoryItem.objects.prefetch_related('device', 'manufacturer', 'role')
|
||||||
table = tables.InventoryItemTable
|
table = tables.InventoryItemTable
|
||||||
template_name = 'dcim/inventoryitem_bulk_delete.html'
|
template_name = 'dcim/inventoryitem_bulk_delete.html'
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Inventory item roles
|
||||||
|
#
|
||||||
|
|
||||||
|
class InventoryItemRoleListView(generic.ObjectListView):
|
||||||
|
queryset = InventoryItemRole.objects.annotate(
|
||||||
|
inventoryitem_count=count_related(InventoryItem, 'role'),
|
||||||
|
)
|
||||||
|
filterset = filtersets.InventoryItemRoleFilterSet
|
||||||
|
filterset_form = forms.InventoryItemRoleFilterForm
|
||||||
|
table = tables.InventoryItemRoleTable
|
||||||
|
|
||||||
|
|
||||||
|
class InventoryItemRoleView(generic.ObjectView):
|
||||||
|
queryset = InventoryItemRole.objects.all()
|
||||||
|
|
||||||
|
def get_extra_context(self, request, instance):
|
||||||
|
return {
|
||||||
|
'inventoryitem_count': InventoryItem.objects.filter(role=instance).count(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class InventoryItemRoleEditView(generic.ObjectEditView):
|
||||||
|
queryset = InventoryItemRole.objects.all()
|
||||||
|
model_form = forms.InventoryItemRoleForm
|
||||||
|
|
||||||
|
|
||||||
|
class InventoryItemRoleDeleteView(generic.ObjectDeleteView):
|
||||||
|
queryset = InventoryItemRole.objects.all()
|
||||||
|
|
||||||
|
|
||||||
|
class InventoryItemRoleBulkImportView(generic.BulkImportView):
|
||||||
|
queryset = InventoryItemRole.objects.all()
|
||||||
|
model_form = forms.InventoryItemRoleCSVForm
|
||||||
|
table = tables.InventoryItemRoleTable
|
||||||
|
|
||||||
|
|
||||||
|
class InventoryItemRoleBulkEditView(generic.BulkEditView):
|
||||||
|
queryset = InventoryItemRole.objects.annotate(
|
||||||
|
inventoryitem_count=count_related(InventoryItem, 'role'),
|
||||||
|
)
|
||||||
|
filterset = filtersets.InventoryItemRoleFilterSet
|
||||||
|
table = tables.InventoryItemRoleTable
|
||||||
|
form = forms.InventoryItemRoleBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
|
class InventoryItemRoleBulkDeleteView(generic.BulkDeleteView):
|
||||||
|
queryset = InventoryItemRole.objects.annotate(
|
||||||
|
inventoryitem_count=count_related(InventoryItem, 'role'),
|
||||||
|
)
|
||||||
|
table = tables.InventoryItemRoleTable
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Bulk Device component creation
|
# Bulk Device component creation
|
||||||
#
|
#
|
||||||
|
@ -166,6 +166,7 @@ DEVICES_MENU = Menu(
|
|||||||
get_model_item('dcim', 'modulebay', 'Module Bays', actions=['import']),
|
get_model_item('dcim', 'modulebay', 'Module Bays', actions=['import']),
|
||||||
get_model_item('dcim', 'devicebay', 'Device Bays', actions=['import']),
|
get_model_item('dcim', 'devicebay', 'Device Bays', actions=['import']),
|
||||||
get_model_item('dcim', 'inventoryitem', 'Inventory Items', actions=['import']),
|
get_model_item('dcim', 'inventoryitem', 'Inventory Items', actions=['import']),
|
||||||
|
get_model_item('dcim', 'inventoryitemrole', 'Inventory Item Roles'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -13,9 +13,7 @@
|
|||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="col col-md-6">
|
<div class="col col-md-6">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h5 class="card-header">
|
<h5 class="card-header">Inventory Item</h5>
|
||||||
Inventory Item
|
|
||||||
</h5>
|
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<table class="table table-hover attr-table">
|
<table class="table table-hover attr-table">
|
||||||
<tr>
|
<tr>
|
||||||
@ -42,6 +40,16 @@
|
|||||||
<th scope="row">Label</th>
|
<th scope="row">Label</th>
|
||||||
<td>{{ object.label|placeholder }}</td>
|
<td>{{ object.label|placeholder }}</td>
|
||||||
</tr>
|
</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">—</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Manufacturer</th>
|
<th scope="row">Manufacturer</th>
|
||||||
<td>
|
<td>
|
||||||
|
53
netbox/templates/dcim/inventoryitemrole.html
Normal file
53
netbox/templates/dcim/inventoryitemrole.html
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
{% extends 'generic/object.html' %}
|
||||||
|
{% load helpers %}
|
||||||
|
{% load plugins %}
|
||||||
|
{% load render_table from django_tables2 %}
|
||||||
|
|
||||||
|
{% block breadcrumbs %}
|
||||||
|
<li class="breadcrumb-item"><a href="{% url 'dcim:inventoryitemrole_list' %}">Inventory Item Roles</a></li>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<h5 class="card-header">Inventory Item Role</h5>
|
||||||
|
<div class="card-body">
|
||||||
|
<table class="table table-hover attr-table">
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Name</th>
|
||||||
|
<td>{{ object.name }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Description</th>
|
||||||
|
<td>{{ object.description|placeholder }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Color</th>
|
||||||
|
<td>
|
||||||
|
<span class="badge color-label" style="background-color: #{{ object.color }}"> </span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Inventory Items</th>
|
||||||
|
<td>
|
||||||
|
<a href="{% url 'dcim:inventoryitem_list' %}?role_id={{ object.pk }}">{{ inventoryitem_count }}</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% include 'inc/panels/tags.html' %}
|
||||||
|
{% plugin_left_page object %}
|
||||||
|
</div>
|
||||||
|
<div class="col col-md-6">
|
||||||
|
{% include 'inc/panels/custom_fields.html' %}
|
||||||
|
{% plugin_right_page object %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col col-md-12">
|
||||||
|
{% plugin_full_width_page object %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
Loading…
Reference in New Issue
Block a user