Repository: inventree/InvenTree

Issue:
[bug] existing_image field is ignored for API patch requests
### Please verify that this bug has NOT been raised before.

- [X] I checked and didn't find a similar issue

### Describe the bug*

Hi,

I am very thankful for the implementation of the existing_image endpoint, as it saves us a lot of storage space. However I noticed a problem while I tried to update existing parts: It seems like the existing_image field is simply ignored for patch requests. I tried it with the python API as well as with direct requests and neither seems to do anything. An example request would look like that:

```
 payload = {'params': {'format': 'json'}, 'timeout': 10, 'headers': {'AUTHORIZATION': 'my_token'}, 'auth': None, 'proxies': {}, 'json': {'existing_image': '2881_thumbnail.jpeg'}}
requests.patch('https://inventree-test/api/part/2882/', **payload)
```

The request does not fail, it returns a `200` response with the part data unchanged.

### Steps to Reproduce

Try to change a image with the `existing_image` field with an API path request.

### Expected behaviour

The part image should change as according to https://github.com/inventree/InvenTree/pull/6021#issue-2021905841 the `existing_image` field should work for existing parts as well as for new ones (where it does work).

### Deployment Method

- [ ] Docker
- [X] Bare metal

### Version Information

# Version Information:
InvenTree-Version: 0.13.0
Django Version: 3.2.23
Commit Hash: 301afaa
Commit Date: 2023-12-15

Database: postgresql
Debug-Mode: True
Deployed using Docker: False
Platform: Linux-5.15.0-91-generic-x86_64-with-glibc2.35
Installer: GIT

Active plugins: [{'name': 'InvenTreeBarcode', 'slug': 'inventreebarcode', 'version': '2.0.0'}, {'name': 'InvenTreeCoreNotificationsPlugin', 'slug': 'inventreecorenotificationsplugin', 'version': '1.0.0'}, {'name': 'InvenTreeCurrencyExchange', 'slug': 'inventreecurrencyexchange', 'version': '1.0.0'}, {'name': 'InvenTreeLabel', 'slug': 'inventreelabel', 'version': '1.0.0'}, {'name': 'InvenTreeLabelSheet', 'slug': 'inventreelabelsheet', 'version': '1.0.0'}, {'name': 'DigiKeyPlugin', 'slug': 'digikeyplugin', 'version': '1.0.0'}, {'name': 'LCSCPlugin', 'slug': 'lcscplugin', 'version': '1.0.0'}, {'name': 'MouserPlugin', 'slug': 'mouserplugin', 'version': '1.0.0'}, {'name': 'TMEPlugin', 'slug': 'tmeplugin', 'version': '1.0.0'}, {'name': 'SupplierCart', 'slug': 'suppliercart', 'version': '0.1.0'}]


### Please verify if you can reproduce this bug on the demo site.

- [x] I can reproduce this bug on the demo site.

### Relevant log output

_No response_


Discussion:
I was having challenges to use it in PUI myself.
Looks like the culprit lies on line 615 here:

https://github.com/inventree/InvenTree/blob/be1820fb948f404e4df6b13aa6fd372964f1ffd1/InvenTree/part/serializers.py#L606-L618

As you're trying to patch, the "create" field isn't defined. So the `exisiting_image` field is removed from the incoming data.

@SchrodingersGat It seems that the name and usage for this function differ in meaning from the docstring. The fields are skipped when _not_ creating a new part. Atm, only a POST will include the `exisiting_image` field.
Ok, yes that docstring is definitely wrong. And I can see the benefits of allowing `existing_image` for a `PATCH` request. I had not considered that originally but happy for it to be updated. I'm away for a few weeks but if someone wants to submit a PR to address this I'll happily review and merge

Resolving patch:
diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py
--- a/InvenTree/part/serializers.py
+++ b/InvenTree/part/serializers.py
@@ -592,7 +592,10 @@ def __init__(self, *args, **kwargs):
 
         if not create:
             # These fields are only used for the LIST API endpoint
-            for f in self.skip_create_fields()[1:]:
+            for f in self.skip_create_fields():
+                # Fields required for certain operations, but are not part of the model
+                if f in ["remote_image", "existing_image"]:
+                    continue
                 self.fields.pop(f)
 
         if not pricing:


Test patch:
diff --git a/InvenTree/part/test_api.py b/InvenTree/part/test_api.py
--- a/InvenTree/part/test_api.py
+++ b/InvenTree/part/test_api.py
@@ -1741,6 +1741,64 @@ def test_existing_image(self):
 
         self.assertEqual(response.data['image'], image_name)
 
+    def test_update_existing_image(self):
+        """Test that we can update the image of an existing part with an already existing image"""
+        # First, upload an image for an existing part
+        p = Part.objects.first()
+
+        fn = 'part_image_123abc.png'
+
+        img = PIL.Image.new('RGB', (128, 128), color='blue')
+        img.save(fn)
+
+        # Upload the image to a part
+        with open(fn, 'rb') as img_file:
+            response = self.upload_client.patch(
+                reverse('api-part-detail', kwargs={'pk': p.pk}),
+                {
+                    'image': img_file,
+                },
+            )
+
+            self.assertEqual(response.status_code, 200)
+            image_name = response.data['image']
+            self.assertTrue(image_name.startswith('/media/part_images/part_image'))
+
+        # Create a new part without an image
+        response = self.post(
+            reverse('api-part-list'),
+            {
+                'name': 'Some New Part',
+                'description': 'Description of the part',
+                'category': 1
+            },
+            expected_code=201
+        )
+
+        self.assertEqual(response.data['image'], None)
+        part_pk = response.data['pk']
+
+        # Add image from the first part to the new part
+        response = self.patch(
+            reverse('api-part-detail', kwargs={'pk': part_pk}),
+            {
+                'existing_image': image_name
+            },
+            expected_code=200
+        )
+
+        self.assertEqual(response.data['image'], image_name)
+
+        # Attempt to add a non-existent image to an existing part
+        last_p = Part.objects.last()
+        response = self.patch(
+            reverse('api-part-detail', kwargs={'pk': last_p.pk}),
+            {
+                'existing_image': 'bogus_image.jpg'
+            },
+            expected_code=400
+        )
+
     def test_details(self):
         """Test that the required details are available."""
         p = Part.objects.get(pk=1)