#8037: Add role field to InventoryItem

This commit is contained in:
jeremystretch 2021-12-27 10:45:33 -05:00
parent 04fb5e544d
commit 6e9afccfd7
16 changed files with 141 additions and 65 deletions

View File

@ -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. 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. 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.

View File

@ -811,12 +811,13 @@ class InventoryItemSerializer(PrimaryModelSerializer):
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) manufacturer = NestedManufacturerSerializer(required=False, allow_null=True, default=None)
role = NestedInventoryItemRoleSerializer(required=False, allow_null=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', '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', 'tags', 'custom_fields', 'created', 'last_updated', '_depth',
] ]

View File

@ -1284,6 +1284,16 @@ class InventoryItemFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet):
to_field_name='slug', to_field_name='slug',
label='Manufacturer (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( serial = django_filters.CharFilter(
lookup_expr='iexact' lookup_expr='iexact'
) )

View File

@ -107,11 +107,11 @@ class DeviceBayBulkCreateForm(DeviceBulkAddComponentForm):
class InventoryItemBulkCreateForm( 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 DeviceBulkAddComponentForm
): ):
model = InventoryItem model = InventoryItem
field_order = ( field_order = (
'name_pattern', 'label_pattern', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered', 'description', 'name_pattern', 'label_pattern', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered',
'tags', 'description', 'tags',
) )

View File

@ -1172,7 +1172,7 @@ class DeviceBayBulkEditForm(
class InventoryItemBulkEditForm( class InventoryItemBulkEditForm(
form_from_model(InventoryItem, ['label', 'manufacturer', 'part_id', 'description']), form_from_model(InventoryItem, ['label', 'role', 'manufacturer', 'part_id', 'description']),
AddRemoveTagsForm, AddRemoveTagsForm,
CustomFieldModelBulkEditForm CustomFieldModelBulkEditForm
): ):
@ -1180,13 +1180,17 @@ class InventoryItemBulkEditForm(
queryset=InventoryItem.objects.all(), queryset=InventoryItem.objects.all(),
widget=forms.MultipleHiddenInput() widget=forms.MultipleHiddenInput()
) )
role = DynamicModelChoiceField(
queryset=InventoryItemRole.objects.all(),
required=False
)
manufacturer = DynamicModelChoiceField( manufacturer = DynamicModelChoiceField(
queryset=Manufacturer.objects.all(), queryset=Manufacturer.objects.all(),
required=False required=False
) )
class Meta: class Meta:
nullable_fields = ['label', 'manufacturer', 'part_id', 'description'] nullable_fields = ['label', 'role', 'manufacturer', 'part_id', 'description']
# #

View File

@ -772,6 +772,11 @@ class InventoryItemCSVForm(CustomFieldModelCSVForm):
queryset=Device.objects.all(), queryset=Device.objects.all(),
to_field_name='name' to_field_name='name'
) )
role = CSVModelChoiceField(
queryset=InventoryItemRole.objects.all(),
to_field_name='name',
required=False
)
manufacturer = CSVModelChoiceField( manufacturer = CSVModelChoiceField(
queryset=Manufacturer.objects.all(), queryset=Manufacturer.objects.all(),
to_field_name='name', to_field_name='name',
@ -787,7 +792,8 @@ class InventoryItemCSVForm(CustomFieldModelCSVForm):
class Meta: class Meta:
model = InventoryItem model = InventoryItem
fields = ( 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): def __init__(self, *args, **kwargs):

View File

@ -1100,6 +1100,12 @@ class InventoryItemFilterForm(DeviceComponentFilterForm):
['name', 'label', 'manufacturer_id', 'serial', 'asset_tag', 'discovered'], ['name', 'label', 'manufacturer_id', 'serial', 'asset_tag', 'discovered'],
['region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id'], ['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( manufacturer_id = DynamicModelMultipleChoiceField(
queryset=Manufacturer.objects.all(), queryset=Manufacturer.objects.all(),
required=False, required=False,

View File

@ -1368,6 +1368,10 @@ class InventoryItemForm(CustomFieldModelForm):
'device_id': '$device' 'device_id': '$device'
} }
) )
role = DynamicModelChoiceField(
queryset=InventoryItemRole.objects.all(),
required=False
)
manufacturer = DynamicModelChoiceField( manufacturer = DynamicModelChoiceField(
queryset=Manufacturer.objects.all(), queryset=Manufacturer.objects.all(),
required=False required=False
@ -1380,8 +1384,8 @@ class InventoryItemForm(CustomFieldModelForm):
class Meta: class Meta:
model = InventoryItem model = InventoryItem
fields = [ fields = [
'device', 'parent', 'name', 'label', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description', 'device', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag',
'tags', 'description', 'tags',
] ]

View File

@ -652,10 +652,6 @@ class DeviceBayCreateForm(ComponentCreateForm):
class InventoryItemCreateForm(ComponentCreateForm): class InventoryItemCreateForm(ComponentCreateForm):
model = InventoryItem model = InventoryItem
manufacturer = DynamicModelChoiceField(
queryset=Manufacturer.objects.all(),
required=False
)
parent = DynamicModelChoiceField( parent = DynamicModelChoiceField(
queryset=InventoryItem.objects.all(), queryset=InventoryItem.objects.all(),
required=False, required=False,
@ -663,6 +659,14 @@ class InventoryItemCreateForm(ComponentCreateForm):
'device_id': '$device' 'device_id': '$device'
} }
) )
role = DynamicModelChoiceField(
queryset=InventoryItemRole.objects.all(),
required=False
)
manufacturer = DynamicModelChoiceField(
queryset=Manufacturer.objects.all(),
required=False
)
part_id = forms.CharField( part_id = forms.CharField(
max_length=50, max_length=50,
required=False, required=False,
@ -677,6 +681,6 @@ class InventoryItemCreateForm(ComponentCreateForm):
required=False, required=False,
) )
field_order = ( 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', 'description', 'tags',
) )

View File

@ -994,6 +994,13 @@ class InventoryItem(MPTTModel, ComponentModel):
null=True, null=True,
db_index=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( manufacturer = models.ForeignKey(
to='dcim.Manufacturer', to='dcim.Manufacturer',
on_delete=models.PROTECT, on_delete=models.PROTECT,
@ -1007,13 +1014,6 @@ class InventoryItem(MPTTModel, ComponentModel):
blank=True, blank=True,
help_text='Manufacturer-assigned part identifier' 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( serial = models.CharField(
max_length=50, max_length=50,
verbose_name='Serial number', verbose_name='Serial number',
@ -1034,7 +1034,7 @@ class InventoryItem(MPTTModel, ComponentModel):
objects = TreeManager() objects = TreeManager()
clone_fields = ['device', 'parent', 'manufacturer', 'part_id', 'role'] clone_fields = ['device', 'parent', 'role', 'manufacturer', 'part_id']
class Meta: class Meta:
ordering = ('device__id', 'parent__id', '_name') ordering = ('device__id', 'parent__id', '_name')

View File

@ -774,6 +774,9 @@ class InventoryItemTable(DeviceComponentTable):
'args': [Accessor('device_id')], 'args': [Accessor('device_id')],
} }
) )
role = tables.Column(
linkify=True
)
manufacturer = tables.Column( manufacturer = tables.Column(
linkify=True linkify=True
) )
@ -786,10 +789,33 @@ class InventoryItemTable(DeviceComponentTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = InventoryItem model = InventoryItem
fields = ( fields = (
'pk', 'id', 'name', 'device', 'label', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description', 'pk', 'id', 'name', 'device', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag',
'discovered', 'tags', 'description', 'discovered', 'tags',
)
default_columns = ('pk', 'name', 'device', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag')
class DeviceInventoryItemTable(InventoryItemTable):
name = tables.TemplateColumn(
template_code='<a href="{{ record.get_absolute_url }}" style="padding-left: {{ record.level }}0px">'
'{{ value }}</a>',
order_by=Accessor('_name'),
attrs={'td': {'class': 'text-nowrap'}}
)
actions = ButtonsColumn(
model=InventoryItem,
buttons=('edit', 'delete')
)
class Meta(BaseTable.Meta):
model = InventoryItem
fields = (
'pk', 'id', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description',
'discovered', 'tags', 'actions',
)
default_columns = (
'pk', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description', 'actions',
) )
default_columns = ('pk', 'name', 'device', 'label', 'manufacturer', 'part_id', 'serial', 'asset_tag')
class InventoryItemRoleTable(BaseTable): class InventoryItemRoleTable(BaseTable):
@ -816,30 +842,6 @@ class InventoryItemRoleTable(BaseTable):
default_columns = ('pk', 'name', 'inventoryitem_count', 'color', 'description', '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">'
'{{ value }}</a>',
order_by=Accessor('_name'),
attrs={'td': {'class': 'text-nowrap'}}
)
actions = ButtonsColumn(
model=InventoryItem,
buttons=('edit', 'delete')
)
class Meta(BaseTable.Meta):
model = InventoryItem
fields = (
'pk', 'id', 'name', 'label', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description', 'discovered',
'tags', 'actions',
)
default_columns = (
'pk', 'name', 'label', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description', 'discovered',
'actions',
)
# #
# Virtual chassis # Virtual chassis
# #

View File

@ -1626,24 +1626,33 @@ class InventoryItemTest(APIViewTestCases.APIViewTestCase):
devicerole = DeviceRole.objects.create(name='Test Device Role 1', slug='test-device-role-1', color='ff0000') 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) 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) roles = (
InventoryItem.objects.create(device=device, name='Inventory Item 2', manufacturer=manufacturer) InventoryItemRole(name='Inventory Item Role 1', slug='inventory-item-role-1'),
InventoryItem.objects.create(device=device, name='Inventory Item 3', manufacturer=manufacturer) 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 = [ cls.create_data = [
{ {
'device': device.pk, 'device': device.pk,
'name': 'Inventory Item 4', 'name': 'Inventory Item 4',
'role': roles[1].pk,
'manufacturer': manufacturer.pk, 'manufacturer': manufacturer.pk,
}, },
{ {
'device': device.pk, 'device': device.pk,
'name': 'Inventory Item 5', 'name': 'Inventory Item 5',
'role': roles[1].pk,
'manufacturer': manufacturer.pk, 'manufacturer': manufacturer.pk,
}, },
{ {
'device': device.pk, 'device': device.pk,
'name': 'Inventory Item 6', 'name': 'Inventory Item 6',
'role': roles[1].pk,
'manufacturer': manufacturer.pk, 'manufacturer': manufacturer.pk,
}, },
] ]

View File

@ -2949,7 +2949,6 @@ class InventoryItemTestCase(TestCase, ChangeLoggedFilterSetTests):
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
manufacturers = ( manufacturers = (
Manufacturer(name='Manufacturer 1', slug='manufacturer-1'), Manufacturer(name='Manufacturer 1', slug='manufacturer-1'),
Manufacturer(name='Manufacturer 2', slug='manufacturer-2'), Manufacturer(name='Manufacturer 2', slug='manufacturer-2'),
@ -2998,10 +2997,17 @@ class InventoryItemTestCase(TestCase, ChangeLoggedFilterSetTests):
) )
Device.objects.bulk_create(devices) 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 = ( 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[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], 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'),
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[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: for i in inventory_items:
i.save() i.save()
@ -3077,6 +3083,13 @@ class InventoryItemTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'parent_id': [parent_items[0].pk, parent_items[1].pk]} params = {'parent_id': [parent_items[0].pk, parent_items[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) 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): def test_manufacturer(self):
manufacturers = Manufacturer.objects.all()[:2] manufacturers = Manufacturer.objects.all()[:2]
params = {'manufacturer_id': [manufacturers[0].pk, manufacturers[1].pk]} params = {'manufacturer_id': [manufacturers[0].pk, manufacturers[1].pk]}

View File

@ -2331,14 +2331,21 @@ class InventoryItemTestCase(ViewTestCases.DeviceComponentViewTestCase):
device = create_test_device('Device 1') device = create_test_device('Device 1')
manufacturer, _ = Manufacturer.objects.get_or_create(name='Manufacturer 1', slug='manufacturer-1') manufacturer, _ = Manufacturer.objects.get_or_create(name='Manufacturer 1', slug='manufacturer-1')
InventoryItem.objects.create(device=device, name='Inventory Item 1') roles = (
InventoryItem.objects.create(device=device, name='Inventory Item 2') InventoryItemRole(name='Inventory Item Role 1', slug='inventory-item-role-1'),
InventoryItem.objects.create(device=device, name='Inventory Item 3') 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') tags = create_tags('Alpha', 'Bravo', 'Charlie')
cls.form_data = { cls.form_data = {
'device': device.pk, 'device': device.pk,
'role': roles[1].pk,
'manufacturer': manufacturer.pk, 'manufacturer': manufacturer.pk,
'name': 'Inventory Item X', 'name': 'Inventory Item X',
'parent': None, 'parent': None,
@ -2353,6 +2360,7 @@ class InventoryItemTestCase(ViewTestCases.DeviceComponentViewTestCase):
cls.bulk_create_data = { cls.bulk_create_data = {
'device': device.pk, 'device': device.pk,
'name_pattern': 'Inventory Item [4-6]', 'name_pattern': 'Inventory Item [4-6]',
'role': roles[1].pk,
'manufacturer': manufacturer.pk, 'manufacturer': manufacturer.pk,
'parent': None, 'parent': None,
'discovered': False, 'discovered': False,
@ -2363,6 +2371,7 @@ class InventoryItemTestCase(ViewTestCases.DeviceComponentViewTestCase):
} }
cls.bulk_edit_data = { cls.bulk_edit_data = {
'role': roles[1].pk,
'part_id': '123456', 'part_id': '123456',
'description': 'New description', 'description': 'New description',
} }

View File

@ -2412,7 +2412,7 @@ class InventoryItemBulkImportView(generic.BulkImportView):
class InventoryItemBulkEditView(generic.BulkEditView): class InventoryItemBulkEditView(generic.BulkEditView):
queryset = InventoryItem.objects.prefetch_related('device', 'manufacturer') queryset = InventoryItem.objects.prefetch_related('device', 'manufacturer', 'role')
filterset = filtersets.InventoryItemFilterSet filterset = filtersets.InventoryItemFilterSet
table = tables.InventoryItemTable table = tables.InventoryItemTable
form = forms.InventoryItemBulkEditForm form = forms.InventoryItemBulkEditForm
@ -2423,7 +2423,7 @@ class InventoryItemBulkRenameView(generic.BulkRenameView):
class InventoryItemBulkDeleteView(generic.BulkDeleteView): class InventoryItemBulkDeleteView(generic.BulkDeleteView):
queryset = InventoryItem.objects.prefetch_related('device', 'manufacturer') queryset = InventoryItem.objects.prefetch_related('device', 'manufacturer', 'role')
table = tables.InventoryItemTable table = tables.InventoryItemTable
template_name = 'dcim/inventoryitem_bulk_delete.html' template_name = 'dcim/inventoryitem_bulk_delete.html'

View File

@ -13,9 +13,7 @@
<div class="row mb-3"> <div class="row mb-3">
<div class="col col-md-6"> <div class="col col-md-6">
<div class="card"> <div class="card">
<h5 class="card-header"> <h5 class="card-header">Inventory Item</h5>
Inventory Item
</h5>
<div class="card-body"> <div class="card-body">
<table class="table table-hover attr-table"> <table class="table table-hover attr-table">
<tr> <tr>
@ -42,6 +40,16 @@
<th scope="row">Label</th> <th scope="row">Label</th>
<td>{{ object.label|placeholder }}</td> <td>{{ object.label|placeholder }}</td>
</tr> </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">&mdash;</span>
{% endif %}
</td>
</tr>
<tr> <tr>
<th scope="row">Manufacturer</th> <th scope="row">Manufacturer</th>
<td> <td>