Repository: inventree/inventree
Issue: Observe "default" values for blank fields when creating objects via API

It appears that when creating objects via the API (e.g. with the python interface), then any fields not explicitly specified will have the value set to "blank" or "false", rather than the value specified as "default" for that field in the model..

Needs to be investigated!

Changed files:
InvenTree/stock/test_api.py, InvenTree/InvenTree/serializers.py, InvenTree/InvenTree/api_tester.py, InvenTree/part/test_api.py

diff --git a/InvenTree/InvenTree/api_tester.py b/InvenTree/InvenTree/api_tester.py
index a803e6797..1cbc62ec0 100644
--- a/InvenTree/InvenTree/api_tester.py
+++ b/InvenTree/InvenTree/api_tester.py
@@ -18,6 +18,7 @@ class InvenTreeAPITestCase(APITestCase):
     email = 'test@testing.com'
 
     superuser = False
+    is_staff = True
     auto_login = True
 
     # Set list of roles automatically associated with the user
@@ -40,8 +41,12 @@ class InvenTreeAPITestCase(APITestCase):
 
         if self.superuser:
             self.user.is_superuser = True
-            self.user.save()
 
+        if self.is_staff:
+            self.user.is_staff = True
+
+        self.user.save()
+        
         for role in self.roles:
             self.assignRole(role)
 
diff --git a/InvenTree/InvenTree/serializers.py b/InvenTree/InvenTree/serializers.py
index f093255ff..50a37d8cb 100644
--- a/InvenTree/InvenTree/serializers.py
+++ b/InvenTree/InvenTree/serializers.py
@@ -2,17 +2,19 @@
 Serializers used in various InvenTree apps
 """
 
-
 # -*- coding: utf-8 -*-
 from __future__ import unicode_literals
 
 import os
 
+from collections import OrderedDict
+
 from django.conf import settings
 from django.contrib.auth.models import User
 from django.core.exceptions import ValidationError as DjangoValidationError
 
 from rest_framework import serializers
+from rest_framework.utils import model_meta
 from rest_framework.fields import empty
 from rest_framework.exceptions import ValidationError
 
@@ -42,6 +44,72 @@ class InvenTreeModelSerializer(serializers.ModelSerializer):
     but also ensures that the underlying model class data are checked on validation.
     """
 
+    def __init__(self, instance=None, data=empty, **kwargs):
+
+        self.instance = instance
+
+        # If instance is None, we are creating a new instance
+        if instance is None:
+            
+            if data is empty:
+                data = OrderedDict()
+            else:
+                # Required to side-step immutability of a QueryDict
+                data = data.copy()
+
+            # Add missing fields which have default values
+            ModelClass = self.Meta.model
+
+            fields = model_meta.get_field_info(ModelClass)
+
+            for field_name, field in fields.fields.items():
+
+                if field.has_default() and field_name not in data:
+
+                    value = field.default
+
+                    # Account for callable functions
+                    if callable(value):
+                        try:
+                            value = value()
+                        except:
+                            continue
+
+                    data[field_name] = value
+
+        super().__init__(instance, data, **kwargs)
+
+    def get_initial(self):
+        """
+        Construct initial data for the serializer.
+        Use the 'default' values specified by the django model definition
+        """
+
+        initials = super().get_initial()
+
+        # Are we creating a new instance?
+        if self.instance is None:
+            ModelClass = self.Meta.model
+
+            fields = model_meta.get_field_info(ModelClass)
+
+            for field_name, field in fields.fields.items():
+
+                if field.has_default() and field_name not in initials:
+
+                    value = field.default
+
+                    # Account for callable functions
+                    if callable(value):
+                        try:
+                            value = value()
+                        except:
+                            continue
+
+                    initials[field_name] = value
+
+        return initials
+
     def run_validation(self, data=empty):
         """ Perform serializer validation.
         In addition to running validators on the serializer fields,
diff --git a/InvenTree/part/test_api.py b/InvenTree/part/test_api.py
index d606b1263..4922ed4e0 100644
--- a/InvenTree/part/test_api.py
+++ b/InvenTree/part/test_api.py
@@ -13,6 +13,7 @@ from InvenTree.status_codes import StockStatus
 from part.models import Part, PartCategory
 from stock.models import StockItem
 from company.models import Company
+from common.models import InvenTreeSetting
 
 
 class PartAPITest(InvenTreeAPITestCase):
@@ -311,6 +312,59 @@ class PartAPITest(InvenTreeAPITestCase):
 
             self.assertEqual(len(data['results']), n)
 
+    def test_default_values(self):
+        """
+        Tests for 'default' values:
+
+        Ensure that unspecified fields revert to "default" values
+        (as specified in the model field definition)
+        """
+
+        url = reverse('api-part-list')
+
+        response = self.client.post(url, {
+            'name': 'all defaults',
+            'description': 'my test part',
+            'category': 1,
+        })
+
+        data = response.data
+
+        # Check that the un-specified fields have used correct default values
+        self.assertTrue(data['active'])
+        self.assertFalse(data['virtual'])
+
+        # By default, parts are not purchaseable
+        self.assertFalse(data['purchaseable'])
+
+        # Set the default 'purchaseable' status to True
+        InvenTreeSetting.set_setting(
+            'PART_PURCHASEABLE',
+            True,
+            self.user
+        )
+
+        response = self.client.post(url, {
+            'name': 'all defaults',
+            'description': 'my test part 2',
+            'category': 1,
+        })
+
+        # Part should now be purchaseable by default
+        self.assertTrue(response.data['purchaseable'])
+
+        # "default" values should not be used if the value is specified
+        response = self.client.post(url, {
+            'name': 'all defaults',
+            'description': 'my test part 2',
+            'category': 1,
+            'active': False,
+            'purchaseable': False,
+        })
+
+        self.assertFalse(response.data['active'])
+        self.assertFalse(response.data['purchaseable'])
+
 
 class PartDetailTests(InvenTreeAPITestCase):
     """
@@ -391,7 +445,14 @@ class PartDetailTests(InvenTreeAPITestCase):
 
         # Try to remove the part
         response = self.client.delete(url)
-    
+
+        # As the part is 'active' we cannot delete it
+        self.assertEqual(response.status_code, 405)
+
+        # So, let's make it not active
+        response = self.patch(url, {'active': False}, expected_code=200)
+
+        response = self.client.delete(url)
         self.assertEqual(response.status_code, 204)
 
         # Part count should have reduced
@@ -542,8 +603,6 @@ class PartDetailTests(InvenTreeAPITestCase):
         # And now check that the image has been set
         p = Part.objects.get(pk=pk)
 
-        print("Image:", p.image.file)
-
 
 class PartAPIAggregationTest(InvenTreeAPITestCase):
     """
diff --git a/InvenTree/stock/test_api.py b/InvenTree/stock/test_api.py
index e1fb61633..729bf25a9 100644
--- a/InvenTree/stock/test_api.py
+++ b/InvenTree/stock/test_api.py
@@ -354,16 +354,18 @@ class StockItemTest(StockAPITestCase):
         self.assertContains(response, 'does not exist', status_code=status.HTTP_400_BAD_REQUEST)
 
         # POST without quantity
-        response = self.client.post(
+        response = self.post(
             self.list_url,
-            data={
+            {
                 'part': 1,
                 'location': 1,
-            }
+            },
+            expected_code=201,
         )
 
-        self.assertContains(response, 'This field is required', status_code=status.HTTP_400_BAD_REQUEST)
-
+        # Item should have been created with default quantity
+        self.assertEqual(response.data['quantity'], 1)
+        
         # POST with quantity and part and location
         response = self.client.post(
             self.list_url,