diff --git a/InvenTree/InvenTree/helpers.py b/InvenTree/InvenTree/helpers.py
index 9d00697230..330bd2bb68 100644
--- a/InvenTree/InvenTree/helpers.py
+++ b/InvenTree/InvenTree/helpers.py
@@ -21,6 +21,9 @@ import InvenTree.version
from common.models import InvenTreeSetting
from .settings import MEDIA_URL, STATIC_URL
+from common.settings import currency_code_default
+
+from djmoney.money import Money
def getSetting(key, backup_value=None):
@@ -247,6 +250,22 @@ def decimal2string(d):
return s.rstrip("0").rstrip(".")
+def decimal2money(d, currency=None):
+ """
+ Format a Decimal number as Money
+
+ Args:
+ d: A python Decimal object
+ currency: Currency of the input amount, defaults to default currency in settings
+
+ Returns:
+ A Money object from the input(s)
+ """
+ if not currency:
+ currency = currency_code_default()
+ return Money(d, currency)
+
+
def WrapWithQuotes(text, quote='"'):
""" Wrap the supplied text with quotes
diff --git a/InvenTree/InvenTree/serializers.py b/InvenTree/InvenTree/serializers.py
index 50a37d8cba..772daa06ab 100644
--- a/InvenTree/InvenTree/serializers.py
+++ b/InvenTree/InvenTree/serializers.py
@@ -7,8 +7,6 @@ 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
@@ -46,16 +44,13 @@ class InvenTreeModelSerializer(serializers.ModelSerializer):
def __init__(self, instance=None, data=empty, **kwargs):
- self.instance = instance
+ # self.instance = instance
# If instance is None, we are creating a new instance
- if instance is None:
+ if instance is None and data is not empty:
- if data is empty:
- data = OrderedDict()
- else:
- # Required to side-step immutability of a QueryDict
- data = data.copy()
+ # Required to side-step immutability of a QueryDict
+ data = data.copy()
# Add missing fields which have default values
ModelClass = self.Meta.model
@@ -64,6 +59,11 @@ class InvenTreeModelSerializer(serializers.ModelSerializer):
for field_name, field in fields.fields.items():
+ """
+ Update the field IF (and ONLY IF):
+ - The field has a specified default value
+ - The field does not already have a value set
+ """
if field.has_default() and field_name not in data:
value = field.default
@@ -85,7 +85,7 @@ class InvenTreeModelSerializer(serializers.ModelSerializer):
Use the 'default' values specified by the django model definition
"""
- initials = super().get_initial()
+ initials = super().get_initial().copy()
# Are we creating a new instance?
if self.instance is None:
@@ -111,7 +111,8 @@ class InvenTreeModelSerializer(serializers.ModelSerializer):
return initials
def run_validation(self, data=empty):
- """ Perform serializer validation.
+ """
+ Perform serializer validation.
In addition to running validators on the serializer fields,
this class ensures that the underlying model is also validated.
"""
diff --git a/InvenTree/InvenTree/test_api.py b/InvenTree/InvenTree/test_api.py
index e37984e20d..791f98025b 100644
--- a/InvenTree/InvenTree/test_api.py
+++ b/InvenTree/InvenTree/test_api.py
@@ -2,6 +2,11 @@
from rest_framework import status
+from django.test import TestCase
+
+from django.contrib.auth import get_user_model
+from django.contrib.auth.models import Group
+
from django.urls import reverse
from InvenTree.api_tester import InvenTreeAPITestCase
@@ -11,6 +16,87 @@ from users.models import RuleSet
from base64 import b64encode
+class HTMLAPITests(TestCase):
+ """
+ Test that we can access the REST API endpoints via the HTML interface.
+
+ History: Discovered on 2021-06-28 a bug in InvenTreeModelSerializer,
+ which raised an AssertionError when using the HTML API interface,
+ while the regular JSON interface continued to work as expected.
+ """
+
+ def setUp(self):
+ super().setUp()
+
+ # Create a user
+ user = get_user_model()
+
+ self.user = user.objects.create_user(
+ username='username',
+ email='user@email.com',
+ password='password'
+ )
+
+ # Put the user into a group with the correct permissions
+ group = Group.objects.create(name='mygroup')
+ self.user.groups.add(group)
+
+ # Give the group *all* the permissions!
+ for rule in group.rule_sets.all():
+ rule.can_view = True
+ rule.can_change = True
+ rule.can_add = True
+ rule.can_delete = True
+
+ rule.save()
+
+ self.client.login(username='username', password='password')
+
+ def test_part_api(self):
+ url = reverse('api-part-list')
+
+ # Check JSON response
+ response = self.client.get(url, HTTP_ACCEPT='application/json')
+ self.assertEqual(response.status_code, 200)
+
+ # Check HTTP response
+ response = self.client.get(url, HTTP_ACCEPT='text/html')
+ self.assertEqual(response.status_code, 200)
+
+ def test_build_api(self):
+ url = reverse('api-build-list')
+
+ # Check JSON response
+ response = self.client.get(url, HTTP_ACCEPT='application/json')
+ self.assertEqual(response.status_code, 200)
+
+ # Check HTTP response
+ response = self.client.get(url, HTTP_ACCEPT='text/html')
+ self.assertEqual(response.status_code, 200)
+
+ def test_stock_api(self):
+ url = reverse('api-stock-list')
+
+ # Check JSON response
+ response = self.client.get(url, HTTP_ACCEPT='application/json')
+ self.assertEqual(response.status_code, 200)
+
+ # Check HTTP response
+ response = self.client.get(url, HTTP_ACCEPT='text/html')
+ self.assertEqual(response.status_code, 200)
+
+ def test_company_list(self):
+ url = reverse('api-company-list')
+
+ # Check JSON response
+ response = self.client.get(url, HTTP_ACCEPT='application/json')
+ self.assertEqual(response.status_code, 200)
+
+ # Check HTTP response
+ response = self.client.get(url, HTTP_ACCEPT='text/html')
+ self.assertEqual(response.status_code, 200)
+
+
class APITests(InvenTreeAPITestCase):
""" Tests for the InvenTree API """
diff --git a/InvenTree/InvenTree/version.py b/InvenTree/InvenTree/version.py
index 08fa5e0ae4..6afa5ebadd 100644
--- a/InvenTree/InvenTree/version.py
+++ b/InvenTree/InvenTree/version.py
@@ -8,7 +8,7 @@ import re
import common.models
-INVENTREE_SW_VERSION = "0.2.4 pre"
+INVENTREE_SW_VERSION = "0.2.5 pre"
INVENTREE_API_VERSION = 6
diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py
index 55ecd9d5ed..b69177c05b 100644
--- a/InvenTree/part/models.py
+++ b/InvenTree/part/models.py
@@ -39,7 +39,7 @@ from InvenTree import helpers
from InvenTree import validators
from InvenTree.models import InvenTreeTree, InvenTreeAttachment
from InvenTree.fields import InvenTreeURLField
-from InvenTree.helpers import decimal2string, normalize
+from InvenTree.helpers import decimal2string, normalize, decimal2money
from InvenTree.status_codes import BuildStatus, PurchaseOrderStatus, SalesOrderStatus
@@ -2451,7 +2451,7 @@ class BomItem(models.Model):
return "{n} x {child} to make {parent}".format(
parent=self.part.full_name,
child=self.sub_part.full_name,
- n=helpers.decimal2string(self.quantity))
+ n=decimal2string(self.quantity))
def available_stock(self):
"""
@@ -2535,12 +2535,12 @@ class BomItem(models.Model):
return required
@property
- def price_range(self):
+ def price_range(self, internal=False):
""" Return the price-range for this BOM item. """
# get internal price setting
use_internal = common.models.InvenTreeSetting.get_setting('PART_BOM_USE_INTERNAL_PRICE', False)
- prange = self.sub_part.get_price_range(self.quantity, intenal=use_internal)
+ prange = self.sub_part.get_price_range(self.quantity, internal=use_internal and internal)
if prange is None:
return prange
@@ -2548,11 +2548,11 @@ class BomItem(models.Model):
pmin, pmax = prange
if pmin == pmax:
- return decimal2string(pmin)
+ return decimal2money(pmin)
# Convert to better string representation
- pmin = decimal2string(pmin)
- pmax = decimal2string(pmax)
+ pmin = decimal2money(pmin)
+ pmax = decimal2money(pmax)
return "{pmin} to {pmax}".format(pmin=pmin, pmax=pmax)
diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py
index 7200309afa..ff178c5941 100644
--- a/InvenTree/part/serializers.py
+++ b/InvenTree/part/serializers.py
@@ -381,7 +381,7 @@ class PartStarSerializer(InvenTreeModelSerializer):
class BomItemSerializer(InvenTreeModelSerializer):
""" Serializer for BomItem object """
- # price_range = serializers.CharField(read_only=True)
+ price_range = serializers.CharField(read_only=True)
quantity = serializers.FloatField()
@@ -496,7 +496,7 @@ class BomItemSerializer(InvenTreeModelSerializer):
'reference',
'sub_part',
'sub_part_detail',
- # 'price_range',
+ 'price_range',
'validated',
]
diff --git a/InvenTree/templates/js/bom.js b/InvenTree/templates/js/bom.js
index 7328bcb331..665379d8d5 100644
--- a/InvenTree/templates/js/bom.js
+++ b/InvenTree/templates/js/bom.js
@@ -259,26 +259,19 @@ function loadBomTable(table, options) {
sortable: true,
});
- /*
-
- // TODO - Re-introduce the pricing column at a later stage,
- // once the pricing has been "fixed"
- // O.W. 2020-11-24
-
cols.push(
{
field: 'price_range',
- title: '{% trans "Price" %}',
+ title: '{% trans "Buy Price" %}',
sortable: true,
formatter: function(value, row, index, field) {
if (value) {
return value;
} else {
- return "{% trans "No pricing available" %}";
+ return "{% trans 'No pricing available' %}";
}
}
});
- */
cols.push({
field: 'optional',
diff --git a/InvenTree/templates/js/build.js b/InvenTree/templates/js/build.js
index 3e5e438add..7b72149ec9 100644
--- a/InvenTree/templates/js/build.js
+++ b/InvenTree/templates/js/build.js
@@ -231,6 +231,7 @@ function loadBuildOrderAllocationTable(table, options={}) {
{
field: 'quantity',
title: '{% trans "Quantity" %}',
+ sortable: true,
}
]
});
diff --git a/InvenTree/templates/js/order.js b/InvenTree/templates/js/order.js
index 649357b083..0af54fa43c 100644
--- a/InvenTree/templates/js/order.js
+++ b/InvenTree/templates/js/order.js
@@ -391,6 +391,7 @@ function loadSalesOrderAllocationTable(table, options={}) {
{
field: 'quantity',
title: '{% trans "Quantity" %}',
+ sortable: true,
}
]
});