Merge branch 'master' into partial-shipment

# Conflicts:
#	InvenTree/order/serializers.py
This commit is contained in:
Oliver 2021-11-18 23:43:36 +11:00
commit 521ec4f1e6
17 changed files with 138 additions and 70 deletions

View File

@ -118,20 +118,31 @@ class InvenTreeMetadata(SimpleMetadata):
# Iterate through simple fields # Iterate through simple fields
for name, field in model_fields.fields.items(): 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): default = field.default
try:
default = default()
except:
continue
serializer_info[name]['default'] = default if callable(default):
try:
default = default()
except:
continue
elif name in model_default_values: serializer_info[name]['default'] = default
serializer_info[name]['default'] = model_default_values[name]
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 # Iterate through relations
for name, relation in model_fields.relations.items(): for name, relation in model_fields.relations.items():

View File

@ -296,3 +296,17 @@ class InvenTreeImageSerializerField(serializers.ImageField):
return None return None
return os.path.join(str(settings.MEDIA_URL), str(value)) 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))

View File

@ -92,6 +92,12 @@ DEBUG = _is_true(get_setting(
CONFIG.get('debug', True) 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( DOCKER = _is_true(get_setting(
'INVENTREE_DOCKER', 'INVENTREE_DOCKER',
False False
@ -234,7 +240,10 @@ STATIC_COLOR_THEMES_DIR = os.path.join(STATIC_ROOT, 'css', 'color-themes')
MEDIA_URL = '/media/' MEDIA_URL = '/media/'
if DEBUG: 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"MEDIA_ROOT: '{MEDIA_ROOT}'")
logger.debug(f"STATIC_ROOT: '{STATIC_ROOT}'") logger.debug(f"STATIC_ROOT: '{STATIC_ROOT}'")

View File

@ -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

View File

@ -18,8 +18,9 @@ from rest_framework.serializers import ValidationError
from InvenTree.serializers import InvenTreeModelSerializer, InvenTreeAttachmentSerializer from InvenTree.serializers import InvenTreeModelSerializer, InvenTreeAttachmentSerializer
from InvenTree.serializers import InvenTreeAttachmentSerializerField, UserSerializerBrief from InvenTree.serializers import InvenTreeAttachmentSerializerField, UserSerializerBrief
from InvenTree.status_codes import StockStatus
import InvenTree.helpers import InvenTree.helpers
from InvenTree.serializers import InvenTreeDecimalField
from InvenTree.status_codes import StockStatus
from stock.models import StockItem, StockLocation from stock.models import StockItem, StockLocation
from stock.serializers import StockItemSerializerBrief, LocationSerializer from stock.serializers import StockItemSerializerBrief, LocationSerializer
@ -41,7 +42,7 @@ class BuildSerializer(InvenTreeModelSerializer):
part_detail = PartBriefSerializer(source='part', many=False, read_only=True) part_detail = PartBriefSerializer(source='part', many=False, read_only=True)
quantity = serializers.FloatField() quantity = InvenTreeDecimalField()
overdue = serializers.BooleanField(required=False, read_only=True) 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) stock_item_detail = StockItemSerializerBrief(source='stock_item', read_only=True)
location_detail = LocationSerializer(source='stock_item.location', read_only=True) location_detail = LocationSerializer(source='stock_item.location', read_only=True)
quantity = serializers.FloatField() quantity = InvenTreeDecimalField()
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):

View File

@ -8,9 +8,10 @@ from rest_framework import serializers
from sql_util.utils import SubqueryCount 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 InvenTreeModelSerializer
from InvenTree.serializers import InvenTreeMoneySerializer from InvenTree.serializers import InvenTreeMoneySerializer
from InvenTree.serializers import InvenTreeImageSerializerField
from part.serializers import PartBriefSerializer from part.serializers import PartBriefSerializer
@ -255,7 +256,7 @@ class SupplierPartSerializer(InvenTreeModelSerializer):
class SupplierPriceBreakSerializer(InvenTreeModelSerializer): class SupplierPriceBreakSerializer(InvenTreeModelSerializer):
""" Serializer for SupplierPriceBreak object """ """ Serializer for SupplierPriceBreak object """
quantity = serializers.FloatField() quantity = InvenTreeDecimalField()
price = InvenTreeMoneySerializer( price = InvenTreeMoneySerializer(
allow_null=True, allow_null=True,

View File

@ -20,10 +20,10 @@ from sql_util.utils import SubqueryCount
from common.settings import currency_code_mappings from common.settings import currency_code_mappings
from company.serializers import CompanyBriefSerializer, SupplierPartSerializer from company.serializers import CompanyBriefSerializer, SupplierPartSerializer
from InvenTree.serializers import InvenTreeAttachmentSerializer
from InvenTree.helpers import normalize from InvenTree.helpers import normalize
from InvenTree.serializers import InvenTreeModelSerializer from InvenTree.serializers import InvenTreeModelSerializer
from InvenTree.serializers import InvenTreeAttachmentSerializer from InvenTree.serializers import InvenTreeDecimalField
from InvenTree.serializers import InvenTreeMoneySerializer from InvenTree.serializers import InvenTreeMoneySerializer
from InvenTree.serializers import InvenTreeAttachmentSerializerField from InvenTree.serializers import InvenTreeAttachmentSerializerField
from InvenTree.status_codes import StockStatus from InvenTree.status_codes import StockStatus
@ -550,7 +550,7 @@ class SOLineItemSerializer(InvenTreeModelSerializer):
part_detail = PartBriefSerializer(source='part', many=False, read_only=True) part_detail = PartBriefSerializer(source='part', many=False, read_only=True)
allocations = SalesOrderAllocationSerializer(many=True, read_only=True, location_detail=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) allocated = serializers.FloatField(source='allocated_quantity', read_only=True)
fulfilled = serializers.FloatField(source='fulfilled_quantity', read_only=True) fulfilled = serializers.FloatField(source='fulfilled_quantity', read_only=True)

View File

@ -15,6 +15,7 @@ from sql_util.utils import SubqueryCount, SubquerySum
from djmoney.contrib.django_rest_framework import MoneyField from djmoney.contrib.django_rest_framework import MoneyField
from InvenTree.serializers import (InvenTreeAttachmentSerializerField, from InvenTree.serializers import (InvenTreeAttachmentSerializerField,
InvenTreeDecimalField,
InvenTreeImageSerializerField, InvenTreeImageSerializerField,
InvenTreeModelSerializer, InvenTreeModelSerializer,
InvenTreeAttachmentSerializer, InvenTreeAttachmentSerializer,
@ -120,7 +121,7 @@ class PartSalePriceSerializer(InvenTreeModelSerializer):
Serializer for sale prices for Part model. Serializer for sale prices for Part model.
""" """
quantity = serializers.FloatField() quantity = InvenTreeDecimalField()
price = InvenTreeMoneySerializer( price = InvenTreeMoneySerializer(
allow_null=True allow_null=True
@ -144,7 +145,7 @@ class PartInternalPriceSerializer(InvenTreeModelSerializer):
Serializer for internal prices for Part model. Serializer for internal prices for Part model.
""" """
quantity = serializers.FloatField() quantity = InvenTreeDecimalField()
price = InvenTreeMoneySerializer( price = InvenTreeMoneySerializer(
allow_null=True allow_null=True
@ -428,7 +429,7 @@ class BomItemSerializer(InvenTreeModelSerializer):
price_range = serializers.CharField(read_only=True) price_range = serializers.CharField(read_only=True)
quantity = serializers.FloatField() quantity = InvenTreeDecimalField()
part = serializers.PrimaryKeyRelatedField(queryset=Part.objects.filter(assembly=True)) part = serializers.PrimaryKeyRelatedField(queryset=Part.objects.filter(assembly=True))

View File

@ -90,6 +90,13 @@ def inventree_in_debug_mode(*args, **kwargs):
return djangosettings.DEBUG 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() @register.simple_tag()
def inventree_docker_mode(*args, **kwargs): def inventree_docker_mode(*args, **kwargs):
""" Return True if the server is running as a Docker image """ """ Return True if the server is running as a Docker image """

View File

@ -69,6 +69,13 @@ class StockDetail(generics.RetrieveUpdateDestroyAPIView):
return queryset 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): def get_serializer(self, *args, **kwargs):
kwargs['part_detail'] = True kwargs['part_detail'] = True
@ -79,16 +86,6 @@ class StockDetail(generics.RetrieveUpdateDestroyAPIView):
return self.serializer_class(*args, **kwargs) 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): def perform_destroy(self, instance):
""" """
Instead of "deleting" the StockItem Instead of "deleting" the StockItem
@ -392,6 +389,13 @@ class StockList(generics.ListCreateAPIView):
queryset = StockItem.objects.all() queryset = StockItem.objects.all()
filterset_class = StockFilter 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): def create(self, request, *args, **kwargs):
""" """
Create a new StockItem object via the API. Create a new StockItem object via the API.

View File

@ -265,15 +265,15 @@ class StockItem(MPTTModel):
user = kwargs.pop('user', None) 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 # If 'add_note = False' specified, then no tracking note will be added for item creation
add_note = kwargs.pop('add_note', True) add_note = kwargs.pop('add_note', True)
notes = kwargs.pop('notes', '') notes = kwargs.pop('notes', '')
if not self.pk: if self.pk:
# StockItem has not yet been saved
add_note = add_note and True
else:
# StockItem has already been saved # StockItem has already been saved
# Check if "interesting" fields have been changed # Check if "interesting" fields have been changed
@ -301,11 +301,10 @@ class StockItem(MPTTModel):
except (ValueError, StockItem.DoesNotExist): except (ValueError, StockItem.DoesNotExist):
pass pass
add_note = False
super(StockItem, self).save(*args, **kwargs) 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 = { tracking_info = {
'status': self.status, 'status': self.status,

View File

@ -32,6 +32,7 @@ from company.serializers import SupplierPartSerializer
import InvenTree.helpers import InvenTree.helpers
import InvenTree.serializers import InvenTree.serializers
from InvenTree.serializers import InvenTreeDecimalField
from part.serializers import PartBriefSerializer from part.serializers import PartBriefSerializer
@ -55,7 +56,8 @@ class StockItemSerializerBrief(InvenTree.serializers.InvenTreeModelSerializer):
location_name = serializers.CharField(source='location', read_only=True) location_name = serializers.CharField(source='location', read_only=True)
part_name = serializers.CharField(source='part.full_name', read_only=True) part_name = serializers.CharField(source='part.full_name', read_only=True)
quantity = serializers.FloatField()
quantity = InvenTreeDecimalField()
class Meta: class Meta:
model = StockItem model = StockItem
@ -79,6 +81,15 @@ class StockItemSerializer(InvenTree.serializers.InvenTreeModelSerializer):
- Includes serialization for the item location - 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 @staticmethod
def annotate_queryset(queryset): 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) 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) allocated = serializers.FloatField(source='allocation_count', required=False)

View File

@ -12,12 +12,15 @@
{% endblock %} {% endblock %}
{% block actions %} {% block actions %}
{% inventree_demo_mode as demo %}
{% if not demo %}
<div class='btn btn-primary' type='button' id='edit-user' title='{% trans "Edit User Information" %}'> <div class='btn btn-primary' type='button' id='edit-user' title='{% trans "Edit User Information" %}'>
<span class='fas fa-user-cog'></span> {% trans "Edit" %} <span class='fas fa-user-cog'></span> {% trans "Edit" %}
</div> </div>
<div class='btn btn-primary' type='button' id='edit-password' title='{% trans "Change Password" %}'> <div class='btn btn-primary' type='button' id='edit-password' title='{% trans "Change Password" %}'>
<span class='fas fa-key'></span> {% trans "Set Password" %} <span class='fas fa-key'></span> {% trans "Set Password" %}
</div> </div>
{% endif %}
{% endblock %} {% endblock %}
{% block content %} {% block content %}

View File

@ -1,5 +1,6 @@
{% extends "account/base.html" %} {% extends "account/base.html" %}
{% load inventree_extras %}
{% load i18n account socialaccount crispy_forms_tags inventree_extras %} {% load i18n account socialaccount crispy_forms_tags inventree_extras %}
{% block head_title %}{% trans "Sign In" %}{% endblock %} {% 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_PWD_FORGOT' as enable_pwd_forgot %}
{% settings_value 'LOGIN_ENABLE_SSO' as enable_sso %} {% settings_value 'LOGIN_ENABLE_SSO' as enable_sso %}
{% mail_configured as mail_conf %} {% mail_configured as mail_conf %}
{% inventree_demo_mode as demo %}
<h1>{% trans "Sign In" %}</h1> <h1>{% trans "Sign In" %}</h1>
@ -36,9 +38,16 @@ for a account and sign in below:{% endblocktrans %}</p>
<div class="btn-group float-right" role="group"> <div class="btn-group float-right" role="group">
<button class="btn btn-success" type="submit">{% trans "Sign In" %}</button> <button class="btn btn-success" type="submit">{% trans "Sign In" %}</button>
</div> </div>
{% if mail_conf and enable_pwd_forgot %} {% if mail_conf and enable_pwd_forgot and not demo %}
<a class="" href="{% url 'account_reset_password' %}"><small>{% trans "Forgot Password?" %}</small></a> <a class="" href="{% url 'account_reset_password' %}"><small>{% trans "Forgot Password?" %}</small></a>
{% endif %} {% endif %}
{% if demo %}
<p>
<h6>
{% trans "InvenTree demo instance" %} - <a href='https://inventree.readthedocs.io/en/latest/demo/'>{% trans "Click here for login details" %}</a>
</h6>
</p>
{% endif %}
</form> </form>
{% if enable_sso %} {% if enable_sso %}

View File

@ -87,24 +87,19 @@
</div> </div>
<main class='col ps-md-2 pt-2 pe-2'> <main class='col ps-md-2 pt-2 pe-2'>
{% if server_restart_required %} {% block alerts %}
<div class='notification-area' id='restart-required'> <div class='notification-area' id='alerts'>
<!-- Div for displayed alerts -->
{% if server_restart_required %}
<div id='alert-restart-server' class='alert alert-danger' role='alert'> <div id='alert-restart-server' class='alert alert-danger' role='alert'>
<span class='fas fa-server'></span> <span class='fas fa-server'></span>
<b>{% trans "Server Restart Required" %}</b> <b>{% trans "Server Restart Required" %}</b>
<small> <small>
<br> <br>
{% trans "A configuration option has been changed which requires a server restart" %}. {% trans "A configuration option has been changed which requires a server restart" %}. {% trans "Contact your system administrator for further information" %}
<br>
{% trans "Contact your system administrator for further information" %}
</small> </small>
</div> </div>
</div> {% endif %}
{% endif %}
{% block alerts %}
<div class='notification-area' id='alerts'>
<!-- Div for displayed alerts -->
</div> </div>
{% endblock %} {% endblock %}

View File

@ -4,6 +4,7 @@
{% settings_value 'BARCODE_ENABLE' as barcodes %} {% settings_value 'BARCODE_ENABLE' as barcodes %}
{% settings_value 'STICKY_HEADER' user=request.user as sticky %} {% settings_value 'STICKY_HEADER' user=request.user as sticky %}
{% inventree_demo_mode as demo %}
<nav class="navbar {% if sticky %}fixed-top{% endif %} navbar-expand-lg navbar-light"> <nav class="navbar {% if sticky %}fixed-top{% endif %} navbar-expand-lg navbar-light">
<div class="container-fluid"> <div class="container-fluid">
@ -58,6 +59,9 @@
{% endif %} {% endif %}
</ul> </ul>
</div> </div>
{% if demo %}
{% include "navbar_demo.html" %}
{% endif %}
{% include "search_form.html" %} {% include "search_form.html" %}
<ul class='navbar-nav flex-row'> <ul class='navbar-nav flex-row'>
{% if barcodes %} {% if barcodes %}
@ -78,7 +82,7 @@
</a> </a>
<ul class='dropdown-menu dropdown-menu-end inventree-navbar-menu'> <ul class='dropdown-menu dropdown-menu-end inventree-navbar-menu'>
{% if user.is_authenticated %} {% if user.is_authenticated %}
{% if user.is_staff %} {% if user.is_staff and not demo %}
<li><a class='dropdown-item' href="/admin/"><span class="fas fa-user"></span> {% trans "Admin" %}</a></li> <li><a class='dropdown-item' href="/admin/"><span class="fas fa-user"></span> {% trans "Admin" %}</a></li>
{% endif %} {% endif %}
<li><a class='dropdown-item' href="{% url 'account_logout' %}"><span class="fas fa-sign-out-alt"></span> {% trans "Logout" %}</a></li> <li><a class='dropdown-item' href="{% url 'account_logout' %}"><span class="fas fa-sign-out-alt"></span> {% trans "Logout" %}</a></li>

View File

@ -0,0 +1,12 @@
{% load i18n %}
{% include "spacer.html" %}
<div class='flex'>
<h6>
{% trans "InvenTree demo mode" %}
<a href='https://inventree.readthedocs.io/en/latest/demo/'>
<span class='fas fa-info-circle'></span>
</a>
</h6>
</div>
{% include "spacer.html" %}
{% include "spacer.html" %}