diff --git a/InvenTree/InvenTree/metadata.py b/InvenTree/InvenTree/metadata.py
index 4294c943ba..46b3acfc21 100644
--- a/InvenTree/InvenTree/metadata.py
+++ b/InvenTree/InvenTree/metadata.py
@@ -118,20 +118,31 @@ class InvenTreeMetadata(SimpleMetadata):
# Iterate through simple fields
for name, field in model_fields.fields.items():
- if field.has_default() and name in serializer_info.keys():
+ if name in serializer_info.keys():
- default = field.default
+ if field.has_default():
- if callable(default):
- try:
- default = default()
- except:
- continue
+ default = field.default
- serializer_info[name]['default'] = default
+ if callable(default):
+ try:
+ default = default()
+ except:
+ continue
- elif name in model_default_values:
- serializer_info[name]['default'] = model_default_values[name]
+ serializer_info[name]['default'] = default
+
+ elif name in model_default_values:
+ serializer_info[name]['default'] = model_default_values[name]
+
+ # Attributes to copy from the model to the field (if they don't exist)
+ attributes = ['help_text']
+
+ for attr in attributes:
+ if attr not in serializer_info[name]:
+
+ if hasattr(field, attr):
+ serializer_info[name][attr] = getattr(field, attr)
# Iterate through relations
for name, relation in model_fields.relations.items():
diff --git a/InvenTree/InvenTree/serializers.py b/InvenTree/InvenTree/serializers.py
index d2d00a932c..ab5a27594f 100644
--- a/InvenTree/InvenTree/serializers.py
+++ b/InvenTree/InvenTree/serializers.py
@@ -296,3 +296,17 @@ class InvenTreeImageSerializerField(serializers.ImageField):
return None
return os.path.join(str(settings.MEDIA_URL), str(value))
+
+
+class InvenTreeDecimalField(serializers.FloatField):
+ """
+ Custom serializer for decimal fields. Solves the following issues:
+
+ - The normal DRF DecimalField renders values with trailing zeros
+ - Using a FloatField can result in rounding issues: https://code.djangoproject.com/ticket/30290
+ """
+
+ def to_internal_value(self, data):
+
+ # Convert the value to a string, and then a decimal
+ return Decimal(str(data))
diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py
index c5a90ac7f3..cda30b0a27 100644
--- a/InvenTree/InvenTree/settings.py
+++ b/InvenTree/InvenTree/settings.py
@@ -92,6 +92,12 @@ DEBUG = _is_true(get_setting(
CONFIG.get('debug', True)
))
+# Determine if we are running in "demo mode"
+DEMO_MODE = _is_true(get_setting(
+ 'INVENTREE_DEMO',
+ CONFIG.get('demo', False)
+))
+
DOCKER = _is_true(get_setting(
'INVENTREE_DOCKER',
False
@@ -234,7 +240,10 @@ STATIC_COLOR_THEMES_DIR = os.path.join(STATIC_ROOT, 'css', 'color-themes')
MEDIA_URL = '/media/'
if DEBUG:
- logger.info("InvenTree running in DEBUG mode")
+ logger.info("InvenTree running with DEBUG enabled")
+
+if DEMO_MODE:
+ logger.warning("InvenTree running in DEMO mode")
logger.debug(f"MEDIA_ROOT: '{MEDIA_ROOT}'")
logger.debug(f"STATIC_ROOT: '{STATIC_ROOT}'")
diff --git a/InvenTree/InvenTree/utils.py b/InvenTree/InvenTree/utils.py
deleted file mode 100644
index dc28da81a0..0000000000
--- a/InvenTree/InvenTree/utils.py
+++ /dev/null
@@ -1,13 +0,0 @@
-from rest_framework.views import exception_handler
-
-
-def api_exception_handler(exc, context):
- response = exception_handler(exc, context)
-
- # Now add the HTTP status code to the response.
- if response is not None:
-
- data = {'error': response.data}
- response.data = data
-
- return response
diff --git a/InvenTree/build/serializers.py b/InvenTree/build/serializers.py
index 29257341b2..55576fd328 100644
--- a/InvenTree/build/serializers.py
+++ b/InvenTree/build/serializers.py
@@ -18,8 +18,9 @@ from rest_framework.serializers import ValidationError
from InvenTree.serializers import InvenTreeModelSerializer, InvenTreeAttachmentSerializer
from InvenTree.serializers import InvenTreeAttachmentSerializerField, UserSerializerBrief
-from InvenTree.status_codes import StockStatus
import InvenTree.helpers
+from InvenTree.serializers import InvenTreeDecimalField
+from InvenTree.status_codes import StockStatus
from stock.models import StockItem, StockLocation
from stock.serializers import StockItemSerializerBrief, LocationSerializer
@@ -41,7 +42,7 @@ class BuildSerializer(InvenTreeModelSerializer):
part_detail = PartBriefSerializer(source='part', many=False, read_only=True)
- quantity = serializers.FloatField()
+ quantity = InvenTreeDecimalField()
overdue = serializers.BooleanField(required=False, read_only=True)
@@ -473,7 +474,7 @@ class BuildItemSerializer(InvenTreeModelSerializer):
stock_item_detail = StockItemSerializerBrief(source='stock_item', read_only=True)
location_detail = LocationSerializer(source='stock_item.location', read_only=True)
- quantity = serializers.FloatField()
+ quantity = InvenTreeDecimalField()
def __init__(self, *args, **kwargs):
diff --git a/InvenTree/company/serializers.py b/InvenTree/company/serializers.py
index 719d94b0a6..a6303cc561 100644
--- a/InvenTree/company/serializers.py
+++ b/InvenTree/company/serializers.py
@@ -8,9 +8,10 @@ from rest_framework import serializers
from sql_util.utils import SubqueryCount
+from InvenTree.serializers import InvenTreeDecimalField
+from InvenTree.serializers import InvenTreeImageSerializerField
from InvenTree.serializers import InvenTreeModelSerializer
from InvenTree.serializers import InvenTreeMoneySerializer
-from InvenTree.serializers import InvenTreeImageSerializerField
from part.serializers import PartBriefSerializer
@@ -255,7 +256,7 @@ class SupplierPartSerializer(InvenTreeModelSerializer):
class SupplierPriceBreakSerializer(InvenTreeModelSerializer):
""" Serializer for SupplierPriceBreak object """
- quantity = serializers.FloatField()
+ quantity = InvenTreeDecimalField()
price = InvenTreeMoneySerializer(
allow_null=True,
diff --git a/InvenTree/order/serializers.py b/InvenTree/order/serializers.py
index d2fcaf7009..9fe0945168 100644
--- a/InvenTree/order/serializers.py
+++ b/InvenTree/order/serializers.py
@@ -20,10 +20,10 @@ from sql_util.utils import SubqueryCount
from common.settings import currency_code_mappings
from company.serializers import CompanyBriefSerializer, SupplierPartSerializer
-
+from InvenTree.serializers import InvenTreeAttachmentSerializer
from InvenTree.helpers import normalize
from InvenTree.serializers import InvenTreeModelSerializer
-from InvenTree.serializers import InvenTreeAttachmentSerializer
+from InvenTree.serializers import InvenTreeDecimalField
from InvenTree.serializers import InvenTreeMoneySerializer
from InvenTree.serializers import InvenTreeAttachmentSerializerField
from InvenTree.status_codes import StockStatus
@@ -550,8 +550,8 @@ class SOLineItemSerializer(InvenTreeModelSerializer):
part_detail = PartBriefSerializer(source='part', many=False, read_only=True)
allocations = SalesOrderAllocationSerializer(many=True, read_only=True, location_detail=True)
- quantity = serializers.FloatField()
-
+ quantity = InvenTreeDecimalField()
+
allocated = serializers.FloatField(source='allocated_quantity', read_only=True)
fulfilled = serializers.FloatField(source='fulfilled_quantity', read_only=True)
diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py
index 47ce3f66c8..9deece4863 100644
--- a/InvenTree/part/serializers.py
+++ b/InvenTree/part/serializers.py
@@ -15,6 +15,7 @@ from sql_util.utils import SubqueryCount, SubquerySum
from djmoney.contrib.django_rest_framework import MoneyField
from InvenTree.serializers import (InvenTreeAttachmentSerializerField,
+ InvenTreeDecimalField,
InvenTreeImageSerializerField,
InvenTreeModelSerializer,
InvenTreeAttachmentSerializer,
@@ -120,7 +121,7 @@ class PartSalePriceSerializer(InvenTreeModelSerializer):
Serializer for sale prices for Part model.
"""
- quantity = serializers.FloatField()
+ quantity = InvenTreeDecimalField()
price = InvenTreeMoneySerializer(
allow_null=True
@@ -144,7 +145,7 @@ class PartInternalPriceSerializer(InvenTreeModelSerializer):
Serializer for internal prices for Part model.
"""
- quantity = serializers.FloatField()
+ quantity = InvenTreeDecimalField()
price = InvenTreeMoneySerializer(
allow_null=True
@@ -428,7 +429,7 @@ class BomItemSerializer(InvenTreeModelSerializer):
price_range = serializers.CharField(read_only=True)
- quantity = serializers.FloatField()
+ quantity = InvenTreeDecimalField()
part = serializers.PrimaryKeyRelatedField(queryset=Part.objects.filter(assembly=True))
diff --git a/InvenTree/part/templatetags/inventree_extras.py b/InvenTree/part/templatetags/inventree_extras.py
index 42c66cf78e..e31fb9e398 100644
--- a/InvenTree/part/templatetags/inventree_extras.py
+++ b/InvenTree/part/templatetags/inventree_extras.py
@@ -90,6 +90,13 @@ def inventree_in_debug_mode(*args, **kwargs):
return djangosettings.DEBUG
+@register.simple_tag()
+def inventree_demo_mode(*args, **kwargs):
+ """ Return True if the server is running in DEMO mode """
+
+ return djangosettings.DEMO_MODE
+
+
@register.simple_tag()
def inventree_docker_mode(*args, **kwargs):
""" Return True if the server is running as a Docker image """
diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py
index 2ffc2e8d69..0c045f1cf5 100644
--- a/InvenTree/stock/api.py
+++ b/InvenTree/stock/api.py
@@ -69,6 +69,13 @@ class StockDetail(generics.RetrieveUpdateDestroyAPIView):
return queryset
+ def get_serializer_context(self):
+
+ ctx = super().get_serializer_context()
+ ctx['user'] = getattr(self.request, 'user', None)
+
+ return ctx
+
def get_serializer(self, *args, **kwargs):
kwargs['part_detail'] = True
@@ -79,16 +86,6 @@ class StockDetail(generics.RetrieveUpdateDestroyAPIView):
return self.serializer_class(*args, **kwargs)
- def update(self, request, *args, **kwargs):
- """
- Record the user who updated the item
- """
-
- # TODO: Record the user!
- # user = request.user
-
- return super().update(request, *args, **kwargs)
-
def perform_destroy(self, instance):
"""
Instead of "deleting" the StockItem
@@ -392,6 +389,13 @@ class StockList(generics.ListCreateAPIView):
queryset = StockItem.objects.all()
filterset_class = StockFilter
+ def get_serializer_context(self):
+
+ ctx = super().get_serializer_context()
+ ctx['user'] = getattr(self.request, 'user', None)
+
+ return ctx
+
def create(self, request, *args, **kwargs):
"""
Create a new StockItem object via the API.
diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py
index 8e07074a76..70e557a901 100644
--- a/InvenTree/stock/models.py
+++ b/InvenTree/stock/models.py
@@ -265,15 +265,15 @@ class StockItem(MPTTModel):
user = kwargs.pop('user', None)
+ if user is None:
+ user = getattr(self, '_user', None)
+
# If 'add_note = False' specified, then no tracking note will be added for item creation
add_note = kwargs.pop('add_note', True)
notes = kwargs.pop('notes', '')
-
- if not self.pk:
- # StockItem has not yet been saved
- add_note = add_note and True
- else:
+
+ if self.pk:
# StockItem has already been saved
# Check if "interesting" fields have been changed
@@ -301,11 +301,10 @@ class StockItem(MPTTModel):
except (ValueError, StockItem.DoesNotExist):
pass
- add_note = False
-
super(StockItem, self).save(*args, **kwargs)
- if add_note:
+ # If user information is provided, and no existing note exists, create one!
+ if user and self.tracking_info.count() == 0:
tracking_info = {
'status': self.status,
diff --git a/InvenTree/stock/serializers.py b/InvenTree/stock/serializers.py
index 850ebcea3b..31f21605a2 100644
--- a/InvenTree/stock/serializers.py
+++ b/InvenTree/stock/serializers.py
@@ -32,6 +32,7 @@ from company.serializers import SupplierPartSerializer
import InvenTree.helpers
import InvenTree.serializers
+from InvenTree.serializers import InvenTreeDecimalField
from part.serializers import PartBriefSerializer
@@ -55,7 +56,8 @@ class StockItemSerializerBrief(InvenTree.serializers.InvenTreeModelSerializer):
location_name = serializers.CharField(source='location', read_only=True)
part_name = serializers.CharField(source='part.full_name', read_only=True)
- quantity = serializers.FloatField()
+
+ quantity = InvenTreeDecimalField()
class Meta:
model = StockItem
@@ -79,6 +81,15 @@ class StockItemSerializer(InvenTree.serializers.InvenTreeModelSerializer):
- Includes serialization for the item location
"""
+ def update(self, instance, validated_data):
+ """
+ Custom update method to pass the user information through to the instance
+ """
+
+ instance._user = self.context['user']
+
+ return super().update(instance, validated_data)
+
@staticmethod
def annotate_queryset(queryset):
"""
@@ -136,7 +147,7 @@ class StockItemSerializer(InvenTree.serializers.InvenTreeModelSerializer):
tracking_items = serializers.IntegerField(source='tracking_info_count', read_only=True, required=False)
- # quantity = serializers.FloatField()
+ quantity = InvenTreeDecimalField()
allocated = serializers.FloatField(source='allocation_count', required=False)
diff --git a/InvenTree/templates/InvenTree/settings/user.html b/InvenTree/templates/InvenTree/settings/user.html
index 2b2637330c..d22c89954f 100644
--- a/InvenTree/templates/InvenTree/settings/user.html
+++ b/InvenTree/templates/InvenTree/settings/user.html
@@ -12,12 +12,15 @@
{% endblock %}
{% block actions %}
+{% inventree_demo_mode as demo %}
+{% if not demo %}
{% trans "Edit" %}
{% trans "Set Password" %}
+{% endif %}
{% endblock %}
{% block content %}
diff --git a/InvenTree/templates/account/login.html b/InvenTree/templates/account/login.html
index fbe48224b4..6e62560bfa 100644
--- a/InvenTree/templates/account/login.html
+++ b/InvenTree/templates/account/login.html
@@ -1,5 +1,6 @@
{% extends "account/base.html" %}
+{% load inventree_extras %}
{% load i18n account socialaccount crispy_forms_tags inventree_extras %}
{% block head_title %}{% trans "Sign In" %}{% endblock %}
@@ -10,6 +11,7 @@
{% settings_value 'LOGIN_ENABLE_PWD_FORGOT' as enable_pwd_forgot %}
{% settings_value 'LOGIN_ENABLE_SSO' as enable_sso %}
{% mail_configured as mail_conf %}
+{% inventree_demo_mode as demo %}
{% trans "Sign In" %}
@@ -36,9 +38,16 @@ for a account and sign in below:{% endblocktrans %}
{% trans "Sign In" %}
- {% if mail_conf and enable_pwd_forgot %}
+ {% if mail_conf and enable_pwd_forgot and not demo %}
{% trans "Forgot Password?" %}
{% endif %}
+ {% if demo %}
+
+
+
+ {% endif %}
{% if enable_sso %}
diff --git a/InvenTree/templates/base.html b/InvenTree/templates/base.html
index 6dc0d7d78a..262a749bfa 100644
--- a/InvenTree/templates/base.html
+++ b/InvenTree/templates/base.html
@@ -86,25 +86,20 @@
-
- {% if server_restart_required %}
-
+
+ {% block alerts %}
+
+
+ {% if server_restart_required %}
{% trans "Server Restart Required" %}
- {% trans "A configuration option has been changed which requires a server restart" %}.
-
- {% trans "Contact your system administrator for further information" %}
+ {% trans "A configuration option has been changed which requires a server restart" %}. {% trans "Contact your system administrator for further information" %}
-
- {% endif %}
-
- {% block alerts %}
-
-
+ {% endif %}
{% endblock %}
diff --git a/InvenTree/templates/navbar.html b/InvenTree/templates/navbar.html
index eeb049d320..c339d7b4e1 100644
--- a/InvenTree/templates/navbar.html
+++ b/InvenTree/templates/navbar.html
@@ -4,6 +4,7 @@
{% settings_value 'BARCODE_ENABLE' as barcodes %}
{% settings_value 'STICKY_HEADER' user=request.user as sticky %}
+{% inventree_demo_mode as demo %}
@@ -58,6 +59,9 @@
{% endif %}
+ {% if demo %}
+ {% include "navbar_demo.html" %}
+ {% endif %}
{% include "search_form.html" %}
{% if barcodes %}
@@ -78,7 +82,7 @@