Repository: netbox-community/netbox

Issue:
Inventory item name uniqueness not enforced with same parent device
### NetBox version

v3.6.1

### Python version

3.8

### Steps to Reproduce

1. Click `Devices`.
2. Click `Add` and create a new device called "Foo".
3. Click on `Devices` -> `Inventory Items`.
4. Click `Add`.
5. Create an inventory item named "Bar" and assign the parent device "Foo".
6. Click `Create`.
7. Repeat steps 4-6.

### Expected Behavior

The user should be stopped from creating an inventory item under the same parent device with the same name according to the documentation: https://demo.netbox.dev/static/docs/models/dcim/inventoryitem/

### Observed Behavior

Two inventory items with the name "Bar" and the parent device "Foo" exist in the database.


Discussion:
Thank you for opening a bug report. I was unable to reproduce the reported behavior on NetBox v3.6.1. Please re-confirm the reported behavior on the current stable release and adjust your post above as necessary. Remember to provide detailed steps that someone else can follow using a clean installation of NetBox to reproduce the issue. Remember to include the steps taken to create any initial objects or other data.
I'm not sure how else to improve the instructions to make it clearer. I tested this on 3.6.1 several times and can confirm the behavior. 

Here is a screenshot from https://demo.netbox.dev/
![image](https://github.com/netbox-community/netbox/assets/35903275/b598eb84-b1b2-48fb-ad46-00ec17999eb8)
I checked your work on demo and it has no parent assigned to the items. Whereas your bug report was when the same parent is assigned the code still let's you add them. Please let me know when you have a bug report which is reproducible 
Chiming in here since I've encountered the same bug. I think there's confusion here as @aaron-iles is referring to the inventory item `device` field and not the `parent` field.

The below screenshot shows that an inventory item with the same name can be added multiple times to the same `device`. Please note, `parent` is not the issue here.
![netbox_duplicate_names](https://github.com/netbox-community/netbox/assets/122414633/42239d95-fa6c-4ae2-b376-50666c8f3a14)

### Steps to Reproduce (Modified/Clarified)

1. Click `Devices`.
2. Click `Add` and create a new device called `Foo`.
3. Click on `Devices` -> `Inventory Items`.
4. Click `Add`.
5. **_Create an inventory item named `Bar` and assign it to the device `Foo`._**
6. Click `Create`.
7. Repeat steps 4-6.

This is happening because PostgreSQL considers NULL values to be unique. We need to add a secondary uniqueness constraint for items which have no parent item assigned.

Resolving patch:
diff --git a/netbox/dcim/migrations/0182_inventoryitem_uniqueness_fix.py b/netbox/dcim/migrations/0182_inventoryitem_uniqueness_fix.py
new file mode 100644
--- /dev/null
+++ b/netbox/dcim/migrations/0182_inventoryitem_uniqueness_fix.py
@@ -0,0 +1,17 @@
+# Generated by Django 4.2.5 on 2023-09-15 14:20
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('dcim', '0181_rename_device_role_device_role'),
+    ]
+
+    operations = [
+        migrations.AddConstraint(
+            model_name='inventoryitem',
+            constraint=models.UniqueConstraint(condition=models.Q(('parent__isnull', True)), fields=('device', 'name'), name='dcim_inventoryitem_unique_device_name'),
+        ),
+    ]
diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py
--- a/netbox/dcim/models/device_components.py
+++ b/netbox/dcim/models/device_components.py
@@ -1246,6 +1246,11 @@ class Meta:
                 fields=('device', 'parent', 'name'),
                 name='%(app_label)s_%(class)s_unique_device_parent_name'
             ),
+            models.UniqueConstraint(
+                fields=('device', 'name'),
+                name='%(app_label)s_%(class)s_unique_device_name',
+                condition=Q(parent__isnull=True)
+            ),
         )
         verbose_name = _('inventory item')
         verbose_name_plural = _('inventory items')


Test patch: