diff --git a/src/backend/InvenTree/common/serializers.py b/src/backend/InvenTree/common/serializers.py
index 99568eed6e..8a3d785e99 100644
--- a/src/backend/InvenTree/common/serializers.py
+++ b/src/backend/InvenTree/common/serializers.py
@@ -929,6 +929,10 @@ class NoteSerializer(FilterableSerializerMixin, InvenTreeModelSerializer):
read_only_fields = ['updated', 'updated_by']
+ def get_unique_together_validators(self):
+ """Suppress the auto-generated validator for 'unique_primary_note_per_model'."""
+ return []
+
def validate(self, data):
"""Validate note data — templates need no model_id; regular notes require both."""
data = super().validate(data)
diff --git a/src/backend/InvenTree/common/test_api.py b/src/backend/InvenTree/common/test_api.py
index fc12d89902..dff21d122c 100644
--- a/src/backend/InvenTree/common/test_api.py
+++ b/src/backend/InvenTree/common/test_api.py
@@ -1471,6 +1471,25 @@ class NoteAPITests(InvenTreeAPITestCase):
primary_pks = [n['pk'] for n in list_response.data if n['primary']]
self.assertEqual(primary_pks, [third.data['pk']])
+ def test_creating_primary_note_demotes_existing_primary(self):
+ """Explicitly creating a new note with primary=True demotes the existing primary note.
+
+ Regression test: DRF auto-generates a UniqueTogetherValidator from the
+ 'unique_primary_note_per_model' partial unique constraint, which used to
+ reject this at the serializer-validation stage (before Note.save()'s
+ demote-then-save logic ever ran), raising a spurious 'unique set' error.
+ """
+ first = self._create_note('First Note')
+ self.assertTrue(first.data['primary'])
+
+ second = self._create_note('Second Note', primary=True)
+ self.assertTrue(second.data['primary'])
+
+ from common.models import Note
+
+ self.assertFalse(Note.objects.get(pk=first.data['pk']).primary)
+ self.assertTrue(Note.objects.get(pk=second.data['pk']).primary)
+
def test_primary_flag_isolated_per_model_instance(self):
"""Primary flag changes on one model instance do not affect notes on another."""
from part.models import Part
diff --git a/src/frontend/src/components/editors/NotesEditor.tsx b/src/frontend/src/components/editors/NotesEditor.tsx
index 8c12e45ff5..a6b7d24d57 100644
--- a/src/frontend/src/components/editors/NotesEditor.tsx
+++ b/src/frontend/src/components/editors/NotesEditor.tsx
@@ -306,7 +306,7 @@ export default function NotesEditor({
return notesQuery.data && notesQuery.data.length > 0;
}, [notesQuery.data]);
- const noteFields = useNoteFields({
+ const { fields: noteFields, resetFields: resetNoteFields } = useNoteFields({
modelType: modelType!,
modelId: modelId!
});
@@ -711,7 +711,10 @@ export default function NotesEditor({
}
- onClick={createNote.open}
+ onClick={() => {
+ resetNoteFields();
+ createNote.open();
+ }}
disabled={isEditing}
>
{t`Add Note`}
diff --git a/src/frontend/src/forms/CommonForms.tsx b/src/frontend/src/forms/CommonForms.tsx
index 6baf6ca412..52c6ff2a2d 100644
--- a/src/frontend/src/forms/CommonForms.tsx
+++ b/src/frontend/src/forms/CommonForms.tsx
@@ -330,13 +330,19 @@ export function useNoteFields({
}: {
modelType: ModelType;
modelId: number;
-}): ApiFormFieldSet {
+}): { fields: ApiFormFieldSet; resetFields: () => void } {
const api = useApi();
const [title, setTitle] = useState('');
const [description, setDescription] = useState('');
const [content, setContent] = useState('');
+ const resetFields = useCallback(() => {
+ setTitle('');
+ setDescription('');
+ setContent('');
+ }, []);
+
const fetchTemplate = useCallback(
(pk: number | null) => {
if (!pk) return;
@@ -352,7 +358,7 @@ export function useNoteFields({
[api]
);
- return useMemo(() => {
+ const fields = useMemo(() => {
return {
model_type: {
hidden: true,
@@ -392,6 +398,8 @@ export function useNoteFields({
}
};
}, [modelType, modelId, title, description, content, fetchTemplate]);
+
+ return { fields, resetFields };
}
export function selectionListFields(): ApiFormFieldSet {