Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions netbox_lifecycle/api/_serializers/contract.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from rest_framework import serializers

from dcim.api.serializers_.devices import DeviceSerializer
from dcim.api.serializers_.devices import DeviceSerializer, ModuleSerializer
from dcim.api.serializers_.manufacturers import ManufacturerSerializer
from netbox.api.serializers import NetBoxModelSerializer
from netbox_lifecycle.api._serializers.license import LicenseAssignmentSerializer
Expand Down Expand Up @@ -44,12 +44,13 @@ class SupportContractAssignmentSerializer(NetBoxModelSerializer):
contract = SupportContractSerializer(nested=True)
sku = SupportSKUSerializer(nested=True, required=False, allow_null=True)
device = DeviceSerializer(nested=True, required=False, allow_null=True)
module = ModuleSerializer(nested=True, required=False, allow_null=True)
license = LicenseAssignmentSerializer(nested=True, required=False, allow_null=True)

class Meta:
model = SupportContractAssignment
fields = (
'url', 'id', 'display', 'contract', 'sku', 'device', 'license', 'end', 'description', 'comments',
'url', 'id', 'display', 'contract', 'sku', 'device', 'license', 'module', 'end', 'description', 'comments',
)

brief_fields = ('url', 'id', 'display', 'contract', 'sku', 'device', 'license', )
brief_fields = ('url', 'id', 'display', 'contract', 'sku', 'device', 'license', 'module', )
7 changes: 6 additions & 1 deletion netbox_lifecycle/filtersets/contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from django.db.models import Q
from django.utils.translation import gettext as _

from dcim.models import Manufacturer, Device
from dcim.models import Manufacturer, Device, Module
from netbox.filtersets import NetBoxModelFilterSet
from netbox_lifecycle.models import Vendor, SupportContract, SupportContractAssignment, SupportSKU, LicenseAssignment, \
License
Expand Down Expand Up @@ -129,6 +129,11 @@ class SupportContractAssignmentFilterSet(NetBoxModelFilterSet):
to_field_name='name',
label=_('License (SKU)'),
)
module_id = django_filters.ModelMultipleChoiceFilter(
field_name='module',
queryset=Module.objects.all(),
label=_('Module (ID)'),
)

class Meta:
model = SupportContractAssignment
Expand Down
8 changes: 7 additions & 1 deletion netbox_lifecycle/forms/filtersets.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from django.db.models import Q
from django.forms import DateField

from dcim.models import Device, Manufacturer
from dcim.models import Device, Manufacturer, Module
from netbox.forms import NetBoxModelFilterSetForm
from netbox_lifecycle.models import HardwareLifecycle, SupportContract, Vendor, License, LicenseAssignment, \
SupportContractAssignment, SupportSKU
Expand Down Expand Up @@ -138,6 +138,12 @@ class SupportContractAssignmentFilterForm(NetBoxModelFilterSetForm):
selector=True,
label=_('Devices'),
)
module_id = DynamicModelMultipleChoiceField(
queryset=Module.objects.all(),
required=False,
selector=True,
label=_('Modules'),
)
tag = TagFilterField(model)


Expand Down
23 changes: 18 additions & 5 deletions netbox_lifecycle/forms/model_forms.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from django import forms
from django.utils.translation import gettext as _

from dcim.models import DeviceType, ModuleType, Manufacturer, Device
from dcim.models import DeviceType, ModuleType, Manufacturer, Device, Module
from netbox.forms import NetBoxModelForm
from netbox_lifecycle.models import HardwareLifecycle, Vendor, SupportContract, LicenseAssignment, License, \
SupportContractAssignment, SupportSKU
Expand Down Expand Up @@ -77,10 +77,16 @@ class SupportContractAssignmentForm(NetBoxModelForm):
selector=True,
label=_('License Assignment'),
)
module = DynamicModelChoiceField(
queryset=Module.objects.all(),
required=False,
selector=True,
label=_('Module'),
)

class Meta:
model = SupportContractAssignment
fields = ('contract', 'sku', 'device', 'license', 'end', 'description', 'comments', 'tags', )
fields = ('contract', 'sku', 'device', 'license', 'module', 'end', 'description', 'comments', 'tags', )
widgets = {
'end': DatePicker(),
}
Expand All @@ -93,15 +99,22 @@ def clean(self):

# Handle object assignment
selected_objects = [
field for field in ('device', 'license') if self.cleaned_data[field]
field for field in ('device', 'license', 'module') if self.cleaned_data[field]
]

if len(selected_objects) == 0:
raise forms.ValidationError({
'device': "You must select at least a device or license",
'license': "You must select at least a device or license"
'device': "You must select at least a device, a license, or a module",
'license': "You must select at least a device, a license, or a module",
'module': "You must select at least a device, a license, or a module"
})

if self.cleaned_data.get('module'):
if self.cleaned_data.get('license') or self.cleaned_data.get('device'):
raise forms.ValidationError({
'module': 'Selecting a Module excludes the selection of a Device or License'
})

Comment on lines +112 to +117
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See comment in the model and adjust accordingly

if self.cleaned_data.get('license') and not self.cleaned_data.get('device'):
self.cleaned_data['device'] = self.cleaned_data.get('license').device

Expand Down
1 change: 1 addition & 0 deletions netbox_lifecycle/graphql/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ class SupportContractAssignmentType(NetBoxObjectType):
sku: SupportSKUType | None
device: DeviceType | None
license: LicenseType | None
module: ModuleType | None
end: str | None


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Generated by Django 5.0.9 on 2025-03-13 16:40

import django.db.models.deletion
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('dcim', '0190_nested_modules'),
('netbox_lifecycle', '0014_rename_last_contract_date_and_more'),
]

operations = [
migrations.AlterModelOptions(
name='supportcontractassignment',
options={'ordering': ['contract', 'device', 'license', 'module']},
),
migrations.AddField(
model_name='supportcontractassignment',
name='module',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='contracts', to='dcim.module'),
),
]
35 changes: 29 additions & 6 deletions netbox_lifecycle/models/contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from django.urls import reverse
from django.utils.translation import gettext as _

from dcim.choices import DeviceStatusChoices
from dcim.choices import DeviceStatusChoices, ModuleStatusChoices
from netbox.models import PrimaryModel


Expand Down Expand Up @@ -138,6 +138,13 @@ class SupportContractAssignment(PrimaryModel):
blank=True,
related_name='contracts',
)
module = models.ForeignKey(
to='dcim.Module',
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='contracts',
)
end = models.DateField(
null=True,
blank=True,
Expand All @@ -153,16 +160,20 @@ class SupportContractAssignment(PrimaryModel):
'netbox_lifecycle.SupportSKU',
'netbox_lifecycle.License',
'dcim.Device',
'dcim.Module',
)

class Meta:
ordering = ['contract', 'device', 'license']
ordering = ['contract', 'device', 'license', 'module']
constraints = ()

def __str__(self):
if self.license and self.device:
return f'{self.device} ({self.license}): {self.contract.contract_id}'
return f'{self.device}: {self.contract.contract_id}'
if self.device:
return f'{self.device}: {self.contract.contract_id}'
if self.module:
return f'{self.module}: {self.contract.contract_id}'

def get_absolute_url(self):
return reverse('plugins:netbox_lifecycle:supportcontractassignment', args=[self.pk])
Expand All @@ -178,16 +189,28 @@ def get_device_status_color(self):
return
return DeviceStatusChoices.colors.get(self.device.status)

def get_module_status_color(self):
if self.module is None:
return
return ModuleStatusChoices.colors.get(self.module.status)

def clean(self):
if self.device and self.license and SupportContractAssignment.objects.filter(
if self.module:
if self.license or self.device:
raise ValidationError('Assigning a Module excludes the assigment of a Device or License')
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know if I agree with this. I think selecting a device or license (or both) would be valid depending on the circumstances.

A module is always going to be in a device, and when removing from a device in NetBox it is deleted.

I think a more reasonable check would be to ensure that this is unique across all 3 fields.

In addition, you would want to add unique checks for:

device, module where license is None
module, license where device is None (or just require a device when saving a module)

elif SupportContractAssignment.objects.filter(
contract=self.contract, module=self.module
).exclude(pk=self.pk).count() > 0:
raise ValidationError('Module must be unique')
elif self.device and self.license and not self.module and SupportContractAssignment.objects.filter(
contract=self.contract, device=self.device, license=self.license, sku=self.sku
).exclude(pk=self.pk).count() > 0:
raise ValidationError('Device or License must be unique')
elif self.device and not self.license and SupportContractAssignment.objects.filter(
elif self.device and not self.license and not self.module and SupportContractAssignment.objects.filter(
contract=self.contract, device=self.device, license=self.license
).exclude(pk=self.pk).count() > 0:
raise ValidationError('Device must be unique')
elif not self.device and self.license and SupportContractAssignment.objects.filter(
elif not self.device and self.license and not self.module and SupportContractAssignment.objects.filter(
contract=self.contract, device=self.device, license=self.license
).exclude(pk=self.pk).count() > 0:
raise ValidationError('License must be unique')
22 changes: 19 additions & 3 deletions netbox_lifecycle/tables/contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ class SupportContractAssignmentTable(NetBoxTable):
orderable=True,
)
device_serial = tables.Column(
verbose_name=_('Serial Number'),
verbose_name=_('Device Serial Number'),
accessor='device__serial',
orderable=True,
)
Expand All @@ -102,6 +102,22 @@ class SupportContractAssignmentTable(NetBoxTable):
linkify=False,
orderable=True,
)
module = tables.Column(
verbose_name=_('Module'),
accessor='module',
linkify=True,
orderable=True,
)
module_serial = tables.Column(
verbose_name=_('Module Serial Number'),
accessor='module__serial',
orderable=True,
)
module_status = ChoiceFieldColumn(
verbose_name=_('Module Status'),
accessor='module__status',
orderable=True,
)
quantity = tables.Column(
verbose_name=_('License Quantity'),
accessor='license__quantity',
Expand All @@ -121,8 +137,8 @@ class Meta(NetBoxTable.Meta):
model = SupportContractAssignment
fields = (
'pk', 'contract', 'sku', 'device_name', 'license_name', 'device_model', 'device_serial', 'quantity',
'renewal', 'end', 'description', 'comments',
'renewal', 'end', 'description', 'comments', 'module', 'module_serial', 'module_status'
)
default_columns = (
'pk', 'contract', 'sku', 'device_name', 'license_name', 'device_model', 'device_serial'
'pk', 'contract', 'sku', 'device_name', 'license_name', 'module', 'device_model', 'device_serial'
)
5 changes: 3 additions & 2 deletions netbox_lifecycle/template_content.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@
class DeviceHardwareInfoExtension(PluginTemplateExtension):
def right_page(self):
object = self.context.get('object')
support_contract = contract.SupportContractAssignment.objects.filter(device_id=self.context['object'].id).first()
match self.kind:
case "device":
support_contract = contract.SupportContractAssignment.objects.filter(device_id=self.context['object'].id).first()
content_type = ContentType.objects.get(app_label="dcim", model="devicetype")
lifecycle_info = hardware.HardwareLifecycle.objects.filter(assigned_object_id=self.context['object'].device_type_id,
assigned_object_type_id=content_type.id).first()
case "module":
support_contract = contract.SupportContractAssignment.objects.filter(module_id=self.context['object'].id).first()
content_type = ContentType.objects.get(app_label="dcim", model="moduletype")
lifecycle_info = hardware.HardwareLifecycle.objects.filter(assigned_object_id=self.context['object'].module_type_id,
assigned_object_type_id=content_type.id).first()
Expand Down Expand Up @@ -53,7 +54,7 @@ class DeviceHardwareLifecycleInfo(DeviceHardwareInfoExtension):
kind = 'device'


class ModuleHardwareLifecycleInfo(TypeInfoExtension):
class ModuleHardwareLifecycleInfo(DeviceHardwareInfoExtension):
model = 'dcim.module'
kind = 'module'

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

{% load filters %}
{% load helpers %}
{# renders panel on object (device) with support contract info assigned to it #}
{# renders panel on object (device, module) with support contract info assigned to it #}

<div class="card">
<h5 class="card-header">Support Contract</h5>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ <h5 class="card-header">Contract</h5>
<th>License</th>
<td>{{ object.license|linkify }}</td>
</tr>
<tr>
<th>Module</th>
<td>{{ object.module|linkify }}</td>
</tr>
<tr>
<th>End Date</th>
<td>{{ object.end }}</td>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ <h5 class="offset-sm-3">Assignment</h5>
License
</button>
</li>
<li role="presentation" class="nav-item">
<button role="tab" type="button" id="module_tab" data-bs-toggle="tab" aria-controls="module" data-bs-target="#module" class="nav-link {% if form.initial.license %}active{% endif %}">
Module
</button>
</li>
</ul>
</div>
</div>
Expand All @@ -37,6 +42,9 @@ <h5 class="offset-sm-3">Assignment</h5>
<div class="tab-pane {% if form.initial.license %}active{% endif %}" id="license" role="tabpanel" aria-labeled-by="vm_tab">
{% render_field form.license %}
</div>
<div class="tab-pane {% if form.initial.license %}active{% endif %}" id="module" role="tabpanel" aria-labeled-by="module_tab">
{% render_field form.module %}
</div>
</div>
</div>
<div class="field-group my-5">
Expand Down
Loading