Repository: netbox-community/netbox
Pull request: Enhance Virtual Machine and Device Platform Filter with Manufacturer Information
<!--
    Thank you for your interest in contributing to NetBox! Please note that
    our contribution policy requires that a feature request or bug report be
    approved and assigned prior to opening a pull request. This helps avoid
    waste time and effort on a proposed change that we might not be able to
    accept.

    IF YOUR PULL REQUEST DOES NOT REFERENCE AN ISSUE WHICH HAS BEEN ASSIGNED
    TO YOU, IT WILL BE CLOSED AUTOMATICALLY.

    Please specify your assigned issue number on the line below.
-->
### Fixes: #12741

<!--
    Please include a summary of the proposed changes below.
-->


Problem statement:
Add manufacturer to platform of virtual machines
### NetBox version

v3.5.2

### Feature type

Data model extension

### Proposed functionality

Add "manufacturer" field before "platform" selection inside virtual machine configuration

### Use case

Currently, when you create a device you should choose it's vendor and platform, and drop down list of platform filters to that vendor.
With that approach you can create platforms as software versions with specific manufacturer.

But when you choose platform for virtual machine it is one list which becomes confusing without vendor

### Database changes

Add field "manufacturer" to Virtualization -> Virtual Machine to filter platforms

### External dependencies

_No response_

Hints from issue discussion:
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. NetBox is governed by a small group of core maintainers which means not all opened issues may receive direct feedback. **Do not** attempt to circumvent this process by "bumping" the issue; doing so will result in its immediate closure and you may be barred from participating in any future discussions. Please see our [contributing guide](https://github.com/netbox-community/netbox/blob/develop/CONTRIBUTING.md).
This can be addressed by enabling the object selector widget for the platform field. This should be done for the device model as well.
Can you please assign this to me ?

Commit message: Add manufacturer for filters in the virtual machine and device #12741

Patch to review:
diff --git a/netbox/dcim/forms/filtersets.py b/netbox/dcim/forms/filtersets.py
index 7f99d1ca499..ce600673c9b 100644
--- a/netbox/dcim/forms/filtersets.py
+++ b/netbox/dcim/forms/filtersets.py
@@ -643,7 +643,7 @@ class DeviceFilterForm(
         (None, ('q', 'filter_id', 'tag')),
         (_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
         (_('Operation'), ('status', 'role_id', 'airflow', 'serial', 'asset_tag', 'mac_address')),
-        (_('Hardware'), ('manufacturer_id', 'device_type_id', 'platform_id')),
+        (_('Hardware'), ('manufacturer_id', 'platform_id', 'device_type_id')),
         (_('Tenant'), ('tenant_group_id', 'tenant_id')),
         (_('Contacts'), ('contact', 'contact_role', 'contact_group')),
         (_('Components'), (
@@ -713,7 +713,10 @@ class DeviceFilterForm(
         queryset=Platform.objects.all(),
         required=False,
         null_option='None',
-        label=_('Platform')
+        label=_('Platform'),
+        query_params={
+            "manufacturer_id": '$manufacturer_id'
+        }
     )
     status = forms.MultipleChoiceField(
         label=_('Status'),
diff --git a/netbox/dcim/forms/model_forms.py b/netbox/dcim/forms/model_forms.py
index 93e21459873..d465f214c4c 100644
--- a/netbox/dcim/forms/model_forms.py
+++ b/netbox/dcim/forms/model_forms.py
@@ -439,10 +439,21 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
         label=_('Device role'),
         queryset=DeviceRole.objects.all()
     )
+    manufacturer = DynamicModelChoiceField(
+        label=_('Manufacturer'),
+        queryset=Manufacturer.objects.all(),
+        required=False,
+        initial_params={
+            'platforms': '$platform'
+        }
+    )
     platform = DynamicModelChoiceField(
         label=_('Platform'),
         queryset=Platform.objects.all(),
-        required=False
+        required=False,
+        query_params={
+            "manufacturer_id": '$manufacturer'
+        }
     )
     cluster = DynamicModelChoiceField(
         label=_('Cluster'),
diff --git a/netbox/templates/dcim/device_edit.html b/netbox/templates/dcim/device_edit.html
index 4dbb2f477d9..af448c2f200 100644
--- a/netbox/templates/dcim/device_edit.html
+++ b/netbox/templates/dcim/device_edit.html
@@ -64,6 +64,7 @@ <h5 class="offset-sm-3">{% trans "Location" %}</h5>
         <h5 class="offset-sm-3">{% trans "Management" %}</h5>
       </div>
       {% render_field form.status %}
+      {% render_field form.manufacturer %}
       {% render_field form.platform %}
       {% render_field form.config_template %}
       {% if object.pk %}
diff --git a/netbox/virtualization/forms/filtersets.py b/netbox/virtualization/forms/filtersets.py
index 99ac0cb774d..91d9f037581 100644
--- a/netbox/virtualization/forms/filtersets.py
+++ b/netbox/virtualization/forms/filtersets.py
@@ -1,7 +1,7 @@
 from django import forms
 from django.utils.translation import gettext_lazy as _
 
-from dcim.models import Device, DeviceRole, Platform, Region, Site, SiteGroup
+from dcim.models import Device, DeviceRole, Manufacturer, Platform, Region, Site, SiteGroup
 from extras.forms import LocalConfigContextFilterForm
 from extras.models import ConfigTemplate
 from ipam.models import L2VPN, VRF
@@ -94,7 +94,7 @@ class VirtualMachineFilterForm(
         (None, ('q', 'filter_id', 'tag')),
         (_('Cluster'), ('cluster_group_id', 'cluster_type_id', 'cluster_id', 'device_id')),
         (_('Location'), ('region_id', 'site_group_id', 'site_id')),
-        (_('Attributes'), ('status', 'role_id', 'platform_id', 'mac_address', 'has_primary_ip', 'config_template_id', 'local_context_data')),
+        (_('Attributes'), ('status', 'role_id', 'manufacturer_id', 'platform_id', 'mac_address', 'has_primary_ip', 'config_template_id', 'local_context_data')),
         (_('Tenant'), ('tenant_group_id', 'tenant_id')),
         (_('Contacts'), ('contact', 'contact_role', 'contact_group')),
     )
@@ -154,11 +154,19 @@ class VirtualMachineFilterForm(
         choices=VirtualMachineStatusChoices,
         required=False
     )
+    manufacturer_id = DynamicModelMultipleChoiceField(
+        queryset=Manufacturer.objects.all(),
+        required=False,
+        label=_('Manufacturer'),
+    )
     platform_id = DynamicModelMultipleChoiceField(
         queryset=Platform.objects.all(),
         required=False,
         null_option='None',
-        label=_('Platform')
+        label=_('Platform'),
+        query_params = {
+            "manufacturer_id": '$manufacturer_id'
+        }
     )
     mac_address = forms.CharField(
         required=False,
diff --git a/netbox/virtualization/forms/model_forms.py b/netbox/virtualization/forms/model_forms.py
index 21dbc895a76..3c03bc96a94 100644
--- a/netbox/virtualization/forms/model_forms.py
+++ b/netbox/virtualization/forms/model_forms.py
@@ -4,7 +4,7 @@
 from django.utils.translation import gettext_lazy as _
 
 from dcim.forms.common import InterfaceCommonForm
-from dcim.models import Device, DeviceRole, Platform, Rack, Region, Site, SiteGroup
+from dcim.models import Device, DeviceRole, Manufacturer, Platform, Rack, Region, Site, SiteGroup
 from extras.models import ConfigTemplate
 from ipam.models import IPAddress, VLAN, VLANGroup, VRF
 from netbox.forms import NetBoxModelForm
@@ -197,10 +197,21 @@ class VirtualMachineForm(TenancyForm, NetBoxModelForm):
             "vm_role": "True"
         }
     )
+    manufacturer = DynamicModelChoiceField(
+        label=_('Manufacturer'),
+        queryset=Manufacturer.objects.all(),
+        required=False,
+        initial_params={
+            'platforms': '$platform'
+        }
+    )
     platform = DynamicModelChoiceField(
         label=_('Platform'),
         queryset=Platform.objects.all(),
-        required=False
+        required=False,
+        query_params={
+            "manufacturer_id": '$manufacturer'
+        }
     )
     local_context_data = JSONField(
         required=False,
@@ -217,7 +228,7 @@ class VirtualMachineForm(TenancyForm, NetBoxModelForm):
         (_('Virtual Machine'), ('name', 'role', 'status', 'description', 'tags')),
         (_('Site/Cluster'), ('site', 'cluster', 'device')),
         (_('Tenancy'), ('tenant_group', 'tenant')),
-        (_('Management'), ('platform', 'primary_ip4', 'primary_ip6', 'config_template')),
+        (_('Management'), ('manufacturer', 'platform', 'primary_ip4', 'primary_ip6', 'config_template')),
         (_('Resources'), ('vcpus', 'memory', 'disk')),
         (_('Config Context'), ('local_context_data',)),
     )


Code review comments:
diff_hunk: @@ -643,7 +643,7 @@ class DeviceFilterForm(
         (None, ('q', 'filter_id', 'tag')),
         (_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
         (_('Operation'), ('status', 'role_id', 'airflow', 'serial', 'asset_tag', 'mac_address')),
-        (_('Hardware'), ('manufacturer_id', 'device_type_id', 'platform_id')),
+        (_('Hardware'), ('manufacturer_id', 'platform_id', 'device_type_id')),
path: netbox/dcim/forms/filtersets.py
text: @user1:
Why did you switch the order of these? Best to retain the current ordering unless there's a specific need to change it.

diff_hunk: @@ -154,11 +154,19 @@ class VirtualMachineFilterForm(
         choices=VirtualMachineStatusChoices,
         required=False
     )
+    manufacturer_id = DynamicModelMultipleChoiceField(
+        queryset=Manufacturer.objects.all(),
+        required=False,
+        label=_('Manufacturer'),
+    )
path: netbox/virtualization/forms/filtersets.py
text: @user1:
There is no `manufacturer_id` filter for virtual machines, so this form field would have no effect beyond constraining the choices for the platform field below, which could be very confusing to the user. The scope of FR #12741 deals only with the VM & device edit forms; we shouldn't be making any changes to the list filters as part of this work anyway.

diff_hunk: @@ -197,10 +197,21 @@ class VirtualMachineForm(TenancyForm, NetBoxModelForm):
             "vm_role": "True"
         }
     )
+    manufacturer = DynamicModelChoiceField(
+        label=_('Manufacturer'),
+        queryset=Manufacturer.objects.all(),
+        required=False,
+        initial_params={
+            'platforms': '$platform'
+        }
+    )
path: netbox/virtualization/forms/model_forms.py
text: @user1:
Per my comment [here](https://github.com/netbox-community/netbox/issues/12741#issuecomment-1751350093), this FR should be addressed by enabling the advanced selector widget on the platform field, rather than adding a separate manufacturer field. Please make this change to both the VM and device model forms.

@author:
@user1,
I'm curious about the `Advanced Selector Widget` and how it operates. Have you had the opportunity to implement it in your code? If so, I'd highly appreciate it if you could provide some references or perhaps share a few examples. Your expertise and insights would be immensely helpful in enhancing my understanding of this widget.

@user2:
You remove `initial_params={}` and instead use `selector=True`