mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-22 20:12:00 -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.
|
||||
|
||||
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.
|
||||
|
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.
|
||||
|
||||
#### 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))
|
||||
|
||||
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
|
||||
|
||||
* Added the following endpoints for modules & module types:
|
||||
* Added the following endpoints:
|
||||
* `/api/dcim/inventory-item-roles/`
|
||||
* `/api/dcim/modules/`
|
||||
* `/api/dcim/module-bays/`
|
||||
* `/api/dcim/module-bay-templates/`
|
||||
@ -70,6 +75,8 @@ FIELD_CHOICES = {
|
||||
* Added `module` field
|
||||
* dcim.Interface
|
||||
* Added `module` field
|
||||
* dcim.InventoryItem
|
||||
* Added `role` field
|
||||
* dcim.PowerPort
|
||||
* Added `module` field
|
||||
* dcim.PowerOutlet
|
||||
|
@ -20,6 +20,7 @@ __all__ = [
|
||||
'NestedInterfaceSerializer',
|
||||
'NestedInterfaceTemplateSerializer',
|
||||
'NestedInventoryItemSerializer',
|
||||
'NestedInventoryItemRoleSerializer',
|
||||
'NestedManufacturerSerializer',
|
||||
'NestedModuleBaySerializer',
|
||||
'NestedModuleBayTemplateSerializer',
|
||||
@ -384,6 +385,15 @@ class NestedInventoryItemSerializer(WritableNestedSerializer):
|
||||
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
|
||||
#
|
||||
|
@ -806,25 +806,38 @@ class DeviceBaySerializer(PrimaryModelSerializer):
|
||||
]
|
||||
|
||||
|
||||
#
|
||||
# Inventory items
|
||||
#
|
||||
|
||||
class InventoryItemSerializer(PrimaryModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitem-detail')
|
||||
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',
|
||||
]
|
||||
|
||||
|
||||
#
|
||||
# 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
|
||||
#
|
||||
|
@ -50,6 +50,9 @@ router.register('module-bays', views.ModuleBayViewSet)
|
||||
router.register('device-bays', views.DeviceBayViewSet)
|
||||
router.register('inventory-items', views.InventoryItemViewSet)
|
||||
|
||||
# Device component roles
|
||||
router.register('inventory-item-roles', views.InventoryItemRoleViewSet)
|
||||
|
||||
# Cables
|
||||
router.register('cables', views.CableViewSet)
|
||||
|
||||
|
@ -623,6 +623,18 @@ class InventoryItemViewSet(ModelViewSet):
|
||||
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
|
||||
#
|
||||
|
@ -39,6 +39,7 @@ __all__ = (
|
||||
'InterfaceFilterSet',
|
||||
'InterfaceTemplateFilterSet',
|
||||
'InventoryItemFilterSet',
|
||||
'InventoryItemRoleFilterSet',
|
||||
'LocationFilterSet',
|
||||
'ManufacturerFilterSet',
|
||||
'ModuleBayFilterSet',
|
||||
@ -1283,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'
|
||||
)
|
||||
@ -1304,6 +1315,14 @@ class InventoryItemFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet):
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
|
||||
class InventoryItemRoleFilterSet(OrganizationalModelFilterSet):
|
||||
tag = TagFilter()
|
||||
|
||||
class Meta:
|
||||
model = InventoryItemRole
|
||||
fields = ['id', 'name', 'slug', 'color']
|
||||
|
||||
|
||||
class VirtualChassisFilterSet(PrimaryModelFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
|
@ -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',
|
||||
)
|
||||
|
@ -30,6 +30,7 @@ __all__ = (
|
||||
'InterfaceBulkEditForm',
|
||||
'InterfaceTemplateBulkEditForm',
|
||||
'InventoryItemBulkEditForm',
|
||||
'InventoryItemRoleBulkEditForm',
|
||||
'LocationBulkEditForm',
|
||||
'ManufacturerBulkEditForm',
|
||||
'ModuleBulkEditForm',
|
||||
@ -1171,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
|
||||
):
|
||||
@ -1179,10 +1180,35 @@ 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']
|
||||
|
||||
|
||||
#
|
||||
# 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',
|
||||
'InterfaceCSVForm',
|
||||
'InventoryItemCSVForm',
|
||||
'InventoryItemRoleCSVForm',
|
||||
'LocationCSVForm',
|
||||
'ManufacturerCSVForm',
|
||||
'ModuleCSVForm',
|
||||
@ -771,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',
|
||||
@ -786,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):
|
||||
@ -805,6 +812,25 @@ class InventoryItemCSVForm(CustomFieldModelCSVForm):
|
||||
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):
|
||||
# Termination A
|
||||
side_a_device = CSVModelChoiceField(
|
||||
@ -906,6 +932,10 @@ class CableCSVForm(CustomFieldModelCSVForm):
|
||||
return length_unit if length_unit is not None else ''
|
||||
|
||||
|
||||
#
|
||||
# Virtual chassis
|
||||
#
|
||||
|
||||
class VirtualChassisCSVForm(CustomFieldModelCSVForm):
|
||||
master = CSVModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
@ -919,6 +949,10 @@ class VirtualChassisCSVForm(CustomFieldModelCSVForm):
|
||||
fields = ('name', 'domain', 'master')
|
||||
|
||||
|
||||
#
|
||||
# Power
|
||||
#
|
||||
|
||||
class PowerPanelCSVForm(CustomFieldModelCSVForm):
|
||||
site = CSVModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
|
@ -27,6 +27,7 @@ __all__ = (
|
||||
'InterfaceConnectionFilterForm',
|
||||
'InterfaceFilterForm',
|
||||
'InventoryItemFilterForm',
|
||||
'InventoryItemRoleFilterForm',
|
||||
'LocationFilterForm',
|
||||
'ManufacturerFilterForm',
|
||||
'ModuleFilterForm',
|
||||
@ -1099,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,
|
||||
@ -1120,6 +1127,15 @@ class InventoryItemFilterForm(DeviceComponentFilterForm):
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
#
|
||||
# Device component roles
|
||||
#
|
||||
|
||||
class InventoryItemRoleFilterForm(CustomFieldModelFilterForm):
|
||||
model = InventoryItemRole
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
#
|
||||
# Connections
|
||||
#
|
||||
|
@ -37,6 +37,7 @@ __all__ = (
|
||||
'InterfaceForm',
|
||||
'InterfaceTemplateForm',
|
||||
'InventoryItemForm',
|
||||
'InventoryItemRoleForm',
|
||||
'LocationForm',
|
||||
'ManufacturerForm',
|
||||
'ModuleForm',
|
||||
@ -1367,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
|
||||
@ -1379,6 +1384,24 @@ 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',
|
||||
]
|
||||
|
||||
|
||||
#
|
||||
# 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):
|
||||
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',
|
||||
)
|
||||
|
@ -50,6 +50,9 @@ class DCIMQuery(graphene.ObjectType):
|
||||
inventory_item = ObjectField(InventoryItemType)
|
||||
inventory_item_list = ObjectListField(InventoryItemType)
|
||||
|
||||
inventory_item_role = ObjectField(InventoryItemRoleType)
|
||||
inventory_item_role_list = ObjectListField(InventoryItemRoleType)
|
||||
|
||||
location = ObjectField(LocationType)
|
||||
location_list = ObjectListField(LocationType)
|
||||
|
||||
|
@ -25,6 +25,7 @@ __all__ = (
|
||||
'InterfaceType',
|
||||
'InterfaceTemplateType',
|
||||
'InventoryItemType',
|
||||
'InventoryItemRoleType',
|
||||
'LocationType',
|
||||
'ManufacturerType',
|
||||
'ModuleType',
|
||||
@ -242,6 +243,14 @@ class InventoryItemType(ComponentObjectType):
|
||||
filterset_class = filtersets.InventoryItemFilterSet
|
||||
|
||||
|
||||
class InventoryItemRoleType(OrganizationalObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.InventoryItemRole
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.InventoryItemRoleFilterSet
|
||||
|
||||
|
||||
class LocationType(VLANGroupsMixin, ImageAttachmentsMixin, OrganizationalObjectType):
|
||||
|
||||
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.svg import CableTraceSVG
|
||||
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.mptt import TreeManager
|
||||
from utilities.ordering import naturalize_interface
|
||||
@ -30,6 +31,7 @@ __all__ = (
|
||||
'FrontPort',
|
||||
'Interface',
|
||||
'InventoryItem',
|
||||
'InventoryItemRole',
|
||||
'ModuleBay',
|
||||
'PathEndpoint',
|
||||
'PowerOutlet',
|
||||
@ -946,6 +948,38 @@ class DeviceBay(ComponentModel):
|
||||
# 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')
|
||||
class InventoryItem(MPTTModel, ComponentModel):
|
||||
"""
|
||||
@ -960,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,
|
||||
@ -993,7 +1034,7 @@ class InventoryItem(MPTTModel, ComponentModel):
|
||||
|
||||
objects = TreeManager()
|
||||
|
||||
clone_fields = ['device', 'parent', 'manufacturer', 'part_id']
|
||||
clone_fields = ['device', 'parent', 'role', 'manufacturer', 'part_id']
|
||||
|
||||
class Meta:
|
||||
ordering = ('device__id', 'parent__id', '_name')
|
||||
|
@ -2,8 +2,8 @@ import django_tables2 as tables
|
||||
from django_tables2.utils import Accessor
|
||||
|
||||
from dcim.models import (
|
||||
ConsolePort, ConsoleServerPort, Device, DeviceBay, DeviceRole, FrontPort, Interface, InventoryItem, ModuleBay,
|
||||
Platform, PowerOutlet, PowerPort, RearPort, VirtualChassis,
|
||||
ConsolePort, ConsoleServerPort, Device, DeviceBay, DeviceRole, FrontPort, Interface, InventoryItem,
|
||||
InventoryItemRole, ModuleBay, Platform, PowerOutlet, PowerPort, RearPort, VirtualChassis,
|
||||
)
|
||||
from tenancy.tables import TenantColumn
|
||||
from utilities.tables import (
|
||||
@ -33,6 +33,7 @@ __all__ = (
|
||||
'DeviceTable',
|
||||
'FrontPortTable',
|
||||
'InterfaceTable',
|
||||
'InventoryItemRoleTable',
|
||||
'InventoryItemTable',
|
||||
'ModuleBayTable',
|
||||
'PlatformTable',
|
||||
@ -68,11 +69,11 @@ def get_interface_state_attribute(record):
|
||||
else:
|
||||
return "disabled"
|
||||
|
||||
|
||||
#
|
||||
# Device roles
|
||||
#
|
||||
|
||||
|
||||
class DeviceRoleTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.Column(
|
||||
@ -773,6 +774,9 @@ class InventoryItemTable(DeviceComponentTable):
|
||||
'args': [Accessor('device_id')],
|
||||
}
|
||||
)
|
||||
role = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
manufacturer = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
@ -785,10 +789,10 @@ 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', 'manufacturer', 'part_id', 'serial', 'asset_tag')
|
||||
default_columns = ('pk', 'name', 'device', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag')
|
||||
|
||||
|
||||
class DeviceInventoryItemTable(InventoryItemTable):
|
||||
@ -806,15 +810,38 @@ class DeviceInventoryItemTable(InventoryItemTable):
|
||||
class Meta(BaseTable.Meta):
|
||||
model = InventoryItem
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'label', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description', 'discovered',
|
||||
'tags', 'actions',
|
||||
'pk', 'id', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description',
|
||||
'discovered', 'tags', 'actions',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'name', 'label', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description', 'discovered',
|
||||
'actions',
|
||||
'pk', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description', '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
|
||||
#
|
||||
|
@ -1626,29 +1626,73 @@ 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,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
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):
|
||||
model = Cable
|
||||
brief_fields = ['display', 'id', 'label', 'url']
|
||||
|
@ -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]}
|
||||
@ -3091,6 +3104,33 @@ class InventoryItemTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
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):
|
||||
queryset = VirtualChassis.objects.all()
|
||||
filterset = VirtualChassisFilterSet
|
||||
|
@ -1408,7 +1408,7 @@ class DeviceRoleTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
||||
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||
|
||||
cls.form_data = {
|
||||
'name': 'Devie Role X',
|
||||
'name': 'Device Role X',
|
||||
'slug': 'device-role-x',
|
||||
'color': 'c0c0c0',
|
||||
'vm_role': False,
|
||||
@ -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',
|
||||
}
|
||||
@ -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
|
||||
# Blocked by lack of common creation view for cables (termination A must be initialized)
|
||||
class CableTestCase(
|
||||
|
@ -425,6 +425,17 @@ urlpatterns = [
|
||||
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'),
|
||||
|
||||
# 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
|
||||
path('cables/', views.CableListView.as_view(), name='cable_list'),
|
||||
path('cables/import/', views.CableBulkImportView.as_view(), name='cable_import'),
|
||||
|
@ -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,11 +2423,64 @@ 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'
|
||||
|
||||
|
||||
#
|
||||
# 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
|
||||
#
|
||||
|
@ -166,6 +166,7 @@ DEVICES_MENU = Menu(
|
||||
get_model_item('dcim', 'modulebay', 'Module 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', 'inventoryitemrole', 'Inventory Item Roles'),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -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">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Manufacturer</th>
|
||||
<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