Repository: netbox-community/netbox

Issue:
Removing Module Bays with Children Power Ports breaks Power Ports count
### NetBox version

v3.6.4

### Python version

3.8

### Steps to Reproduce

1. Create module types for device (for example PWR-MX960-4100-AC with 2 power ports):
![image](https://github.com/netbox-community/netbox/assets/122742913/a3f14cf5-ffd4-4c1f-987a-77893388b62a)
2. In Devices add to chassis device (for example mx960) PEM module:
![image](https://github.com/netbox-community/netbox/assets/122742913/83ff33db-399c-4180-a834-b38d29ca56a4)
3. Check count Power Ports:
![image](https://github.com/netbox-community/netbox/assets/122742913/6c005529-2066-41e0-b529-7e47489494e2)
4. Delete module from device:
![image](https://github.com/netbox-community/netbox/assets/122742913/58478124-b73a-4a7a-b9a2-040b2c36ab4d)


### Expected Behavior

For the Power Ports count to be correct

### Observed Behavior

The port counter has not decreased


Discussion:
Similarly removing a rearport that is associated with a frontport will not decrement the rearport count. Deleting an inventory item that has children also does not account for the children in the count, so deleting an inventoryitem named `parent_inventoryitem` that has one child inventory item `child_inventoryitem` will decrese the counter by 1 instead of 2.

I assume the counting logic is broken for any and all CASCADE deleted objects.

Resolving patch:
diff --git a/netbox/utilities/counters.py b/netbox/utilities/counters.py
--- a/netbox/utilities/counters.py
+++ b/netbox/utilities/counters.py
@@ -1,6 +1,6 @@
 from django.apps import apps
 from django.db.models import F, Count, OuterRef, Subquery
-from django.db.models.signals import post_delete, post_save
+from django.db.models.signals import post_delete, post_save, pre_delete
 
 from netbox.registry import registry
 from .fields import CounterCacheField
@@ -62,6 +62,12 @@ def post_save_receiver(sender, instance, created, **kwargs):
             update_counter(parent_model, new_pk, counter_name, 1)
 
 
+def pre_delete_receiver(sender, instance, origin, **kwargs):
+    model = instance._meta.model
+    if not model.objects.filter(pk=instance.pk).exists():
+        instance._previously_removed = True
+
+
 def post_delete_receiver(sender, instance, origin, **kwargs):
     """
     Update counter fields on related objects when a TrackingModelMixin subclass is deleted.
@@ -71,10 +77,8 @@ def post_delete_receiver(sender, instance, origin, **kwargs):
         parent_pk = getattr(instance, field_name, None)
 
         # Decrement the parent's counter by one
-        if parent_pk is not None:
-            # MPTT sends two delete signals for child elements so guard against multiple decrements
-            if not origin or origin == instance:
-                update_counter(parent_model, parent_pk, counter_name, -1)
+        if parent_pk is not None and not hasattr(instance, "_previously_removed"):
+            update_counter(parent_model, parent_pk, counter_name, -1)
 
 
 #
@@ -106,6 +110,12 @@ def connect_counters(*models):
                 weak=False,
                 dispatch_uid=f'{model._meta.label}.{field.name}'
             )
+            pre_delete.connect(
+                pre_delete_receiver,
+                sender=to_model,
+                weak=False,
+                dispatch_uid=f'{model._meta.label}.{field.name}'
+            )
             post_delete.connect(
                 post_delete_receiver,
                 sender=to_model,


Test patch: