Arthur 1 week geleden
bovenliggende
commit
4fcc6a24b8

+ 9 - 2
netbox/dcim/api/serializers_/device_components.py

@@ -26,7 +26,7 @@ from ipam.models import VLAN
 from netbox.api.fields import ChoiceField, ContentTypeField, RestrictedPrimaryKeyRelatedField, SerializedPKRelatedField
 from netbox.api.fields import ChoiceField, ContentTypeField, RestrictedPrimaryKeyRelatedField, SerializedPKRelatedField
 from netbox.api.gfk_fields import GFKSerializerField
 from netbox.api.gfk_fields import GFKSerializerField
 from netbox.api.serializers import NetBoxModelSerializer
 from netbox.api.serializers import NetBoxModelSerializer
-from netbox.choices import DiameterUnitChoices
+from netbox.choices import DiameterUnitChoices, FlowRateUnitChoices
 from users.api.serializers_.mixins import OwnerMixin
 from users.api.serializers_.mixins import OwnerMixin
 from vpn.api.serializers_.l2vpn import L2VPNTerminationSerializer
 from vpn.api.serializers_.l2vpn import L2VPNTerminationSerializer
 from wireless.api.serializers_.nested import NestedWirelessLinkSerializer
 from wireless.api.serializers_.nested import NestedWirelessLinkSerializer
@@ -232,12 +232,19 @@ class CoolingPortSerializer(
         required=False,
         required=False,
         allow_null=True
         allow_null=True
     )
     )
+    maximum_flow_unit = ChoiceField(
+        choices=FlowRateUnitChoices,
+        allow_blank=True,
+        required=False,
+        allow_null=True
+    )
 
 
     class Meta:
     class Meta:
         model = CoolingPort
         model = CoolingPort
         fields = [
         fields = [
             'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'label', 'type', 'connector_type',
             'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'label', 'type', 'connector_type',
-            'diameter', 'diameter_unit', 'maximum_flow', 'heat_capacity', 'description', 'mark_connected', 'cable',
+            'diameter', 'diameter_unit', 'maximum_flow', 'maximum_flow_unit', 'heat_capacity', 'description',
+            'mark_connected', 'cable',
             'cable_end',
             'cable_end',
             'link_peers', 'link_peers_type', 'connected_endpoints', 'connected_endpoints_type',
             'link_peers', 'link_peers_type', 'connected_endpoints', 'connected_endpoints_type',
             'connected_endpoints_reachable', 'owner', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
             'connected_endpoints_reachable', 'owner', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',

+ 9 - 2
netbox/dcim/api/serializers_/devicetype_components.py

@@ -21,7 +21,7 @@ from dcim.models import (
 from netbox.api.fields import ChoiceField, ContentTypeField, RestrictedPrimaryKeyRelatedField
 from netbox.api.fields import ChoiceField, ContentTypeField, RestrictedPrimaryKeyRelatedField
 from netbox.api.gfk_fields import GFKSerializerField
 from netbox.api.gfk_fields import GFKSerializerField
 from netbox.api.serializers import ChangeLogMessageSerializer, ValidatedModelSerializer
 from netbox.api.serializers import ChangeLogMessageSerializer, ValidatedModelSerializer
-from netbox.choices import DiameterUnitChoices
+from netbox.choices import DiameterUnitChoices, FlowRateUnitChoices
 from wireless.choices import *
 from wireless.choices import *
 
 
 from .base import PortSerializer
 from .base import PortSerializer
@@ -206,12 +206,19 @@ class CoolingPortTemplateSerializer(ComponentTemplateSerializer):
         required=False,
         required=False,
         allow_null=True
         allow_null=True
     )
     )
+    maximum_flow_unit = ChoiceField(
+        choices=FlowRateUnitChoices,
+        allow_blank=True,
+        required=False,
+        allow_null=True
+    )
 
 
     class Meta:
     class Meta:
         model = CoolingPortTemplate
         model = CoolingPortTemplate
         fields = [
         fields = [
             'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'connector_type',
             'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'connector_type',
-            'diameter', 'diameter_unit', 'maximum_flow', 'heat_capacity', 'description', 'created', 'last_updated',
+            'diameter', 'diameter_unit', 'maximum_flow', 'maximum_flow_unit', 'heat_capacity', 'description',
+            'created', 'last_updated',
         ]
         ]
         brief_fields = ('id', 'url', 'display', 'name', 'description')
         brief_fields = ('id', 'url', 'display', 'name', 'description')
 
 

+ 9 - 3
netbox/dcim/forms/bulk_edit.py

@@ -1396,6 +1396,11 @@ class CoolingPortTemplateBulkEditForm(ComponentTemplateBulkEditForm):
         min_value=0,
         min_value=0,
         required=False
         required=False
     )
     )
+    maximum_flow_unit = forms.ChoiceField(
+        label=_('Maximum flow unit'),
+        choices=add_blank_choice(FlowRateUnitChoices),
+        required=False
+    )
     heat_capacity = forms.DecimalField(
     heat_capacity = forms.DecimalField(
         label=_('Heat capacity'),
         label=_('Heat capacity'),
         min_value=0,
         min_value=0,
@@ -1410,12 +1415,13 @@ class CoolingPortTemplateBulkEditForm(ComponentTemplateBulkEditForm):
         FieldSet(
         FieldSet(
             'label', 'type', 'connector_type',
             'label', 'type', 'connector_type',
             InlineFields('diameter', 'diameter_unit', label=_('Diameter')),
             InlineFields('diameter', 'diameter_unit', label=_('Diameter')),
-            'maximum_flow', 'heat_capacity', 'description',
+            InlineFields('maximum_flow', 'maximum_flow_unit', label=_('Maximum flow')),
+            'heat_capacity', 'description',
         ),
         ),
     )
     )
     nullable_fields = (
     nullable_fields = (
-        'label', 'type', 'connector_type', 'diameter', 'diameter_unit', 'maximum_flow', 'heat_capacity',
-        'description',
+        'label', 'type', 'connector_type', 'diameter', 'diameter_unit', 'maximum_flow', 'maximum_flow_unit',
+        'heat_capacity', 'description',
     )
     )
 
 
 
 

+ 6 - 4
netbox/dcim/forms/model_forms.py

@@ -1260,7 +1260,8 @@ class CoolingPortTemplateForm(ModularComponentTemplateForm):
             ),
             ),
             'name', 'label', 'type', 'connector_type',
             'name', 'label', 'type', 'connector_type',
             InlineFields('diameter', 'diameter_unit', label=_('Diameter')),
             InlineFields('diameter', 'diameter_unit', label=_('Diameter')),
-            'maximum_flow', 'heat_capacity', 'description',
+            InlineFields('maximum_flow', 'maximum_flow_unit', label=_('Maximum flow')),
+            'heat_capacity', 'description',
         ),
         ),
     )
     )
 
 
@@ -1268,7 +1269,7 @@ class CoolingPortTemplateForm(ModularComponentTemplateForm):
         model = CoolingPortTemplate
         model = CoolingPortTemplate
         fields = [
         fields = [
             'device_type', 'module_type', 'name', 'label', 'type', 'connector_type', 'diameter', 'diameter_unit',
             'device_type', 'module_type', 'name', 'label', 'type', 'connector_type', 'diameter', 'diameter_unit',
-            'maximum_flow', 'heat_capacity', 'description',
+            'maximum_flow', 'maximum_flow_unit', 'heat_capacity', 'description',
         ]
         ]
 
 
 
 
@@ -1670,7 +1671,8 @@ class CoolingPortForm(ModularDeviceComponentForm):
         FieldSet(
         FieldSet(
             'device', 'module', 'name', 'label', 'type', 'connector_type',
             'device', 'module', 'name', 'label', 'type', 'connector_type',
             InlineFields('diameter', 'diameter_unit', label=_('Diameter')),
             InlineFields('diameter', 'diameter_unit', label=_('Diameter')),
-            'maximum_flow', 'heat_capacity', 'mark_connected', 'description', 'tags',
+            InlineFields('maximum_flow', 'maximum_flow_unit', label=_('Maximum flow')),
+            'heat_capacity', 'mark_connected', 'description', 'tags',
         ),
         ),
     )
     )
 
 
@@ -1678,7 +1680,7 @@ class CoolingPortForm(ModularDeviceComponentForm):
         model = CoolingPort
         model = CoolingPort
         fields = [
         fields = [
             'device', 'module', 'name', 'label', 'type', 'connector_type', 'diameter', 'diameter_unit', 'maximum_flow',
             'device', 'module', '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',
         ]
         ]
 
 
 
 

+ 69 - 49
netbox/dcim/migrations/0241_device_cooling_method_device_cooling_outlet_count_and_more.py

@@ -1,4 +1,4 @@
-# Generated by Django 6.0.6 on 2026-06-24 21:45
+# Generated by Django 6.0.6 on 2026-06-25 22:43
 
 
 import django.contrib.postgres.fields
 import django.contrib.postgres.fields
 import django.core.validators
 import django.core.validators
@@ -161,6 +161,26 @@ class Migration(migrations.Migration):
                         blank=True, decimal_places=4, max_digits=13, null=True
                         blank=True, decimal_places=4, max_digits=13, null=True
                     ),
                     ),
                 ),
                 ),
+                (
+                    "maximum_flow",
+                    models.DecimalField(
+                        blank=True,
+                        decimal_places=2,
+                        max_digits=8,
+                        null=True,
+                        validators=[django.core.validators.MinValueValidator(0)],
+                    ),
+                ),
+                (
+                    "maximum_flow_unit",
+                    models.CharField(blank=True, max_length=50, null=True),
+                ),
+                (
+                    "_abs_maximum_flow",
+                    models.DecimalField(
+                        blank=True, decimal_places=4, max_digits=13, null=True
+                    ),
+                ),
                 ("name", models.CharField(db_collation="natural_sort", max_length=64)),
                 ("name", models.CharField(db_collation="natural_sort", max_length=64)),
                 ("label", models.CharField(blank=True, max_length=64)),
                 ("label", models.CharField(blank=True, max_length=64)),
                 ("description", models.CharField(blank=True, max_length=200)),
                 ("description", models.CharField(blank=True, max_length=200)),
@@ -195,16 +215,6 @@ class Migration(migrations.Migration):
                     "connector_type",
                     "connector_type",
                     models.CharField(blank=True, max_length=50, null=True),
                     models.CharField(blank=True, max_length=50, null=True),
                 ),
                 ),
-                (
-                    "maximum_flow",
-                    models.DecimalField(
-                        blank=True,
-                        decimal_places=2,
-                        max_digits=8,
-                        null=True,
-                        validators=[django.core.validators.MinValueValidator(0)],
-                    ),
-                ),
                 (
                 (
                     "heat_capacity",
                     "heat_capacity",
                     models.DecimalField(
                     models.DecimalField(
@@ -514,14 +524,6 @@ class Migration(migrations.Migration):
                         blank=True, decimal_places=4, max_digits=13, null=True
                         blank=True, decimal_places=4, max_digits=13, null=True
                     ),
                     ),
                 ),
                 ),
-                ("name", models.CharField(db_collation="natural_sort", max_length=64)),
-                ("label", models.CharField(blank=True, max_length=64)),
-                ("description", models.CharField(blank=True, max_length=200)),
-                ("type", models.CharField(blank=True, max_length=50, null=True)),
-                (
-                    "connector_type",
-                    models.CharField(blank=True, max_length=50, null=True),
-                ),
                 (
                 (
                     "maximum_flow",
                     "maximum_flow",
                     models.DecimalField(
                     models.DecimalField(
@@ -532,6 +534,24 @@ class Migration(migrations.Migration):
                         validators=[django.core.validators.MinValueValidator(0)],
                         validators=[django.core.validators.MinValueValidator(0)],
                     ),
                     ),
                 ),
                 ),
+                (
+                    "maximum_flow_unit",
+                    models.CharField(blank=True, max_length=50, null=True),
+                ),
+                (
+                    "_abs_maximum_flow",
+                    models.DecimalField(
+                        blank=True, decimal_places=4, max_digits=13, null=True
+                    ),
+                ),
+                ("name", models.CharField(db_collation="natural_sort", max_length=64)),
+                ("label", models.CharField(blank=True, max_length=64)),
+                ("description", models.CharField(blank=True, max_length=200)),
+                ("type", models.CharField(blank=True, max_length=50, null=True)),
+                (
+                    "connector_type",
+                    models.CharField(blank=True, max_length=50, null=True),
+                ),
                 (
                 (
                     "heat_capacity",
                     "heat_capacity",
                     models.DecimalField(
                     models.DecimalField(
@@ -673,8 +693,6 @@ class Migration(migrations.Migration):
                         encoder=utilities.json.CustomFieldJSONEncoder,
                         encoder=utilities.json.CustomFieldJSONEncoder,
                     ),
                     ),
                 ),
                 ),
-                ("description", models.CharField(blank=True, max_length=200)),
-                ("comments", models.TextField(blank=True)),
                 (
                 (
                     "supply_temperature",
                     "supply_temperature",
                     models.DecimalField(
                     models.DecimalField(
@@ -703,6 +721,8 @@ class Migration(migrations.Migration):
                         blank=True, decimal_places=4, max_digits=8, null=True
                         blank=True, decimal_places=4, max_digits=8, null=True
                     ),
                     ),
                 ),
                 ),
+                ("description", models.CharField(blank=True, max_length=200)),
+                ("comments", models.TextField(blank=True)),
                 ("name", models.CharField(db_collation="natural_sort", max_length=100)),
                 ("name", models.CharField(db_collation="natural_sort", max_length=100)),
                 ("type", models.CharField(max_length=50)),
                 ("type", models.CharField(max_length=50)),
                 ("status", models.CharField(default="active", max_length=50)),
                 ("status", models.CharField(default="active", max_length=50)),
@@ -814,34 +834,6 @@ class Migration(migrations.Migration):
                         blank=True, decimal_places=4, max_digits=13, null=True
                         blank=True, decimal_places=4, max_digits=13, null=True
                     ),
                     ),
                 ),
                 ),
-                ("description", models.CharField(blank=True, max_length=200)),
-                ("comments", models.TextField(blank=True)),
-                ("cable_end", models.CharField(blank=True, max_length=1, null=True)),
-                (
-                    "cable_connector",
-                    models.PositiveSmallIntegerField(
-                        blank=True,
-                        null=True,
-                        validators=[
-                            django.core.validators.MinValueValidator(1),
-                            django.core.validators.MaxValueValidator(256),
-                        ],
-                    ),
-                ),
-                (
-                    "cable_positions",
-                    django.contrib.postgres.fields.ArrayField(
-                        base_field=models.PositiveSmallIntegerField(
-                            validators=[
-                                django.core.validators.MinValueValidator(1),
-                                django.core.validators.MaxValueValidator(1024),
-                            ]
-                        ),
-                        blank=True,
-                        null=True,
-                    ),
-                ),
-                ("mark_connected", models.BooleanField(default=False)),
                 (
                 (
                     "supply_temperature",
                     "supply_temperature",
                     models.DecimalField(
                     models.DecimalField(
@@ -870,6 +862,34 @@ class Migration(migrations.Migration):
                         blank=True, decimal_places=4, max_digits=8, null=True
                         blank=True, decimal_places=4, max_digits=8, null=True
                     ),
                     ),
                 ),
                 ),
+                ("description", models.CharField(blank=True, max_length=200)),
+                ("comments", models.TextField(blank=True)),
+                ("cable_end", models.CharField(blank=True, max_length=1, null=True)),
+                (
+                    "cable_connector",
+                    models.PositiveSmallIntegerField(
+                        blank=True,
+                        null=True,
+                        validators=[
+                            django.core.validators.MinValueValidator(1),
+                            django.core.validators.MaxValueValidator(256),
+                        ],
+                    ),
+                ),
+                (
+                    "cable_positions",
+                    django.contrib.postgres.fields.ArrayField(
+                        base_field=models.PositiveSmallIntegerField(
+                            validators=[
+                                django.core.validators.MinValueValidator(1),
+                                django.core.validators.MaxValueValidator(1024),
+                            ]
+                        ),
+                        blank=True,
+                        null=True,
+                    ),
+                ),
+                ("mark_connected", models.BooleanField(default=False)),
                 ("name", models.CharField(db_collation="natural_sort", max_length=100)),
                 ("name", models.CharField(db_collation="natural_sort", max_length=100)),
                 ("status", models.CharField(default="active", max_length=50)),
                 ("status", models.CharField(default="active", max_length=50)),
                 ("type", models.CharField(default="supply", max_length=50)),
                 ("type", models.CharField(default="supply", max_length=50)),

+ 5 - 11
netbox/dcim/models/device_component_templates.py

@@ -12,7 +12,7 @@ from dcim.models.mixins import InterfaceValidationMixin
 from dcim.utils import get_module_bay_positions, resolve_module_placeholder
 from dcim.utils import get_module_bay_positions, resolve_module_placeholder
 from netbox.models import ChangeLoggedModel
 from netbox.models import ChangeLoggedModel
 from netbox.models.ltree import LtreeManager, LtreeModel
 from netbox.models.ltree import LtreeManager, LtreeModel
-from netbox.models.mixins import DiameterMixin
+from netbox.models.mixins import DiameterMixin, MaximumFlowMixin
 from utilities.fields import ColorField, NaturalOrderingField
 from utilities.fields import ColorField, NaturalOrderingField
 from utilities.ordering import naturalize_interface
 from utilities.ordering import naturalize_interface
 from utilities.tracking import TrackingModelMixin
 from utilities.tracking import TrackingModelMixin
@@ -437,7 +437,7 @@ class PowerOutletTemplate(ModularComponentTemplateModel):
         }
         }
 
 
 
 
-class CoolingPortTemplate(DiameterMixin, ModularComponentTemplateModel):
+class CoolingPortTemplate(DiameterMixin, MaximumFlowMixin, ModularComponentTemplateModel):
     """
     """
     A template for a CoolingPort to be created for a new Device.
     A template for a CoolingPort to be created for a new Device.
     """
     """
@@ -456,15 +456,7 @@ class CoolingPortTemplate(DiameterMixin, ModularComponentTemplateModel):
         null=True
         null=True
     )
     )
     # diameter, diameter_unit, _abs_diameter provided by DiameterMixin
     # diameter, diameter_unit, _abs_diameter provided by DiameterMixin
-    maximum_flow = models.DecimalField(
-        verbose_name=_('maximum flow'),
-        max_digits=8,
-        decimal_places=2,
-        blank=True,
-        null=True,
-        validators=[MinValueValidator(0)],
-        help_text=_('Maximum coolant flow rate (L/min)')
-    )
+    # maximum_flow, maximum_flow_unit, _abs_maximum_flow provided by MaximumFlowMixin
     heat_capacity = models.DecimalField(
     heat_capacity = models.DecimalField(
         verbose_name=_('heat capacity'),
         verbose_name=_('heat capacity'),
         max_digits=8,
         max_digits=8,
@@ -490,6 +482,7 @@ class CoolingPortTemplate(DiameterMixin, ModularComponentTemplateModel):
             diameter=self.diameter,
             diameter=self.diameter,
             diameter_unit=self.diameter_unit,
             diameter_unit=self.diameter_unit,
             maximum_flow=self.maximum_flow,
             maximum_flow=self.maximum_flow,
+            maximum_flow_unit=self.maximum_flow_unit,
             heat_capacity=self.heat_capacity,
             heat_capacity=self.heat_capacity,
             **kwargs
             **kwargs
         )
         )
@@ -503,6 +496,7 @@ class CoolingPortTemplate(DiameterMixin, ModularComponentTemplateModel):
             'diameter': float(self.diameter) if self.diameter is not None else None,
             'diameter': float(self.diameter) if self.diameter is not None else None,
             'diameter_unit': self.diameter_unit,
             'diameter_unit': self.diameter_unit,
             'maximum_flow': float(self.maximum_flow) if self.maximum_flow is not None else None,
             'maximum_flow': float(self.maximum_flow) if self.maximum_flow is not None else None,
+            'maximum_flow_unit': self.maximum_flow_unit,
             'heat_capacity': float(self.heat_capacity) if self.heat_capacity is not None else None,
             'heat_capacity': float(self.heat_capacity) if self.heat_capacity is not None else None,
             'label': self.label,
             'label': self.label,
             'description': self.description,
             'description': self.description,

+ 7 - 12
netbox/dcim/models/device_components.py

@@ -16,7 +16,7 @@ from dcim.models.mixins import InterfaceValidationMixin
 from netbox.choices import ColorChoices
 from netbox.choices import ColorChoices
 from netbox.models import NetBoxModel, OrganizationalModel
 from netbox.models import NetBoxModel, OrganizationalModel
 from netbox.models.ltree import LtreeManager, LtreeModel, SortPathField
 from netbox.models.ltree import LtreeManager, LtreeModel, SortPathField
-from netbox.models.mixins import DiameterMixin, OwnerMixin
+from netbox.models.mixins import DiameterMixin, MaximumFlowMixin, OwnerMixin
 from utilities.fields import ColorField, NaturalOrderingField
 from utilities.fields import ColorField, NaturalOrderingField
 from utilities.ordering import naturalize_interface
 from utilities.ordering import naturalize_interface
 from utilities.query_functions import CollateAsChar
 from utilities.query_functions import CollateAsChar
@@ -689,7 +689,9 @@ class PowerOutlet(ModularComponentModel, CabledObjectModel, PathEndpoint, Tracki
 # Cooling components
 # Cooling components
 #
 #
 
 
-class CoolingPort(DiameterMixin, ModularComponentModel, CabledObjectModel, PathEndpoint, TrackingModelMixin):
+class CoolingPort(
+    DiameterMixin, MaximumFlowMixin, ModularComponentModel, CabledObjectModel, PathEndpoint, TrackingModelMixin
+):
     """
     """
     A coolant intake/outlet port within a Device (e.g. a server cold-plate inlet or CDU intake).
     A coolant intake/outlet port within a Device (e.g. a server cold-plate inlet or CDU intake).
     CoolingPorts connect to CoolingOutlets.
     CoolingPorts connect to CoolingOutlets.
@@ -711,15 +713,7 @@ class CoolingPort(DiameterMixin, ModularComponentModel, CabledObjectModel, PathE
         help_text=_('Physical connector type')
         help_text=_('Physical connector type')
     )
     )
     # diameter, diameter_unit, _abs_diameter provided by DiameterMixin
     # diameter, diameter_unit, _abs_diameter provided by DiameterMixin
-    maximum_flow = models.DecimalField(
-        verbose_name=_('maximum flow'),
-        max_digits=8,
-        decimal_places=2,
-        blank=True,
-        null=True,
-        validators=[MinValueValidator(0)],
-        help_text=_('Maximum coolant flow rate (L/min)')
-    )
+    # maximum_flow, maximum_flow_unit, _abs_maximum_flow provided by MaximumFlowMixin
     heat_capacity = models.DecimalField(
     heat_capacity = models.DecimalField(
         verbose_name=_('heat capacity'),
         verbose_name=_('heat capacity'),
         max_digits=8,
         max_digits=8,
@@ -731,7 +725,8 @@ class CoolingPort(DiameterMixin, ModularComponentModel, CabledObjectModel, PathE
     )
     )
 
 
     clone_fields = (
     clone_fields = (
-        'device', 'module', 'type', 'connector_type', 'diameter', 'diameter_unit', 'maximum_flow', 'heat_capacity',
+        'device', 'module', 'type', 'connector_type', 'diameter', 'diameter_unit', 'maximum_flow',
+        'maximum_flow_unit', 'heat_capacity',
     )
     )
 
 
     class Meta(ModularComponentModel.Meta):
     class Meta(ModularComponentModel.Meta):

+ 54 - 0
netbox/netbox/models/mixins.py

@@ -18,6 +18,7 @@ __all__ = (
     'DiameterMixin',
     'DiameterMixin',
     'DistanceMixin',
     'DistanceMixin',
     'FlowRateMixin',
     'FlowRateMixin',
+    'MaximumFlowMixin',
     'OwnerMixin',
     'OwnerMixin',
     'PressureMixin',
     'PressureMixin',
     'WeightMixin',
     'WeightMixin',
@@ -192,6 +193,59 @@ class FlowRateMixin(models.Model):
             raise ValidationError(_("Must specify a unit when setting a flow rate"))
             raise ValidationError(_("Must specify a unit when setting a flow rate"))
 
 
 
 
+class MaximumFlowMixin(models.Model):
+    maximum_flow = models.DecimalField(
+        verbose_name=_('maximum flow'),
+        max_digits=8,
+        decimal_places=2,
+        blank=True,
+        null=True,
+        validators=[MinValueValidator(0)],
+    )
+    maximum_flow_unit = models.CharField(
+        verbose_name=_('maximum flow unit'),
+        max_length=50,
+        choices=FlowRateUnitChoices,
+        blank=True,
+        null=True,
+    )
+    # Stores the normalized maximum flow (in liters per minute) for database ordering
+    _abs_maximum_flow = models.DecimalField(
+        max_digits=13,
+        decimal_places=4,
+        blank=True,
+        null=True
+    )
+
+    class Meta:
+        abstract = True
+
+    @property
+    def abs_maximum_flow(self):
+        # Public alias for _abs_maximum_flow; Django templates cannot access underscore-prefixed attributes.
+        return self._abs_maximum_flow
+
+    def save(self, *args, **kwargs):
+        # Store the given maximum flow (if any) in liters per minute for use in database ordering
+        if self.maximum_flow is not None and self.maximum_flow_unit:
+            self._abs_maximum_flow = to_liters_per_minute(self.maximum_flow, self.maximum_flow_unit)
+        else:
+            self._abs_maximum_flow = None
+
+        # Clear maximum_flow_unit if no maximum flow is defined
+        if self.maximum_flow is None:
+            self.maximum_flow_unit = None
+
+        super().save(*args, **kwargs)
+
+    def clean(self):
+        super().clean()
+
+        # Validate maximum_flow and maximum_flow_unit
+        if self.maximum_flow is not None and not self.maximum_flow_unit:
+            raise ValidationError(_("Must specify a unit when setting a maximum flow"))
+
+
 class PressureMixin(models.Model):
 class PressureMixin(models.Model):
     pressure = models.DecimalField(
     pressure = models.DecimalField(
         verbose_name=_('pressure'),
         verbose_name=_('pressure'),