Arthur 1 vecka sedan
förälder
incheckning
585c6a74e5

+ 1 - 1
docs/models/dcim/coolingport.md

@@ -37,7 +37,7 @@ The connector diameter, expressed as a numeric value with a selectable unit (mil
 
 ### Maximum Flow
 
-The maximum coolant flow rate this port supports, in litres per minute (L/min).
+The maximum coolant flow rate this port supports, expressed as a numeric value with a selectable unit (litres per minute, cubic meters per hour, or gallons per minute).
 
 ### Heat Capacity
 

+ 13 - 3
netbox/dcim/filtersets.py

@@ -1015,12 +1015,17 @@ class PowerOutletTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceType
 
 @register_filterset
 class CoolingPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
+    maximum_flow_unit = django_filters.MultipleChoiceFilter(
+        choices=FlowRateUnitChoices,
+        distinct=False,
+        null_value=None
+    )
 
     class Meta:
         model = CoolingPortTemplate
         fields = (
             'id', 'name', 'label', 'type', 'connector_type', 'diameter', 'diameter_unit', 'maximum_flow',
-            'heat_capacity', 'description',
+            'maximum_flow_unit', 'heat_capacity', 'description',
         )
 
 
@@ -2084,12 +2089,17 @@ class CoolingPortFilterSet(ModularDeviceComponentFilterSet, CabledObjectFilterSe
         distinct=False,
         null_value=None
     )
+    maximum_flow_unit = django_filters.MultipleChoiceFilter(
+        choices=FlowRateUnitChoices,
+        distinct=False,
+        null_value=None
+    )
 
     class Meta:
         model = CoolingPort
         fields = (
-            'id', 'name', 'label', 'diameter', 'diameter_unit', 'maximum_flow', 'heat_capacity', 'description',
-            'mark_connected', 'cable_end', 'cable_connector',
+            'id', 'name', 'label', 'diameter', 'diameter_unit', 'maximum_flow', 'maximum_flow_unit', 'heat_capacity',
+            'description', 'mark_connected', 'cable_end', 'cable_connector',
         )
 
 

+ 6 - 3
netbox/dcim/forms/bulk_create.py

@@ -86,14 +86,17 @@ class PowerOutletBulkCreateForm(
 class CoolingPortBulkCreateForm(
     form_from_model(
         CoolingPort,
-        ['type', 'connector_type', 'diameter', 'diameter_unit', 'maximum_flow', 'heat_capacity', 'mark_connected']
+        [
+            'type', 'connector_type', 'diameter', 'diameter_unit', 'maximum_flow', 'maximum_flow_unit',
+            'heat_capacity', 'mark_connected'
+        ]
     ),
     DeviceBulkAddComponentForm
 ):
     model = CoolingPort
     field_order = (
-        'name', 'label', 'type', 'connector_type', 'diameter', 'diameter_unit', 'maximum_flow', 'heat_capacity',
-        'mark_connected', 'description', 'tags',
+        'name', 'label', 'type', 'connector_type', 'diameter', 'diameter_unit', 'maximum_flow', 'maximum_flow_unit',
+        'heat_capacity', 'mark_connected', 'description', 'tags',
     )
 
 

+ 13 - 5
netbox/dcim/forms/bulk_edit.py

@@ -1799,8 +1799,8 @@ class PowerOutletBulkEditForm(
 class CoolingPortBulkEditForm(
     ComponentBulkEditForm,
     form_from_model(CoolingPort, [
-        'label', 'type', 'connector_type', 'diameter', 'diameter_unit', 'maximum_flow', 'heat_capacity',
-        'mark_connected', 'description'
+        'label', 'type', 'connector_type', 'diameter', 'diameter_unit', 'maximum_flow', 'maximum_flow_unit',
+        'heat_capacity', 'mark_connected', 'description'
     ])
 ):
     mark_connected = forms.NullBooleanField(
@@ -1808,6 +1808,11 @@ class CoolingPortBulkEditForm(
         required=False,
         widget=BulkEditNullBooleanSelect
     )
+    maximum_flow_unit = forms.ChoiceField(
+        label=_('Maximum flow unit'),
+        choices=add_blank_choice(FlowRateUnitChoices),
+        required=False
+    )
 
     model = CoolingPort
     fieldsets = (
@@ -1816,11 +1821,14 @@ class CoolingPortBulkEditForm(
             InlineFields('diameter', 'diameter_unit', label=_('Diameter')),
             'label', 'description', 'mark_connected',
         ),
-        FieldSet('maximum_flow', 'heat_capacity', name=_('Characteristics')),
+        FieldSet(
+            InlineFields('maximum_flow', 'maximum_flow_unit', label=_('Maximum flow')),
+            'heat_capacity', name=_('Characteristics')
+        ),
     )
     nullable_fields = (
-        'module', 'label', 'type', 'connector_type', 'diameter', 'diameter_unit', 'maximum_flow', 'heat_capacity',
-        'description',
+        'module', 'label', 'type', 'connector_type', 'diameter', 'diameter_unit', 'maximum_flow', 'maximum_flow_unit',
+        'heat_capacity', 'description',
     )
 
 

+ 6 - 1
netbox/dcim/forms/bulk_import.py

@@ -1000,12 +1000,17 @@ class CoolingPortImportForm(OwnerCSVMixin, NetBoxModelImportForm):
         required=False,
         help_text=_('Diameter unit')
     )
+    maximum_flow_unit = CSVChoiceField(
+        choices=FlowRateUnitChoices,
+        required=False,
+        help_text=_('Unit for maximum flow')
+    )
 
     class Meta:
         model = CoolingPort
         fields = (
             'device', 'name', 'label', 'type', 'connector_type', 'diameter', 'diameter_unit', 'maximum_flow',
-            'heat_capacity', 'mark_connected', 'description', 'owner', 'tags',
+            'maximum_flow_unit', 'heat_capacity', 'mark_connected', 'description', 'owner', 'tags',
         )
 
 

+ 18 - 2
netbox/dcim/forms/filtersets.py

@@ -1847,7 +1847,10 @@ class CoolingPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
     model = CoolingPort
     fieldsets = (
         FieldSet('q', 'filter_id', 'tag'),
-        FieldSet('name', 'label', 'type', 'connector_type', 'diameter', 'diameter_unit', name=_('Attributes')),
+        FieldSet(
+            'name', 'label', 'type', 'connector_type', 'diameter', 'diameter_unit', 'maximum_flow_unit',
+            name=_('Attributes')
+        ),
         FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
         FieldSet(
             'tenant_id', 'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id',
@@ -1875,6 +1878,11 @@ class CoolingPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
         choices=add_blank_choice(DiameterUnitChoices),
         required=False
     )
+    maximum_flow_unit = forms.MultipleChoiceField(
+        label=_('Maximum flow unit'),
+        choices=FlowRateUnitChoices,
+        required=False
+    )
     tag = TagFilterField(model)
 
 
@@ -1882,7 +1890,10 @@ class CoolingPortTemplateFilterForm(ModularDeviceComponentTemplateFilterForm):
     model = CoolingPortTemplate
     fieldsets = (
         FieldSet('q', 'filter_id', 'tag'),
-        FieldSet('name', 'label', 'type', 'connector_type', 'diameter', 'diameter_unit', name=_('Attributes')),
+        FieldSet(
+            'name', 'label', 'type', 'connector_type', 'diameter', 'diameter_unit', 'maximum_flow_unit',
+            name=_('Attributes')
+        ),
         FieldSet('device_type_id', 'module_type_id', name=_('Device')),
     )
     type = forms.MultipleChoiceField(
@@ -1904,6 +1915,11 @@ class CoolingPortTemplateFilterForm(ModularDeviceComponentTemplateFilterForm):
         choices=add_blank_choice(DiameterUnitChoices),
         required=False
     )
+    maximum_flow_unit = forms.MultipleChoiceField(
+        label=_('Maximum flow unit'),
+        choices=FlowRateUnitChoices,
+        required=False
+    )
 
 
 class CoolingOutletFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):

+ 1 - 1
netbox/dcim/forms/object_import.py

@@ -88,7 +88,7 @@ class CoolingPortTemplateImportForm(forms.ModelForm):
         model = CoolingPortTemplate
         fields = [
             'device_type', 'module_type', 'name', 'label', 'type', 'connector_type', 'diameter', 'maximum_flow',
-            'heat_capacity', 'description',
+            'maximum_flow_unit', 'heat_capacity', 'description',
         ]
 
 

+ 6 - 0
netbox/dcim/graphql/filters.py

@@ -1050,6 +1050,9 @@ class CoolingPortFilter(ModularComponentFilterMixin, CabledObjectModelFilterMixi
     maximum_flow: Annotated['FloatLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
         strawberry_django.filter_field()
     )
+    maximum_flow_unit: BaseFilterLookup[
+        Annotated['FlowRateUnitEnum', strawberry.lazy('dcim.graphql.enums')]
+    ] | None = strawberry_django.filter_field()
     heat_capacity: Annotated['FloatLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
         strawberry_django.filter_field()
     )
@@ -1072,6 +1075,9 @@ class CoolingPortTemplateFilter(ModularComponentTemplateFilterMixin, ChangeLogge
     maximum_flow: Annotated['FloatLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
         strawberry_django.filter_field()
     )
+    maximum_flow_unit: BaseFilterLookup[
+        Annotated['FlowRateUnitEnum', strawberry.lazy('dcim.graphql.enums')]
+    ] | None = strawberry_django.filter_field()
     heat_capacity: Annotated['FloatLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
         strawberry_django.filter_field()
     )

+ 2 - 2
netbox/dcim/graphql/types.py

@@ -828,7 +828,7 @@ class CoolingOutletTemplateType(ModularComponentTemplateType):
 
 @strawberry_django.type(
     models.CoolingPort,
-    exclude=['_path', '_abs_diameter'],
+    exclude=['_path', '_abs_diameter', '_abs_maximum_flow'],
     filters=CoolingPortFilter,
     pagination=True
 )
@@ -839,7 +839,7 @@ class CoolingPortType(ModularComponentType, CabledObjectMixin, PathEndpointMixin
 
 @strawberry_django.type(
     models.CoolingPortTemplate,
-    exclude=['_abs_diameter'],
+    exclude=['_abs_diameter', '_abs_maximum_flow'],
     filters=CoolingPortTemplateFilter,
     pagination=True
 )

+ 12 - 0
netbox/dcim/migrations/0241_device_cooling_method_device_cooling_outlet_count_and_more.py

@@ -10,6 +10,7 @@ import netbox.models.deletion
 import utilities.fields
 import utilities.json
 import utilities.tracking
+from utilities.migration import InstallDenormalizationTrigger
 
 
 class Migration(migrations.Migration):
@@ -1042,4 +1043,15 @@ class Migration(migrations.Migration):
                 name="dcim_coolingfeed_unique_cooling_source_name",
             ),
         ),
+        # Install denormalized device → component triggers for the cooling device components,
+        # mirroring the triggers created for the other device components in migration 0239.
+        *[
+            InstallDenormalizationTrigger(
+                dependent_table=table,
+                source_table="dcim_device",
+                fk_column="device_id",
+                mappings={"_site_id": "site_id", "_location_id": "location_id", "_rack_id": "rack_id"},
+            )
+            for table in ("dcim_coolingport", "dcim_coolingoutlet")
+        ],
     ]

+ 0 - 32
netbox/dcim/migrations/0242_cooling_denormalization_triggers.py

@@ -1,32 +0,0 @@
-"""
-Install denormalized device → component triggers for the cooling device components (CoolingPort,
-CoolingOutlet), mirroring the triggers created for the other device components in migration 0239.
-"""
-from django.db import migrations
-
-from utilities.migration import InstallDenormalizationTrigger
-
-# Cooling device component tables carrying _site/_location/_rack denormalized from their parent Device.
-COMPONENT_TABLES = (
-    'dcim_coolingport',
-    'dcim_coolingoutlet',
-)
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('dcim', '0241_device_cooling_method_device_cooling_outlet_count_and_more'),
-    ]
-
-    operations = [
-        *[
-            InstallDenormalizationTrigger(
-                dependent_table=table,
-                source_table='dcim_device',
-                fk_column='device_id',
-                mappings={'_site_id': 'site_id', '_location_id': 'location_id', '_rack_id': 'rack_id'},
-            )
-            for table in COMPONENT_TABLES
-        ],
-    ]

+ 18 - 7
netbox/dcim/tables/cooling.py

@@ -193,7 +193,11 @@ class CoolingPortTable(ModularDeviceComponentTable, PathEndpointTable):
         order_by=('_abs_diameter', 'diameter_unit')
     )
     maximum_flow = tables.Column(
-        verbose_name=_('Maximum flow (L/min)')
+        verbose_name=_('Maximum flow'),
+        order_by=('_abs_maximum_flow',)
+    )
+    maximum_flow_unit = columns.ChoiceFieldColumn(
+        verbose_name=_('Maximum Flow Unit')
     )
     heat_capacity = tables.Column(
         verbose_name=_('Heat capacity (kW)')
@@ -206,8 +210,8 @@ class CoolingPortTable(ModularDeviceComponentTable, PathEndpointTable):
         model = models.CoolingPort
         fields = (
             'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'type', 'connector_type', 'diameter',
-            'description', 'mark_connected', 'maximum_flow', 'heat_capacity', 'cable', 'cable_color', 'link_peer',
-            'connection', 'inventory_items', 'tags', 'created', 'last_updated',
+            'description', 'mark_connected', 'maximum_flow', 'maximum_flow_unit', 'heat_capacity', 'cable',
+            'cable_color', 'link_peer', 'connection', 'inventory_items', 'tags', 'created', 'last_updated',
         )
         default_columns = (
             'pk', 'name', 'device', 'label', 'type', 'connector_type', 'diameter', 'maximum_flow', 'heat_capacity',
@@ -270,6 +274,13 @@ class CoolingPortTemplateTable(ComponentTemplateTable):
         template_code=DIAMETER,
         order_by=('_abs_diameter', 'diameter_unit')
     )
+    maximum_flow = tables.Column(
+        verbose_name=_('Maximum flow'),
+        order_by=('_abs_maximum_flow',)
+    )
+    maximum_flow_unit = columns.ChoiceFieldColumn(
+        verbose_name=_('Maximum Flow Unit')
+    )
     actions = columns.ActionsColumn(
         actions=('edit', 'delete'),
         extra_buttons=MODULAR_COMPONENT_TEMPLATE_BUTTONS
@@ -278,8 +289,8 @@ class CoolingPortTemplateTable(ComponentTemplateTable):
     class Meta(ComponentTemplateTable.Meta):
         model = models.CoolingPortTemplate
         fields = (
-            'pk', 'name', 'label', 'type', 'connector_type', 'diameter', 'maximum_flow', 'heat_capacity',
-            'description', 'actions',
+            'pk', 'name', 'label', 'type', 'connector_type', 'diameter', 'maximum_flow', 'maximum_flow_unit',
+            'heat_capacity', 'description', 'actions',
         )
         empty_text = "None"
 
@@ -329,8 +340,8 @@ class DeviceCoolingPortTable(CoolingPortTable):
         model = models.CoolingPort
         fields = (
             'pk', 'id', 'name', 'module_bay', 'module', 'label', 'type', 'connector_type', 'diameter', 'maximum_flow',
-            'heat_capacity', 'description', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'connection', 'tags',
-            'actions',
+            'maximum_flow_unit', 'heat_capacity', 'description', 'mark_connected', 'cable', 'cable_color', 'link_peer',
+            'connection', 'tags', 'actions',
         )
         default_columns = (
             'pk', 'name', 'label', 'type', 'connector_type', 'diameter', 'maximum_flow', 'heat_capacity',

+ 18 - 0
netbox/dcim/tests/test_filtersets.py

@@ -2066,6 +2066,7 @@ class CoolingPortTemplateTestCase(TestCase, DeviceComponentTemplateFilterSetTest
                 diameter=Decimal('25'),
                 diameter_unit=DiameterUnitChoices.UNIT_MILLIMETER,
                 maximum_flow=100,
+                maximum_flow_unit=FlowRateUnitChoices.UNIT_LITERS_PER_MINUTE,
                 heat_capacity=50,
                 description='foobar1'
             ),
@@ -2077,6 +2078,7 @@ class CoolingPortTemplateTestCase(TestCase, DeviceComponentTemplateFilterSetTest
                 diameter=Decimal('32'),
                 diameter_unit=DiameterUnitChoices.UNIT_MILLIMETER,
                 maximum_flow=200,
+                maximum_flow_unit=FlowRateUnitChoices.UNIT_CUBIC_METERS_PER_HOUR,
                 heat_capacity=100,
                 description='foobar2'
             ),
@@ -2088,6 +2090,7 @@ class CoolingPortTemplateTestCase(TestCase, DeviceComponentTemplateFilterSetTest
                 diameter=Decimal('40'),
                 diameter_unit=DiameterUnitChoices.UNIT_MILLIMETER,
                 maximum_flow=300,
+                maximum_flow_unit=FlowRateUnitChoices.UNIT_GALLONS_PER_MINUTE,
                 heat_capacity=150,
                 description='foobar3'
             ),
@@ -2113,6 +2116,12 @@ class CoolingPortTemplateTestCase(TestCase, DeviceComponentTemplateFilterSetTest
         params = {'maximum_flow': [100, 200]}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
 
+    def test_maximum_flow_unit(self):
+        params = {'maximum_flow_unit': [
+            FlowRateUnitChoices.UNIT_LITERS_PER_MINUTE, FlowRateUnitChoices.UNIT_CUBIC_METERS_PER_HOUR
+        ]}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
     def test_heat_capacity(self):
         params = {'heat_capacity': [50, 100]}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -4816,6 +4825,7 @@ class CoolingPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedF
                 diameter=Decimal('25'),
                 diameter_unit=DiameterUnitChoices.UNIT_MILLIMETER,
                 maximum_flow=100,
+                maximum_flow_unit=FlowRateUnitChoices.UNIT_LITERS_PER_MINUTE,
                 heat_capacity=50,
                 description='First',
                 _site=devices[0].site,
@@ -4832,6 +4842,7 @@ class CoolingPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedF
                 diameter=Decimal('32'),
                 diameter_unit=DiameterUnitChoices.UNIT_MILLIMETER,
                 maximum_flow=200,
+                maximum_flow_unit=FlowRateUnitChoices.UNIT_CUBIC_METERS_PER_HOUR,
                 heat_capacity=100,
                 description='Second',
                 _site=devices[1].site,
@@ -4848,6 +4859,7 @@ class CoolingPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedF
                 diameter=Decimal('40'),
                 diameter_unit=DiameterUnitChoices.UNIT_MILLIMETER,
                 maximum_flow=300,
+                maximum_flow_unit=FlowRateUnitChoices.UNIT_GALLONS_PER_MINUTE,
                 heat_capacity=150,
                 description='Third',
                 _site=devices[2].site,
@@ -4890,6 +4902,12 @@ class CoolingPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedF
         params = {'maximum_flow': [100, 200]}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
 
+    def test_maximum_flow_unit(self):
+        params = {'maximum_flow_unit': [
+            FlowRateUnitChoices.UNIT_LITERS_PER_MINUTE, FlowRateUnitChoices.UNIT_CUBIC_METERS_PER_HOUR
+        ]}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
     def test_heat_capacity(self):
         params = {'heat_capacity': [50, 100]}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)

+ 3 - 1
netbox/dcim/tests/test_models.py

@@ -11,7 +11,7 @@ from dcim.models import *
 from extras.events import serialize_for_event
 from extras.models import CustomField
 from ipam.models import Prefix
-from netbox.choices import DiameterUnitChoices, TemperatureUnitChoices, WeightUnitChoices
+from netbox.choices import DiameterUnitChoices, FlowRateUnitChoices, TemperatureUnitChoices, WeightUnitChoices
 from tenancy.models import Tenant
 from utilities.data import drange
 from virtualization.models import Cluster, ClusterType
@@ -2864,6 +2864,7 @@ class CoolingComponentTestCase(TestCase):
             diameter=Decimal('25'),
             diameter_unit=DiameterUnitChoices.UNIT_MILLIMETER,
             maximum_flow=100,
+            maximum_flow_unit=FlowRateUnitChoices.UNIT_LITERS_PER_MINUTE,
             heat_capacity=50
         )
         CoolingOutletTemplate.objects.create(
@@ -2890,6 +2891,7 @@ class CoolingComponentTestCase(TestCase):
             diameter=Decimal('25'),
             diameter_unit=DiameterUnitChoices.UNIT_MILLIMETER,
             maximum_flow=100,
+            maximum_flow_unit=FlowRateUnitChoices.UNIT_LITERS_PER_MINUTE,
             heat_capacity=50
         )
         self.assertEqual(cooling_port_template.maximum_flow, cooling_port.maximum_flow)

+ 6 - 0
netbox/dcim/tests/test_views.py

@@ -4263,6 +4263,7 @@ class CoolingPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestC
             'diameter': Decimal('25'),
             'diameter_unit': DiameterUnitChoices.UNIT_MILLIMETER,
             'maximum_flow': 100,
+            'maximum_flow_unit': FlowRateUnitChoices.UNIT_LITERS_PER_MINUTE,
             'heat_capacity': 50,
         }
 
@@ -4274,6 +4275,7 @@ class CoolingPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestC
             'diameter': Decimal('25'),
             'diameter_unit': DiameterUnitChoices.UNIT_MILLIMETER,
             'maximum_flow': 100,
+            'maximum_flow_unit': FlowRateUnitChoices.UNIT_LITERS_PER_MINUTE,
             'heat_capacity': 50,
         }
 
@@ -4283,6 +4285,7 @@ class CoolingPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestC
             'diameter': Decimal('25'),
             'diameter_unit': DiameterUnitChoices.UNIT_MILLIMETER,
             'maximum_flow': 100,
+            'maximum_flow_unit': FlowRateUnitChoices.UNIT_LITERS_PER_MINUTE,
             'heat_capacity': 50,
         }
 
@@ -4360,6 +4363,7 @@ class CoolingPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
             'diameter': Decimal('25'),
             'diameter_unit': DiameterUnitChoices.UNIT_MILLIMETER,
             'maximum_flow': 100,
+            'maximum_flow_unit': FlowRateUnitChoices.UNIT_LITERS_PER_MINUTE,
             'heat_capacity': 50,
             'description': 'A cooling port',
             'tags': [t.pk for t in tags],
@@ -4373,6 +4377,7 @@ class CoolingPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
             'diameter': Decimal('25'),
             'diameter_unit': DiameterUnitChoices.UNIT_MILLIMETER,
             'maximum_flow': 100,
+            'maximum_flow_unit': FlowRateUnitChoices.UNIT_LITERS_PER_MINUTE,
             'heat_capacity': 50,
             'description': 'A cooling port',
             'tags': [t.pk for t in tags],
@@ -4384,6 +4389,7 @@ class CoolingPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
             'diameter': Decimal('25'),
             'diameter_unit': DiameterUnitChoices.UNIT_MILLIMETER,
             'maximum_flow': 100,
+            'maximum_flow_unit': FlowRateUnitChoices.UNIT_LITERS_PER_MINUTE,
             'heat_capacity': 50,
             'description': 'New description',
         }

+ 1 - 1
netbox/dcim/ui/panels.py

@@ -253,7 +253,7 @@ class CoolingPortPanel(panels.ObjectAttributesPanel):
     connector_type = attrs.ChoiceAttr('connector_type')
     diameter = attrs.NumericAttr('diameter', unit_accessor='get_diameter_unit_display')
     description = attrs.TextAttr('description')
-    maximum_flow = attrs.TextAttr('maximum_flow', format_string=_('{} L/min'))
+    maximum_flow = attrs.NumericAttr('maximum_flow', unit_accessor='get_maximum_flow_unit_display')
     heat_capacity = attrs.TextAttr('heat_capacity', format_string=_('{} kW'))
 
 

+ 82 - 2
netbox/utilities/tests/test_conversions.py

@@ -1,8 +1,21 @@
 from decimal import Decimal
 
 from dcim.choices import CableLengthUnitChoices
-from netbox.choices import WeightUnitChoices
-from utilities.conversion import to_grams, to_meters
+from netbox.choices import (
+    DiameterUnitChoices,
+    FlowRateUnitChoices,
+    PressureUnitChoices,
+    TemperatureUnitChoices,
+    WeightUnitChoices,
+)
+from utilities.conversion import (
+    to_celsius,
+    to_grams,
+    to_kilopascals,
+    to_liters_per_minute,
+    to_meters,
+    to_millimeters,
+)
 from utilities.testing.base import TestCase
 
 
@@ -51,3 +64,70 @@ class ConversionsTestCase(TestCase):
             to_meters(1, CableLengthUnitChoices.UNIT_INCH),
             Decimal('0.0254')
         )
+
+    def test_to_celsius(self):
+        self.assertEqual(
+            to_celsius(20, TemperatureUnitChoices.UNIT_CELSIUS),
+            Decimal('20')
+        )
+        self.assertEqual(
+            to_celsius(68, TemperatureUnitChoices.UNIT_FAHRENHEIT),
+            Decimal('20')
+        )
+        self.assertEqual(
+            to_celsius(-4, TemperatureUnitChoices.UNIT_FAHRENHEIT),
+            Decimal('-20')
+        )
+        with self.assertRaises(ValueError):
+            to_celsius(20, 'invalid')
+
+    def test_to_millimeters(self):
+        self.assertEqual(
+            to_millimeters(1, DiameterUnitChoices.UNIT_MILLIMETER),
+            Decimal('1')
+        )
+        self.assertEqual(
+            to_millimeters(1, DiameterUnitChoices.UNIT_CENTIMETER),
+            Decimal('10')
+        )
+        self.assertEqual(
+            to_millimeters(1, DiameterUnitChoices.UNIT_INCH),
+            Decimal('25.4')
+        )
+        with self.assertRaises(ValueError):
+            to_millimeters(1, 'invalid')
+
+    def test_to_liters_per_minute(self):
+        self.assertEqual(
+            to_liters_per_minute(10, FlowRateUnitChoices.UNIT_LITERS_PER_MINUTE),
+            Decimal('10')
+        )
+        self.assertAlmostEqual(
+            to_liters_per_minute(6, FlowRateUnitChoices.UNIT_CUBIC_METERS_PER_HOUR),
+            Decimal('100'),
+            places=4
+        )
+        self.assertAlmostEqual(
+            to_liters_per_minute(10, FlowRateUnitChoices.UNIT_GALLONS_PER_MINUTE),
+            Decimal('37.8541'),
+            places=4
+        )
+        with self.assertRaises(ValueError):
+            to_liters_per_minute(10, 'invalid')
+
+    def test_to_kilopascals(self):
+        self.assertEqual(
+            to_kilopascals(1, PressureUnitChoices.UNIT_KILOPASCAL),
+            Decimal('1')
+        )
+        self.assertEqual(
+            to_kilopascals(1, PressureUnitChoices.UNIT_BAR),
+            Decimal('100')
+        )
+        self.assertAlmostEqual(
+            to_kilopascals(30, PressureUnitChoices.UNIT_PSI),
+            Decimal('206.8427'),
+            places=4
+        )
+        with self.assertRaises(ValueError):
+            to_kilopascals(30, 'invalid')