mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-14 01:41:22 -06:00
#3087: Add InvetoryItemRole
This commit is contained in:
parent
77dd684916
commit
04fb5e544d
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.
|
@ -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,10 +806,6 @@ class DeviceBaySerializer(PrimaryModelSerializer):
|
||||
]
|
||||
|
||||
|
||||
#
|
||||
# Inventory items
|
||||
#
|
||||
|
||||
class InventoryItemSerializer(PrimaryModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitem-detail')
|
||||
device = NestedDeviceSerializer()
|
||||
@ -825,6 +821,22 @@ class InventoryItemSerializer(PrimaryModelSerializer):
|
||||
]
|
||||
|
||||
|
||||
#
|
||||
# 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',
|
||||
@ -1304,6 +1305,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',
|
||||
|
@ -30,6 +30,7 @@ __all__ = (
|
||||
'InterfaceBulkEditForm',
|
||||
'InterfaceTemplateBulkEditForm',
|
||||
'InventoryItemBulkEditForm',
|
||||
'InventoryItemRoleBulkEditForm',
|
||||
'LocationBulkEditForm',
|
||||
'ManufacturerBulkEditForm',
|
||||
'ModuleBulkEditForm',
|
||||
@ -1186,3 +1187,24 @@ class InventoryItemBulkEditForm(
|
||||
|
||||
class Meta:
|
||||
nullable_fields = ['label', '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',
|
||||
@ -805,6 +806,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 +926,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 +943,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',
|
||||
@ -1120,6 +1121,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',
|
||||
@ -1382,3 +1383,21 @@ class InventoryItemForm(CustomFieldModelForm):
|
||||
'device', 'parent', 'name', 'label', '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',
|
||||
]
|
||||
|
@ -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):
|
||||
"""
|
||||
@ -973,6 +1007,13 @@ class InventoryItem(MPTTModel, ComponentModel):
|
||||
blank=True,
|
||||
help_text='Manufacturer-assigned part identifier'
|
||||
)
|
||||
role = models.ForeignKey(
|
||||
to='dcim.InventoryItemRole',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='inventory_items',
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
serial = models.CharField(
|
||||
max_length=50,
|
||||
verbose_name='Serial number',
|
||||
@ -993,7 +1034,7 @@ class InventoryItem(MPTTModel, ComponentModel):
|
||||
|
||||
objects = TreeManager()
|
||||
|
||||
clone_fields = ['device', 'parent', 'manufacturer', 'part_id']
|
||||
clone_fields = ['device', 'parent', 'manufacturer', 'part_id', 'role']
|
||||
|
||||
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(
|
||||
@ -791,6 +792,30 @@ class InventoryItemTable(DeviceComponentTable):
|
||||
default_columns = ('pk', 'name', 'device', 'label', 'manufacturer', 'part_id', 'serial', 'asset_tag')
|
||||
|
||||
|
||||
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')
|
||||
|
||||
|
||||
class DeviceInventoryItemTable(InventoryItemTable):
|
||||
name = tables.TemplateColumn(
|
||||
template_code='<a href="{{ record.get_absolute_url }}" style="padding-left: {{ record.level }}0px">'
|
||||
|
@ -1649,6 +1649,41 @@ class InventoryItemTest(APIViewTestCases.APIViewTestCase):
|
||||
]
|
||||
|
||||
|
||||
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']
|
||||
|
@ -3091,6 +3091,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,
|
||||
@ -2375,6 +2375,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'),
|
||||
|
@ -2428,6 +2428,59 @@ class InventoryItemBulkDeleteView(generic.BulkDeleteView):
|
||||
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'),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
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