mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-23 04:22:01 -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')
|
||||
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)
|
||||
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)
|
||||
|
||||
class Meta:
|
||||
model = InventoryItem
|
||||
fields = [
|
||||
'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
|
||||
|
@ -50,16 +50,31 @@ NONCONNECTABLE_IFACE_TYPES = VIRTUAL_IFACE_TYPES + WIRELESS_IFACE_TYPES
|
||||
|
||||
|
||||
#
|
||||
# PowerFeeds
|
||||
# Power feeds
|
||||
#
|
||||
|
||||
POWERFEED_VOLTAGE_DEFAULT = 120
|
||||
|
||||
POWERFEED_AMPERAGE_DEFAULT = 20
|
||||
|
||||
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
|
||||
#
|
||||
|
@ -1294,6 +1294,8 @@ class InventoryItemFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet):
|
||||
to_field_name='slug',
|
||||
label='Role (slug)',
|
||||
)
|
||||
component_type = ContentTypeFilter()
|
||||
component_id = MultiValueNumberFilter()
|
||||
serial = django_filters.CharFilter(
|
||||
lookup_expr='iexact'
|
||||
)
|
||||
|
@ -12,8 +12,8 @@ from extras.models import Tag
|
||||
from ipam.models import IPAddress, VLAN, VLANGroup, ASN
|
||||
from tenancy.forms import TenancyForm
|
||||
from utilities.forms import (
|
||||
APISelect, add_blank_choice, BootstrapMixin, ClearableFileInput, CommentField, DynamicModelChoiceField,
|
||||
DynamicModelMultipleChoiceField, JSONField, NumericArrayField, SelectWithPK, SmallTextarea,
|
||||
APISelect, add_blank_choice, BootstrapMixin, ClearableFileInput, CommentField, ContentTypeChoiceField,
|
||||
DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField, NumericArrayField, SelectWithPK, SmallTextarea,
|
||||
SlugField, StaticSelect,
|
||||
)
|
||||
from virtualization.models import Cluster, ClusterGroup
|
||||
@ -1376,6 +1376,15 @@ class InventoryItemForm(CustomFieldModelForm):
|
||||
queryset=Manufacturer.objects.all(),
|
||||
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(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
@ -1385,8 +1394,13 @@ class InventoryItemForm(CustomFieldModelForm):
|
||||
model = InventoryItem
|
||||
fields = [
|
||||
'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.contrib.contenttypes.models import ContentType
|
||||
|
||||
from dcim.choices import *
|
||||
from dcim.constants import *
|
||||
@ -7,8 +8,8 @@ from extras.forms import CustomFieldModelForm, CustomFieldsMixin
|
||||
from extras.models import Tag
|
||||
from ipam.models import VLAN
|
||||
from utilities.forms import (
|
||||
add_blank_choice, BootstrapMixin, ColorField, DynamicModelChoiceField, DynamicModelMultipleChoiceField,
|
||||
ExpandableNameField, StaticSelect,
|
||||
add_blank_choice, BootstrapMixin, ColorField, ContentTypeChoiceField, DynamicModelChoiceField,
|
||||
DynamicModelMultipleChoiceField, ExpandableNameField, StaticSelect,
|
||||
)
|
||||
from wireless.choices import *
|
||||
from .common import InterfaceCommonForm
|
||||
@ -680,7 +681,16 @@ class InventoryItemCreateForm(ComponentCreateForm):
|
||||
max_length=50,
|
||||
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 = (
|
||||
'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,
|
||||
null=True
|
||||
)
|
||||
inventory_items = GenericRelation(
|
||||
to='dcim.InventoryItem',
|
||||
content_type_field='component_type',
|
||||
object_id_field='component_id',
|
||||
related_name='%(class)ss',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
@ -994,6 +1000,22 @@ class InventoryItem(MPTTModel, ComponentModel):
|
||||
null=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(
|
||||
to='dcim.InventoryItemRole',
|
||||
on_delete=models.PROTECT,
|
||||
|
@ -780,6 +780,9 @@ class InventoryItemTable(DeviceComponentTable):
|
||||
manufacturer = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
component = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
discovered = BooleanColumn()
|
||||
tags = TagColumn(
|
||||
url_name='dcim:inventoryitem_list'
|
||||
@ -790,9 +793,11 @@ class InventoryItemTable(DeviceComponentTable):
|
||||
model = InventoryItem
|
||||
fields = (
|
||||
'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):
|
||||
@ -810,11 +815,11 @@ class DeviceInventoryItemTable(InventoryItemTable):
|
||||
class Meta(BaseTable.Meta):
|
||||
model = InventoryItem
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description',
|
||||
'discovered', 'tags', 'actions',
|
||||
'pk', 'id', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'component',
|
||||
'description', 'discovered', 'tags', 'actions',
|
||||
)
|
||||
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)
|
||||
|
||||
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)
|
||||
interfaces = (
|
||||
Interface(device=device, name='Interface 1'),
|
||||
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 = [
|
||||
{
|
||||
@ -1642,18 +1649,24 @@ class InventoryItemTest(APIViewTestCases.APIViewTestCase):
|
||||
'name': 'Inventory Item 4',
|
||||
'role': roles[1].pk,
|
||||
'manufacturer': manufacturer.pk,
|
||||
'component_type': 'dcim.interface',
|
||||
'component_id': interfaces[0].pk,
|
||||
},
|
||||
{
|
||||
'device': device.pk,
|
||||
'name': 'Inventory Item 5',
|
||||
'role': roles[1].pk,
|
||||
'manufacturer': manufacturer.pk,
|
||||
'component_type': 'dcim.interface',
|
||||
'component_id': interfaces[1].pk,
|
||||
},
|
||||
{
|
||||
'device': device.pk,
|
||||
'name': 'Inventory Item 6',
|
||||
'role': roles[1].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)
|
||||
|
||||
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 = (
|
||||
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'),
|
||||
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', 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', component=components[2]),
|
||||
)
|
||||
for i in inventory_items:
|
||||
i.save()
|
||||
@ -3103,6 +3109,10 @@ class InventoryItemTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
params = {'serial': 'abc'}
|
||||
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):
|
||||
queryset = InventoryItemRole.objects.all()
|
||||
|
@ -50,6 +50,16 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
</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>
|
||||
<th scope="row">Manufacturer</th>
|
||||
<td>
|
||||
|
Loading…
Reference in New Issue
Block a user