mirror of
https://github.com/netbox-community/netbox.git
synced 2024-05-10 07:54:54 +00:00
Closes #144: Implemented list and bulk edit/delete views for InventoryItems
This commit is contained in:
@ -613,6 +613,10 @@ class DeviceBayFilter(DeviceComponentFilterSet):
|
|||||||
|
|
||||||
|
|
||||||
class InventoryItemFilter(DeviceComponentFilterSet):
|
class InventoryItemFilter(DeviceComponentFilterSet):
|
||||||
|
q = django_filters.CharFilter(
|
||||||
|
method='search',
|
||||||
|
label='Search',
|
||||||
|
)
|
||||||
parent_id = django_filters.ModelMultipleChoiceFilter(
|
parent_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=InventoryItem.objects.all(),
|
queryset=InventoryItem.objects.all(),
|
||||||
label='Parent inventory item (ID)',
|
label='Parent inventory item (ID)',
|
||||||
@ -631,7 +635,19 @@ class InventoryItemFilter(DeviceComponentFilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = InventoryItem
|
model = InventoryItem
|
||||||
fields = ['name', 'part_id', 'serial', 'discovered']
|
fields = ['name', 'part_id', 'serial', 'asset_tag', 'discovered']
|
||||||
|
|
||||||
|
def search(self, queryset, name, value):
|
||||||
|
if not value.strip():
|
||||||
|
return queryset
|
||||||
|
qs_filter = (
|
||||||
|
Q(name__icontains=value) |
|
||||||
|
Q(part_id__icontains=value) |
|
||||||
|
Q(serial__iexact=value) |
|
||||||
|
Q(asset_tag__iexact=value) |
|
||||||
|
Q(description__icontains=value)
|
||||||
|
)
|
||||||
|
return queryset.filter(qs_filter)
|
||||||
|
|
||||||
|
|
||||||
class ConsoleConnectionFilter(django_filters.FilterSet):
|
class ConsoleConnectionFilter(django_filters.FilterSet):
|
||||||
|
@ -1928,3 +1928,47 @@ class InventoryItemForm(BootstrapMixin, forms.ModelForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = InventoryItem
|
model = InventoryItem
|
||||||
fields = ['name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description']
|
fields = ['name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description']
|
||||||
|
|
||||||
|
|
||||||
|
class InventoryItemCSVForm(forms.ModelForm):
|
||||||
|
device = FlexibleModelChoiceField(
|
||||||
|
queryset=Device.objects.all(),
|
||||||
|
to_field_name='name',
|
||||||
|
help_text='Device name or ID',
|
||||||
|
error_messages={
|
||||||
|
'invalid_choice': 'Device not found.',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
manufacturer = forms.ModelChoiceField(
|
||||||
|
queryset=Manufacturer.objects.all(),
|
||||||
|
to_field_name='name',
|
||||||
|
required=False,
|
||||||
|
help_text='Manufacturer name',
|
||||||
|
error_messages={
|
||||||
|
'invalid_choice': 'Invalid manufacturer.',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = InventoryItem
|
||||||
|
fields = ['device', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description']
|
||||||
|
|
||||||
|
|
||||||
|
class InventoryItemBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||||
|
pk = forms.ModelMultipleChoiceField(queryset=InventoryItem.objects.all(), widget=forms.MultipleHiddenInput)
|
||||||
|
manufacturer = forms.ModelChoiceField(queryset=Manufacturer.objects.all(), required=False)
|
||||||
|
part_id = forms.CharField(max_length=50, required=False, label='Part ID')
|
||||||
|
description = forms.CharField(max_length=100, required=False)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
nullable_fields = ['manufacturer', 'part_id', 'description']
|
||||||
|
|
||||||
|
|
||||||
|
class InventoryItemFilterForm(BootstrapMixin, forms.Form):
|
||||||
|
model = InventoryItem
|
||||||
|
q = forms.CharField(required=False, label='Search')
|
||||||
|
manufacturer = FilterChoiceField(
|
||||||
|
queryset=Manufacturer.objects.annotate(filter_count=Count('inventory_items')),
|
||||||
|
to_field_name='slug',
|
||||||
|
null_label='-- None --'
|
||||||
|
)
|
||||||
|
@ -1452,9 +1452,24 @@ class InventoryItem(models.Model):
|
|||||||
discovered = models.BooleanField(default=False, verbose_name='Discovered')
|
discovered = models.BooleanField(default=False, verbose_name='Discovered')
|
||||||
description = models.CharField(max_length=100, blank=True)
|
description = models.CharField(max_length=100, blank=True)
|
||||||
|
|
||||||
|
csv_headers = [
|
||||||
|
'device', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description',
|
||||||
|
]
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['device__id', 'parent__id', 'name']
|
ordering = ['device__id', 'parent__id', 'name']
|
||||||
unique_together = ['device', 'parent', 'name']
|
unique_together = ['device', 'parent', 'name']
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
def to_csv(self):
|
||||||
|
return csv_format([
|
||||||
|
self.device.name or '{' + self.device.pk + '}',
|
||||||
|
self.name,
|
||||||
|
self.manufacturer.name if self.manufacturer else None,
|
||||||
|
self.part_id,
|
||||||
|
self.serial,
|
||||||
|
self.asset_tag,
|
||||||
|
self.description
|
||||||
|
])
|
||||||
|
@ -7,8 +7,8 @@ from tenancy.tables import COL_TENANT
|
|||||||
from utilities.tables import BaseTable, ToggleColumn
|
from utilities.tables import BaseTable, ToggleColumn
|
||||||
from .models import (
|
from .models import (
|
||||||
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||||
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceTemplate, Manufacturer, Platform, PowerOutlet,
|
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceTemplate, InventoryItem, Manufacturer, Platform,
|
||||||
PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, Region, Site,
|
PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, Region, Site,
|
||||||
)
|
)
|
||||||
|
|
||||||
REGION_LINK = """
|
REGION_LINK = """
|
||||||
@ -528,3 +528,17 @@ class InterfaceConnectionTable(BaseTable):
|
|||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = Interface
|
model = Interface
|
||||||
fields = ('device_a', 'interface_a', 'device_b', 'interface_b')
|
fields = ('device_a', 'interface_a', 'device_b', 'interface_b')
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# InventoryItems
|
||||||
|
#
|
||||||
|
|
||||||
|
class InventoryItemTable(BaseTable):
|
||||||
|
pk = ToggleColumn()
|
||||||
|
device = tables.LinkColumn('dcim:device_inventory', args=[Accessor('device.pk')])
|
||||||
|
manufacturer = tables.Column(accessor=Accessor('manufacturer.name'), verbose_name='Manufacturer')
|
||||||
|
|
||||||
|
class Meta(BaseTable.Meta):
|
||||||
|
model = InventoryItem
|
||||||
|
fields = ('pk', 'device', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description')
|
||||||
|
@ -195,9 +195,13 @@ urlpatterns = [
|
|||||||
url(r'^device-bays/(?P<pk>\d+)/depopulate/$', views.devicebay_depopulate, name='devicebay_depopulate'),
|
url(r'^device-bays/(?P<pk>\d+)/depopulate/$', views.devicebay_depopulate, name='devicebay_depopulate'),
|
||||||
|
|
||||||
# Inventory items
|
# Inventory items
|
||||||
url(r'^devices/(?P<device>\d+)/inventory-items/add/$', views.InventoryItemEditView.as_view(), name='inventoryitem_add'),
|
url(r'^inventory-items/$', views.InventoryItemListView.as_view(), name='inventoryitem_list'),
|
||||||
|
url(r'^inventory-items/import/$', views.InventoryItemBulkImportView.as_view(), name='inventoryitem_import'),
|
||||||
|
url(r'^inventory-items/edit/$', views.InventoryItemBulkEditView.as_view(), name='inventoryitem_bulk_edit'),
|
||||||
|
url(r'^inventory-items/delete/$', views.InventoryItemBulkDeleteView.as_view(), name='inventoryitem_bulk_delete'),
|
||||||
url(r'^inventory-items/(?P<pk>\d+)/edit/$', views.InventoryItemEditView.as_view(), name='inventoryitem_edit'),
|
url(r'^inventory-items/(?P<pk>\d+)/edit/$', views.InventoryItemEditView.as_view(), name='inventoryitem_edit'),
|
||||||
url(r'^inventory-items/(?P<pk>\d+)/delete/$', views.InventoryItemDeleteView.as_view(), name='inventoryitem_delete'),
|
url(r'^inventory-items/(?P<pk>\d+)/delete/$', views.InventoryItemDeleteView.as_view(), name='inventoryitem_delete'),
|
||||||
|
url(r'^devices/(?P<device>\d+)/inventory-items/add/$', views.InventoryItemEditView.as_view(), name='inventoryitem_add'),
|
||||||
|
|
||||||
# Console/power/interface connections
|
# Console/power/interface connections
|
||||||
url(r'^console-connections/$', views.ConsoleConnectionsListView.as_view(), name='console_connections_list'),
|
url(r'^console-connections/$', views.ConsoleConnectionsListView.as_view(), name='console_connections_list'),
|
||||||
|
@ -23,7 +23,7 @@ from utilities.forms import ConfirmationForm
|
|||||||
from utilities.paginator import EnhancedPaginator
|
from utilities.paginator import EnhancedPaginator
|
||||||
from utilities.views import (
|
from utilities.views import (
|
||||||
BulkComponentCreateView, BulkDeleteView, BulkEditView, BulkImportView, ComponentCreateView, ComponentDeleteView,
|
BulkComponentCreateView, BulkDeleteView, BulkEditView, BulkImportView, ComponentCreateView, ComponentDeleteView,
|
||||||
ComponentEditView, GetReturnURLMixin, ObjectDeleteView, ObjectEditView, ObjectListView,
|
ComponentEditView, ObjectDeleteView, ObjectEditView, ObjectListView,
|
||||||
)
|
)
|
||||||
from virtualization.models import VirtualMachine
|
from virtualization.models import VirtualMachine
|
||||||
from . import filters, forms, tables
|
from . import filters, forms, tables
|
||||||
@ -1815,6 +1815,14 @@ class InterfaceConnectionsListView(ObjectListView):
|
|||||||
# Inventory items
|
# Inventory items
|
||||||
#
|
#
|
||||||
|
|
||||||
|
class InventoryItemListView(ObjectListView):
|
||||||
|
queryset = InventoryItem.objects.select_related('device', 'manufacturer')
|
||||||
|
filter = filters.InventoryItemFilter
|
||||||
|
filter_form = forms.InventoryItemFilterForm
|
||||||
|
table = tables.InventoryItemTable
|
||||||
|
template_name = 'dcim/inventoryitem_list.html'
|
||||||
|
|
||||||
|
|
||||||
class InventoryItemEditView(PermissionRequiredMixin, ComponentEditView):
|
class InventoryItemEditView(PermissionRequiredMixin, ComponentEditView):
|
||||||
permission_required = 'dcim.change_inventoryitem'
|
permission_required = 'dcim.change_inventoryitem'
|
||||||
model = InventoryItem
|
model = InventoryItem
|
||||||
@ -1837,3 +1845,28 @@ class InventoryItemDeleteView(PermissionRequiredMixin, ComponentDeleteView):
|
|||||||
|
|
||||||
def get_return_url(self, request, obj):
|
def get_return_url(self, request, obj):
|
||||||
return reverse('dcim:device_inventory', kwargs={'pk': obj.device.pk})
|
return reverse('dcim:device_inventory', kwargs={'pk': obj.device.pk})
|
||||||
|
|
||||||
|
|
||||||
|
class InventoryItemBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||||
|
permission_required = 'dcim.add_inventoryitem'
|
||||||
|
model_form = forms.InventoryItemCSVForm
|
||||||
|
table = tables.InventoryItemTable
|
||||||
|
default_return_url = 'dcim:inventoryitem_list'
|
||||||
|
|
||||||
|
|
||||||
|
class InventoryItemBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||||
|
permission_required = 'dcim.change_inventoryitem'
|
||||||
|
cls = InventoryItem
|
||||||
|
queryset = InventoryItem.objects.select_related('device', 'manufacturer')
|
||||||
|
filter = filters.InventoryItemFilter
|
||||||
|
table = tables.InventoryItemTable
|
||||||
|
form = forms.InventoryItemBulkEditForm
|
||||||
|
default_return_url = 'dcim:inventoryitem_list'
|
||||||
|
|
||||||
|
|
||||||
|
class InventoryItemBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
|
permission_required = 'dcim.delete_inventoryitem'
|
||||||
|
cls = InventoryItem
|
||||||
|
queryset = InventoryItem.objects.select_related('device', 'manufacturer')
|
||||||
|
table = tables.InventoryItemTable
|
||||||
|
default_return_url = 'dcim:inventoryitem_list'
|
||||||
|
23
netbox/templates/dcim/inventoryitem_list.html
Normal file
23
netbox/templates/dcim/inventoryitem_list.html
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
{% extends '_base.html' %}
|
||||||
|
{% load helpers %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="pull-right">
|
||||||
|
{% if perms.dcim.add_devicetype %}
|
||||||
|
<a href="{% url 'dcim:inventoryitem_import' %}" class="btn btn-info">
|
||||||
|
<span class="fa fa-download" aria-hidden="true"></span>
|
||||||
|
Import inventory items
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% include 'inc/export_button.html' with obj_type='inventory items' %}
|
||||||
|
</div>
|
||||||
|
<h1>{% block title %}Inventory Items{% endblock %}</h1>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-9">
|
||||||
|
{% include 'utilities/obj_table.html' with bulk_edit_url='dcim:inventoryitem_bulk_edit' bulk_delete_url='dcim:inventoryitem_bulk_delete' %}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
{% include 'inc/search_panel.html' %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
@ -104,7 +104,7 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li class="dropdown{% if request.path|contains:'/dcim/device,/dcim/manufacturers/,/dcim/platforms/,-connections/' %} active{% endif %}">
|
<li class="dropdown{% if request.path|contains:'/dcim/device,/dcim/manufacturers/,/dcim/platforms/,-connections/,/dcim/inventory-items/' %} active{% endif %}">
|
||||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Devices <span class="caret"></span></a>
|
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Devices <span class="caret"></span></a>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
<li class="dropdown-header">Devices</li>
|
<li class="dropdown-header">Devices</li>
|
||||||
@ -156,6 +156,16 @@
|
|||||||
<a href="{% url 'dcim:manufacturer_list' %}">Manufacturers</a>
|
<a href="{% url 'dcim:manufacturer_list' %}">Manufacturers</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="divider"></li>
|
<li class="divider"></li>
|
||||||
|
<li class="dropdown-header">Inventory</li>
|
||||||
|
<li>
|
||||||
|
{% if perms.dcim.add_inventoryitem %}
|
||||||
|
<div class="buttons pull-right">
|
||||||
|
<a href="{% url 'dcim:inventoryitem_import' %}" class="btn btn-xs btn-info" title="Import"><i class="fa fa-download"></i></a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<a href="{% url 'dcim:inventoryitem_list' %}">Inventory Items</a>
|
||||||
|
</li>
|
||||||
|
<li class="divider"></li>
|
||||||
<li class="dropdown-header">Connections</li>
|
<li class="dropdown-header">Connections</li>
|
||||||
<li>
|
<li>
|
||||||
{% if perms.dcim.change_consoleport %}
|
{% if perms.dcim.change_consoleport %}
|
||||||
|
Reference in New Issue
Block a user