mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge remote-tracking branch 'inventree/master' into drf-api-forms
This commit is contained in:
commit
798bc17311
@ -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
|
||||||
|
|
||||||
|
@ -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,14 +44,11 @@ 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:
|
|
||||||
data = OrderedDict()
|
|
||||||
else:
|
|
||||||
# Required to side-step immutability of a QueryDict
|
# Required to side-step immutability of a QueryDict
|
||||||
data = data.copy()
|
data = data.copy()
|
||||||
|
|
||||||
@ -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.
|
||||||
"""
|
"""
|
||||||
|
@ -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 """
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -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',
|
||||||
|
@ -231,6 +231,7 @@ function loadBuildOrderAllocationTable(table, options={}) {
|
|||||||
{
|
{
|
||||||
field: 'quantity',
|
field: 'quantity',
|
||||||
title: '{% trans "Quantity" %}',
|
title: '{% trans "Quantity" %}',
|
||||||
|
sortable: true,
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
@ -391,6 +391,7 @@ function loadSalesOrderAllocationTable(table, options={}) {
|
|||||||
{
|
{
|
||||||
field: 'quantity',
|
field: 'quantity',
|
||||||
title: '{% trans "Quantity" %}',
|
title: '{% trans "Quantity" %}',
|
||||||
|
sortable: true,
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user