mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-24 17:38:37 -06:00
Initial work on #7846
This commit is contained in:
parent
a58f1c6a7d
commit
99d5013de3
@ -810,17 +810,32 @@ 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)
|
|
||||||
role = NestedInventoryItemRoleSerializer(required=False, allow_null=True)
|
role = NestedInventoryItemRoleSerializer(required=False, allow_null=True)
|
||||||
|
manufacturer = NestedManufacturerSerializer(required=False, allow_null=True, default=None)
|
||||||
|
component_type = ContentTypeField(
|
||||||
|
queryset=ContentType.objects.filter(MODULAR_COMPONENT_MODELS),
|
||||||
|
required=False,
|
||||||
|
allow_null=True
|
||||||
|
)
|
||||||
|
component = serializers.SerializerMethodField(read_only=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', 'role', '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', 'component_type', 'component_id', 'component', 'tags',
|
||||||
|
'custom_fields', 'created', 'last_updated', '_depth',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@swagger_serializer_method(serializer_or_field=serializers.DictField)
|
||||||
|
def get_component(self, obj):
|
||||||
|
if obj.component is None:
|
||||||
|
return None
|
||||||
|
serializer = get_serializer_for_model(obj.component, prefix='Nested')
|
||||||
|
context = {'request': self.context['request']}
|
||||||
|
return serializer(obj.component, context=context).data
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Device component roles
|
# Device component roles
|
||||||
|
@ -50,16 +50,31 @@ NONCONNECTABLE_IFACE_TYPES = VIRTUAL_IFACE_TYPES + WIRELESS_IFACE_TYPES
|
|||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# PowerFeeds
|
# Power feeds
|
||||||
#
|
#
|
||||||
|
|
||||||
POWERFEED_VOLTAGE_DEFAULT = 120
|
POWERFEED_VOLTAGE_DEFAULT = 120
|
||||||
|
|
||||||
POWERFEED_AMPERAGE_DEFAULT = 20
|
POWERFEED_AMPERAGE_DEFAULT = 20
|
||||||
|
|
||||||
POWERFEED_MAX_UTILIZATION_DEFAULT = 80 # Percentage
|
POWERFEED_MAX_UTILIZATION_DEFAULT = 80 # Percentage
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Device components
|
||||||
|
#
|
||||||
|
|
||||||
|
MODULAR_COMPONENT_MODELS = Q(
|
||||||
|
app_label='dcim',
|
||||||
|
model__in=(
|
||||||
|
'consoleport',
|
||||||
|
'consoleserverport',
|
||||||
|
'frontport',
|
||||||
|
'interface',
|
||||||
|
'poweroutlet',
|
||||||
|
'powerport',
|
||||||
|
'rearport',
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Cabling and connections
|
# Cabling and connections
|
||||||
#
|
#
|
||||||
|
@ -1294,6 +1294,8 @@ class InventoryItemFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet):
|
|||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Role (slug)',
|
label='Role (slug)',
|
||||||
)
|
)
|
||||||
|
component_type = ContentTypeFilter()
|
||||||
|
component_id = MultiValueNumberFilter()
|
||||||
serial = django_filters.CharFilter(
|
serial = django_filters.CharFilter(
|
||||||
lookup_expr='iexact'
|
lookup_expr='iexact'
|
||||||
)
|
)
|
||||||
|
@ -12,8 +12,8 @@ from extras.models import Tag
|
|||||||
from ipam.models import IPAddress, VLAN, VLANGroup, ASN
|
from ipam.models import IPAddress, VLAN, VLANGroup, ASN
|
||||||
from tenancy.forms import TenancyForm
|
from tenancy.forms import TenancyForm
|
||||||
from utilities.forms import (
|
from utilities.forms import (
|
||||||
APISelect, add_blank_choice, BootstrapMixin, ClearableFileInput, CommentField, DynamicModelChoiceField,
|
APISelect, add_blank_choice, BootstrapMixin, ClearableFileInput, CommentField, ContentTypeChoiceField,
|
||||||
DynamicModelMultipleChoiceField, JSONField, NumericArrayField, SelectWithPK, SmallTextarea,
|
DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField, NumericArrayField, SelectWithPK, SmallTextarea,
|
||||||
SlugField, StaticSelect,
|
SlugField, StaticSelect,
|
||||||
)
|
)
|
||||||
from virtualization.models import Cluster, ClusterGroup
|
from virtualization.models import Cluster, ClusterGroup
|
||||||
@ -1376,6 +1376,15 @@ class InventoryItemForm(CustomFieldModelForm):
|
|||||||
queryset=Manufacturer.objects.all(),
|
queryset=Manufacturer.objects.all(),
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
|
component_type = ContentTypeChoiceField(
|
||||||
|
queryset=ContentType.objects.all(),
|
||||||
|
limit_choices_to=MODULAR_COMPONENT_MODELS,
|
||||||
|
required=False,
|
||||||
|
widget=StaticSelect
|
||||||
|
)
|
||||||
|
component_id = forms.IntegerField(
|
||||||
|
required=False
|
||||||
|
)
|
||||||
tags = DynamicModelMultipleChoiceField(
|
tags = DynamicModelMultipleChoiceField(
|
||||||
queryset=Tag.objects.all(),
|
queryset=Tag.objects.all(),
|
||||||
required=False
|
required=False
|
||||||
@ -1385,8 +1394,13 @@ class InventoryItemForm(CustomFieldModelForm):
|
|||||||
model = InventoryItem
|
model = InventoryItem
|
||||||
fields = [
|
fields = [
|
||||||
'device', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag',
|
'device', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag',
|
||||||
'description', 'tags',
|
'description', 'component_type', 'component_id', 'tags',
|
||||||
]
|
]
|
||||||
|
fieldsets = (
|
||||||
|
('Inventory Item', ('device', 'parent', 'name', 'label', 'role', 'description', 'tags')),
|
||||||
|
('Hardware', ('manufacturer', 'part_id', 'serial', 'asset_tag')),
|
||||||
|
('Component', ('component_type', 'component_id')),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
|
||||||
from dcim.choices import *
|
from dcim.choices import *
|
||||||
from dcim.constants import *
|
from dcim.constants import *
|
||||||
@ -7,8 +8,8 @@ from extras.forms import CustomFieldModelForm, CustomFieldsMixin
|
|||||||
from extras.models import Tag
|
from extras.models import Tag
|
||||||
from ipam.models import VLAN
|
from ipam.models import VLAN
|
||||||
from utilities.forms import (
|
from utilities.forms import (
|
||||||
add_blank_choice, BootstrapMixin, ColorField, DynamicModelChoiceField, DynamicModelMultipleChoiceField,
|
add_blank_choice, BootstrapMixin, ColorField, ContentTypeChoiceField, DynamicModelChoiceField,
|
||||||
ExpandableNameField, StaticSelect,
|
DynamicModelMultipleChoiceField, ExpandableNameField, StaticSelect,
|
||||||
)
|
)
|
||||||
from wireless.choices import *
|
from wireless.choices import *
|
||||||
from .common import InterfaceCommonForm
|
from .common import InterfaceCommonForm
|
||||||
@ -680,7 +681,16 @@ class InventoryItemCreateForm(ComponentCreateForm):
|
|||||||
max_length=50,
|
max_length=50,
|
||||||
required=False,
|
required=False,
|
||||||
)
|
)
|
||||||
|
component_type = ContentTypeChoiceField(
|
||||||
|
queryset=ContentType.objects.all(),
|
||||||
|
limit_choices_to=MODULAR_COMPONENT_MODELS,
|
||||||
|
required=False,
|
||||||
|
widget=StaticSelect
|
||||||
|
)
|
||||||
|
component_id = forms.IntegerField(
|
||||||
|
required=False
|
||||||
|
)
|
||||||
field_order = (
|
field_order = (
|
||||||
'device', 'parent', 'name_pattern', 'label_pattern', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag',
|
'device', 'parent', 'name_pattern', 'label_pattern', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag',
|
||||||
'description', 'tags',
|
'description', 'component_type', 'component_id', 'tags',
|
||||||
)
|
)
|
||||||
|
23
netbox/dcim/migrations/0147_inventoryitem_component.py
Normal file
23
netbox/dcim/migrations/0147_inventoryitem_component.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('contenttypes', '0002_remove_content_type_name'),
|
||||||
|
('dcim', '0146_inventoryitemrole'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='inventoryitem',
|
||||||
|
name='component_id',
|
||||||
|
field=models.PositiveBigIntegerField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='inventoryitem',
|
||||||
|
name='component_type',
|
||||||
|
field=models.ForeignKey(blank=True, limit_choices_to=models.Q(('app_label', 'dcim'), ('model__in', ('consoleport', 'consoleserverport', 'frontport', 'interface', 'poweroutlet', 'powerport', 'rearport'))), null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.contenttype'),
|
||||||
|
),
|
||||||
|
]
|
@ -97,6 +97,12 @@ class ModularComponentModel(ComponentModel):
|
|||||||
blank=True,
|
blank=True,
|
||||||
null=True
|
null=True
|
||||||
)
|
)
|
||||||
|
inventory_items = GenericRelation(
|
||||||
|
to='dcim.InventoryItem',
|
||||||
|
content_type_field='component_type',
|
||||||
|
object_id_field='component_id',
|
||||||
|
related_name='%(class)ss',
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
abstract = True
|
abstract = True
|
||||||
@ -994,6 +1000,22 @@ class InventoryItem(MPTTModel, ComponentModel):
|
|||||||
null=True,
|
null=True,
|
||||||
db_index=True
|
db_index=True
|
||||||
)
|
)
|
||||||
|
component_type = models.ForeignKey(
|
||||||
|
to=ContentType,
|
||||||
|
limit_choices_to=MODULAR_COMPONENT_MODELS,
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
related_name='+',
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
|
component_id = models.PositiveBigIntegerField(
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
|
component = GenericForeignKey(
|
||||||
|
ct_field='component_type',
|
||||||
|
fk_field='component_id'
|
||||||
|
)
|
||||||
role = models.ForeignKey(
|
role = models.ForeignKey(
|
||||||
to='dcim.InventoryItemRole',
|
to='dcim.InventoryItemRole',
|
||||||
on_delete=models.PROTECT,
|
on_delete=models.PROTECT,
|
||||||
|
@ -780,6 +780,9 @@ class InventoryItemTable(DeviceComponentTable):
|
|||||||
manufacturer = tables.Column(
|
manufacturer = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
|
component = tables.Column(
|
||||||
|
linkify=True
|
||||||
|
)
|
||||||
discovered = BooleanColumn()
|
discovered = BooleanColumn()
|
||||||
tags = TagColumn(
|
tags = TagColumn(
|
||||||
url_name='dcim:inventoryitem_list'
|
url_name='dcim:inventoryitem_list'
|
||||||
@ -790,9 +793,11 @@ class InventoryItemTable(DeviceComponentTable):
|
|||||||
model = InventoryItem
|
model = InventoryItem
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'name', 'device', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag',
|
'pk', 'id', 'name', 'device', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag',
|
||||||
'description', 'discovered', 'tags',
|
'component', 'description', 'discovered', 'tags',
|
||||||
|
)
|
||||||
|
default_columns = (
|
||||||
|
'pk', 'name', 'device', 'label', 'role', '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):
|
||||||
@ -810,11 +815,11 @@ class DeviceInventoryItemTable(InventoryItemTable):
|
|||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = InventoryItem
|
model = InventoryItem
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description',
|
'pk', 'id', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'component',
|
||||||
'discovered', 'tags', 'actions',
|
'description', 'discovered', 'tags', 'actions',
|
||||||
)
|
)
|
||||||
default_columns = (
|
default_columns = (
|
||||||
'pk', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description', 'actions',
|
'pk', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'component', 'actions',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -1632,9 +1632,16 @@ class InventoryItemTest(APIViewTestCases.APIViewTestCase):
|
|||||||
)
|
)
|
||||||
InventoryItemRole.objects.bulk_create(roles)
|
InventoryItemRole.objects.bulk_create(roles)
|
||||||
|
|
||||||
InventoryItem.objects.create(device=device, name='Inventory Item 1', role=roles[0], manufacturer=manufacturer)
|
interfaces = (
|
||||||
InventoryItem.objects.create(device=device, name='Inventory Item 2', role=roles[0], manufacturer=manufacturer)
|
Interface(device=device, name='Interface 1'),
|
||||||
InventoryItem.objects.create(device=device, name='Inventory Item 3', role=roles[0], manufacturer=manufacturer)
|
Interface(device=device, name='Interface 2'),
|
||||||
|
Interface(device=device, name='Interface 3'),
|
||||||
|
)
|
||||||
|
Interface.objects.bulk_create(interfaces)
|
||||||
|
|
||||||
|
InventoryItem.objects.create(device=device, name='Inventory Item 1', role=roles[0], manufacturer=manufacturer, component=interfaces[0])
|
||||||
|
InventoryItem.objects.create(device=device, name='Inventory Item 2', role=roles[0], manufacturer=manufacturer, component=interfaces[1])
|
||||||
|
InventoryItem.objects.create(device=device, name='Inventory Item 3', role=roles[0], manufacturer=manufacturer, component=interfaces[2])
|
||||||
|
|
||||||
cls.create_data = [
|
cls.create_data = [
|
||||||
{
|
{
|
||||||
@ -1642,18 +1649,24 @@ class InventoryItemTest(APIViewTestCases.APIViewTestCase):
|
|||||||
'name': 'Inventory Item 4',
|
'name': 'Inventory Item 4',
|
||||||
'role': roles[1].pk,
|
'role': roles[1].pk,
|
||||||
'manufacturer': manufacturer.pk,
|
'manufacturer': manufacturer.pk,
|
||||||
|
'component_type': 'dcim.interface',
|
||||||
|
'component_id': interfaces[0].pk,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'device': device.pk,
|
'device': device.pk,
|
||||||
'name': 'Inventory Item 5',
|
'name': 'Inventory Item 5',
|
||||||
'role': roles[1].pk,
|
'role': roles[1].pk,
|
||||||
'manufacturer': manufacturer.pk,
|
'manufacturer': manufacturer.pk,
|
||||||
|
'component_type': 'dcim.interface',
|
||||||
|
'component_id': interfaces[1].pk,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'device': device.pk,
|
'device': device.pk,
|
||||||
'name': 'Inventory Item 6',
|
'name': 'Inventory Item 6',
|
||||||
'role': roles[1].pk,
|
'role': roles[1].pk,
|
||||||
'manufacturer': manufacturer.pk,
|
'manufacturer': manufacturer.pk,
|
||||||
|
'component_type': 'dcim.interface',
|
||||||
|
'component_id': interfaces[2].pk,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -3004,10 +3004,16 @@ class InventoryItemTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
)
|
)
|
||||||
InventoryItemRole.objects.bulk_create(roles)
|
InventoryItemRole.objects.bulk_create(roles)
|
||||||
|
|
||||||
|
components = (
|
||||||
|
Interface.objects.create(device=devices[0], name='Interface 1'),
|
||||||
|
ConsolePort.objects.create(device=devices[1], name='Console Port 1'),
|
||||||
|
ConsoleServerPort.objects.create(device=devices[2], name='Console Server Port 1'),
|
||||||
|
)
|
||||||
|
|
||||||
inventory_items = (
|
inventory_items = (
|
||||||
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[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', component=components[0]),
|
||||||
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[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', component=components[1]),
|
||||||
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'),
|
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', component=components[2]),
|
||||||
)
|
)
|
||||||
for i in inventory_items:
|
for i in inventory_items:
|
||||||
i.save()
|
i.save()
|
||||||
@ -3103,6 +3109,10 @@ class InventoryItemTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
params = {'serial': 'abc'}
|
params = {'serial': 'abc'}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||||
|
|
||||||
|
def test_component_type(self):
|
||||||
|
params = {'component_type': 'dcim.interface'}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||||
|
|
||||||
|
|
||||||
class InventoryItemRoleTestCase(TestCase, ChangeLoggedFilterSetTests):
|
class InventoryItemRoleTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||||
queryset = InventoryItemRole.objects.all()
|
queryset = InventoryItemRole.objects.all()
|
||||||
|
@ -50,6 +50,16 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Component</th>
|
||||||
|
<td>
|
||||||
|
{% if object.component %}
|
||||||
|
<a href="{{ object.component.get_absolute_url }}">{{ object.component }}</a>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">—</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Manufacturer</th>
|
<th scope="row">Manufacturer</th>
|
||||||
<td>
|
<td>
|
||||||
|
Loading…
Reference in New Issue
Block a user