Merge remote-tracking branch 'inventree/master' into drf-api-forms

This commit is contained in:
Oliver 2021-06-29 09:57:03 +10:00
commit 798bc17311
9 changed files with 131 additions and 30 deletions

View File

@ -21,6 +21,9 @@ import InvenTree.version
from common.models import InvenTreeSetting from common.models import InvenTreeSetting
from .settings import MEDIA_URL, STATIC_URL 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): def getSetting(key, backup_value=None):
@ -247,6 +250,22 @@ def decimal2string(d):
return s.rstrip("0").rstrip(".") 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='"'): def WrapWithQuotes(text, quote='"'):
""" Wrap the supplied text with quotes """ Wrap the supplied text with quotes

View File

@ -7,8 +7,6 @@ from __future__ import unicode_literals
import os import os
from collections import OrderedDict
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.exceptions import ValidationError as DjangoValidationError from django.core.exceptions import ValidationError as DjangoValidationError
@ -46,16 +44,13 @@ class InvenTreeModelSerializer(serializers.ModelSerializer):
def __init__(self, instance=None, data=empty, **kwargs): 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, we are creating a new instance
if instance is None: if instance is None and data is not empty:
if data is empty: # Required to side-step immutability of a QueryDict
data = OrderedDict() data = data.copy()
else:
# Required to side-step immutability of a QueryDict
data = data.copy()
# Add missing fields which have default values # Add missing fields which have default values
ModelClass = self.Meta.model ModelClass = self.Meta.model
@ -64,6 +59,11 @@ class InvenTreeModelSerializer(serializers.ModelSerializer):
for field_name, field in fields.fields.items(): 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: if field.has_default() and field_name not in data:
value = field.default value = field.default
@ -85,7 +85,7 @@ class InvenTreeModelSerializer(serializers.ModelSerializer):
Use the 'default' values specified by the django model definition 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? # Are we creating a new instance?
if self.instance is None: if self.instance is None:
@ -111,7 +111,8 @@ class InvenTreeModelSerializer(serializers.ModelSerializer):
return initials return initials
def run_validation(self, data=empty): def run_validation(self, data=empty):
""" Perform serializer validation. """
Perform serializer validation.
In addition to running validators on the serializer fields, In addition to running validators on the serializer fields,
this class ensures that the underlying model is also validated. this class ensures that the underlying model is also validated.
""" """

View File

@ -2,6 +2,11 @@
from rest_framework import status 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 django.urls import reverse
from InvenTree.api_tester import InvenTreeAPITestCase from InvenTree.api_tester import InvenTreeAPITestCase
@ -11,6 +16,87 @@ from users.models import RuleSet
from base64 import b64encode 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): class APITests(InvenTreeAPITestCase):
""" Tests for the InvenTree API """ """ Tests for the InvenTree API """

View File

@ -8,7 +8,7 @@ import re
import common.models import common.models
INVENTREE_SW_VERSION = "0.2.4 pre" INVENTREE_SW_VERSION = "0.2.5 pre"
INVENTREE_API_VERSION = 6 INVENTREE_API_VERSION = 6

View File

@ -39,7 +39,7 @@ from InvenTree import helpers
from InvenTree import validators from InvenTree import validators
from InvenTree.models import InvenTreeTree, InvenTreeAttachment from InvenTree.models import InvenTreeTree, InvenTreeAttachment
from InvenTree.fields import InvenTreeURLField 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 from InvenTree.status_codes import BuildStatus, PurchaseOrderStatus, SalesOrderStatus
@ -2451,7 +2451,7 @@ class BomItem(models.Model):
return "{n} x {child} to make {parent}".format( return "{n} x {child} to make {parent}".format(
parent=self.part.full_name, parent=self.part.full_name,
child=self.sub_part.full_name, child=self.sub_part.full_name,
n=helpers.decimal2string(self.quantity)) n=decimal2string(self.quantity))
def available_stock(self): def available_stock(self):
""" """
@ -2535,12 +2535,12 @@ class BomItem(models.Model):
return required return required
@property @property
def price_range(self): def price_range(self, internal=False):
""" Return the price-range for this BOM item. """ """ Return the price-range for this BOM item. """
# get internal price setting # get internal price setting
use_internal = common.models.InvenTreeSetting.get_setting('PART_BOM_USE_INTERNAL_PRICE', False) 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: if prange is None:
return prange return prange
@ -2548,11 +2548,11 @@ class BomItem(models.Model):
pmin, pmax = prange pmin, pmax = prange
if pmin == pmax: if pmin == pmax:
return decimal2string(pmin) return decimal2money(pmin)
# Convert to better string representation # Convert to better string representation
pmin = decimal2string(pmin) pmin = decimal2money(pmin)
pmax = decimal2string(pmax) pmax = decimal2money(pmax)
return "{pmin} to {pmax}".format(pmin=pmin, pmax=pmax) return "{pmin} to {pmax}".format(pmin=pmin, pmax=pmax)

View File

@ -381,7 +381,7 @@ class PartStarSerializer(InvenTreeModelSerializer):
class BomItemSerializer(InvenTreeModelSerializer): class BomItemSerializer(InvenTreeModelSerializer):
""" Serializer for BomItem object """ """ Serializer for BomItem object """
# price_range = serializers.CharField(read_only=True) price_range = serializers.CharField(read_only=True)
quantity = serializers.FloatField() quantity = serializers.FloatField()
@ -496,7 +496,7 @@ class BomItemSerializer(InvenTreeModelSerializer):
'reference', 'reference',
'sub_part', 'sub_part',
'sub_part_detail', 'sub_part_detail',
# 'price_range', 'price_range',
'validated', 'validated',
] ]

View File

@ -259,26 +259,19 @@ function loadBomTable(table, options) {
sortable: true, 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( cols.push(
{ {
field: 'price_range', field: 'price_range',
title: '{% trans "Price" %}', title: '{% trans "Buy Price" %}',
sortable: true, sortable: true,
formatter: function(value, row, index, field) { formatter: function(value, row, index, field) {
if (value) { if (value) {
return value; return value;
} else { } else {
return "<span class='warning-msg'>{% trans "No pricing available" %}</span>"; return "<span class='warning-msg'>{% trans 'No pricing available' %}</span>";
} }
} }
}); });
*/
cols.push({ cols.push({
field: 'optional', field: 'optional',

View File

@ -231,6 +231,7 @@ function loadBuildOrderAllocationTable(table, options={}) {
{ {
field: 'quantity', field: 'quantity',
title: '{% trans "Quantity" %}', title: '{% trans "Quantity" %}',
sortable: true,
} }
] ]
}); });

View File

@ -391,6 +391,7 @@ function loadSalesOrderAllocationTable(table, options={}) {
{ {
field: 'quantity', field: 'quantity',
title: '{% trans "Quantity" %}', title: '{% trans "Quantity" %}',
sortable: true,
} }
] ]
}); });