From 8b515571cabd36cdabf756674ef19531107da4cf Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 21 Jun 2021 23:33:27 +0200 Subject: [PATCH 01/11] I think a fix for #1663 Closes #1663 --- InvenTree/templates/js/build.js | 1 + InvenTree/templates/js/order.js | 1 + 2 files changed, 2 insertions(+) diff --git a/InvenTree/templates/js/build.js b/InvenTree/templates/js/build.js index e8af981817..31917aab26 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, } ] }); From c8defae5752e182984dd1a0af2f95ec8998929ec Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 22 Jun 2021 00:03:54 +0200 Subject: [PATCH 02/11] fixing allocation sorting --- InvenTree/templates/js/build.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/InvenTree/templates/js/build.js b/InvenTree/templates/js/build.js index 31917aab26..398796773c 100644 --- a/InvenTree/templates/js/build.js +++ b/InvenTree/templates/js/build.js @@ -693,9 +693,6 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { var qA = rowA.quantity; var qB = rowB.quantity; - qA *= output.quantity; - qB *= output.quantity; - // Handle the case where both numerators are zero if ((aA == 0) && (aB == 0)) { return (qA > qB) ? 1 : -1; From 67128c308bb8a5985abd233f20f94fc13e4e6401 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 27 Jun 2021 12:26:02 +0200 Subject: [PATCH 03/11] fixing typo --- InvenTree/part/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 2b97fde596..7ed4c99057 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -2503,7 +2503,7 @@ class BomItem(models.Model): # 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) if prange is None: return prange From 786e994e19923a742e40922939dbd6cc91a157e1 Mon Sep 17 00:00:00 2001 From: Oliver Date: Sun, 27 Jun 2021 20:58:24 +1000 Subject: [PATCH 04/11] Update version.py 0.2.4 --- InvenTree/InvenTree/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/InvenTree/version.py b/InvenTree/InvenTree/version.py index 08fa5e0ae4..738274b1cd 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.4" INVENTREE_API_VERSION = 6 From 604c136b003eed3d82c488eecbeb85cbf0fe2ebf Mon Sep 17 00:00:00 2001 From: Oliver Date: Sun, 27 Jun 2021 21:00:01 +1000 Subject: [PATCH 05/11] Update version.py --- InvenTree/InvenTree/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/InvenTree/version.py b/InvenTree/InvenTree/version.py index 738274b1cd..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" +INVENTREE_SW_VERSION = "0.2.5 pre" INVENTREE_API_VERSION = 6 From a3ec24fbcc4a837a1e795c96dd3510e5cc2ac3d5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 27 Jun 2021 13:48:08 +0200 Subject: [PATCH 06/11] Reenabling prices for BOM items Closes #1721 --- InvenTree/part/serializers.py | 4 ++-- InvenTree/templates/js/bom.js | 11 ++--------- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index 6c47f1310f..2da2d05d3b 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -377,7 +377,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() @@ -492,7 +492,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', From 4f726931a63a3a80b52c171483e12e6bf952f258 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 27 Jun 2021 14:18:37 +0200 Subject: [PATCH 07/11] adds in money-conversion helper --- InvenTree/InvenTree/helpers.py | 19 +++++++++++++++++++ InvenTree/part/models.py | 14 +++++++------- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/InvenTree/InvenTree/helpers.py b/InvenTree/InvenTree/helpers.py index 9d00697230..cc1e60eb8e 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/part/models.py b/InvenTree/part/models.py index 7ed4c99057..f76ff4c104 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 @@ -2414,7 +2414,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): """ @@ -2498,12 +2498,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, internal=use_internal) + prange = self.sub_part.get_price_range(self.quantity, internal=use_internal and internal) if prange is None: return prange @@ -2511,11 +2511,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) From e4a9d56ba0d7aeebbd2f20fa52035b0c7b086ad3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 27 Jun 2021 14:26:51 +0200 Subject: [PATCH 08/11] style fixes --- InvenTree/InvenTree/helpers.py | 2 +- InvenTree/part/models.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/InvenTree/InvenTree/helpers.py b/InvenTree/InvenTree/helpers.py index cc1e60eb8e..330bd2bb68 100644 --- a/InvenTree/InvenTree/helpers.py +++ b/InvenTree/InvenTree/helpers.py @@ -250,7 +250,7 @@ def decimal2string(d): return s.rstrip("0").rstrip(".") -def decimal2money(d, currency = None): +def decimal2money(d, currency=None): """ Format a Decimal number as Money diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index f76ff4c104..9f4da436df 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -2498,7 +2498,7 @@ class BomItem(models.Model): return required @property - def price_range(self, internal = False): + def price_range(self, internal=False): """ Return the price-range for this BOM item. """ # get internal price setting From 3c1f0637dcddfa6f6b4782e64e81883f113c0f30 Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 28 Jun 2021 20:42:37 +1000 Subject: [PATCH 09/11] Adds unit tests for HTML API endpoints --- InvenTree/InvenTree/test_api.py | 87 +++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/InvenTree/InvenTree/test_api.py b/InvenTree/InvenTree/test_api.py index f196006df9..bc1157cf45 100644 --- a/InvenTree/InvenTree/test_api.py +++ b/InvenTree/InvenTree/test_api.py @@ -1,7 +1,13 @@ """ Low level tests for the InvenTree API """ +from django.http import response 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 +17,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 """ From 4dbd770f2d4f3bf65acef7d43cd048ef2956c482 Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 28 Jun 2021 21:08:50 +1000 Subject: [PATCH 10/11] Fixed (I think?) --- InvenTree/InvenTree/serializers.py | 18 +++++++----------- InvenTree/InvenTree/test_api.py | 1 - 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/InvenTree/InvenTree/serializers.py b/InvenTree/InvenTree/serializers.py index 50a37d8cba..16db21ca37 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 @@ -85,7 +80,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 +106,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 bc1157cf45..18f9319624 100644 --- a/InvenTree/InvenTree/test_api.py +++ b/InvenTree/InvenTree/test_api.py @@ -1,6 +1,5 @@ """ Low level tests for the InvenTree API """ -from django.http import response from rest_framework import status from django.test import TestCase From f0f6c7d186f0062f4a3b3d80062dbdf00acde83c Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 28 Jun 2021 21:09:48 +1000 Subject: [PATCH 11/11] Add a comment --- InvenTree/InvenTree/serializers.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/InvenTree/InvenTree/serializers.py b/InvenTree/InvenTree/serializers.py index 16db21ca37..772daa06ab 100644 --- a/InvenTree/InvenTree/serializers.py +++ b/InvenTree/InvenTree/serializers.py @@ -59,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