From c6fd2281d689309007b06c46560e6f446ee931bd Mon Sep 17 00:00:00 2001 From: rgilham Date: Thu, 24 Jun 2021 02:13:55 +0200 Subject: [PATCH 01/62] Allow BOM pricing to be valid when using internal pricing --- InvenTree/part/models.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 8ddf049216..53f9a51595 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -1465,16 +1465,16 @@ class Part(MPTTModel): return self.supplier_parts.count() @property - def has_pricing_info(self): + def has_pricing_info(self,internal=False): """ Return true if there is pricing information for this part """ - return self.get_price_range() is not None + return self.get_price_range(internal=internal) is not None @property def has_complete_bom_pricing(self): """ Return true if there is pricing information for each item in the BOM. """ - + use_internal = common.models.get_setting('PART_BOM_USE_INTERNAL_PRICE', False) for item in self.get_bom_items().all().select_related('sub_part'): - if not item.sub_part.has_pricing_info: + if not item.sub_part.has_pricing_info(use_internal): return False return True From c71fbf7893d83d3c9efdf990bcfdc537577c353f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 18 Jul 2021 20:57:15 +0200 Subject: [PATCH 02/62] added autocomplete to jquery ui --- .../images/ui-icons_444444_256x240.png | Bin 7090 -> 7090 bytes .../images/ui-icons_555555_256x240.png | Bin 7074 -> 7074 bytes .../images/ui-icons_777620_256x240.png | Bin 4618 -> 4618 bytes .../images/ui-icons_777777_256x240.png | Bin 7111 -> 7111 bytes .../images/ui-icons_cc0000_256x240.png | Bin 4618 -> 4618 bytes .../images/ui-icons_ffffff_256x240.png | Bin 6487 -> 6487 bytes .../static/script/jquery-ui/index.html | 50 + .../static/script/jquery-ui/jquery-ui.css | 64 +- .../static/script/jquery-ui/jquery-ui.js | 1915 ++++++++++++++++- .../static/script/jquery-ui/jquery-ui.min.css | 6 +- .../static/script/jquery-ui/jquery-ui.min.js | 6 +- .../script/jquery-ui/jquery-ui.structure.css | 60 + .../jquery-ui/jquery-ui.structure.min.css | 4 +- .../script/jquery-ui/jquery-ui.theme.min.css | 2 +- 14 files changed, 2094 insertions(+), 13 deletions(-) diff --git a/InvenTree/InvenTree/static/script/jquery-ui/images/ui-icons_444444_256x240.png b/InvenTree/InvenTree/static/script/jquery-ui/images/ui-icons_444444_256x240.png index ed8d9135cae2bd9f40fa9349a213fe38add638a7..84220b0da13f9ef302168bf4de13d7dd806cd48e 100644 GIT binary patch delta 48 zcmdmFzR7$-DJQ#-kg2JruaMlvy4_OZX1az(Ax4H)My6J#=Gq1ZRt5&0Q+Bvb4vvl_t8|fOFhZq@H8Jby{7-$<9SQ!}Xc^#xWIY2rI E0Ax)LV*mgE diff --git a/InvenTree/InvenTree/static/script/jquery-ui/images/ui-icons_555555_256x240.png b/InvenTree/InvenTree/static/script/jquery-ui/images/ui-icons_555555_256x240.png index 67648e3e9e973973d6e89bab73c5a0893972148a..4ac750dd3122d0c1c96339a03e14e7740bddbb1b 100644 GIT binary patch delta 48 zcmZ2vzQ}w+DJQ#-kg3@Q7w07#>sCvNo9P-Fg%}xH8JSv{nrj;vSQ!{}PTAo$*+Du9 E0Bd9pkpKVy delta 48 zcmZ2vzQ}w+DJPRSCzo;g-gQSd)~%KjH_|mU4>2;ZGBmR?G0-+Jure^%^Eya%vV(LI E0DgH7AOHXW diff --git a/InvenTree/InvenTree/static/script/jquery-ui/images/ui-icons_777620_256x240.png b/InvenTree/InvenTree/static/script/jquery-ui/images/ui-icons_777620_256x240.png index f2cc3f0e64e5efa86cf4f80291e063a5d19a4e79..c5f9c9af678315af0ba9b2a536a12599636564d8 100644 GIT binary patch delta 49 zcmeBD=~CJ7ospAWNXXPw(^p7t6VpU~aWh>*qYxuQDn=!%8|fOFhZq@H8Jby{7-$<9SQ!}Xc^#xWIaN9d E0B*Prw*UYD diff --git a/InvenTree/InvenTree/static/script/jquery-ui/images/ui-icons_cc0000_256x240.png b/InvenTree/InvenTree/static/script/jquery-ui/images/ui-icons_cc0000_256x240.png index a24cde337d5e5c7394cd38e1633d1f6d93793dae..fdaa36b2198846cd6039ea09e4b2a7d4d1512a76 100644 GIT binary patch delta 49 zcmeBD=~CJ7ospAWNXXPw(^p7t6VpU~aWh>*qYxuQDKoxDOY F2>@9$4lV!y diff --git a/InvenTree/InvenTree/static/script/jquery-ui/images/ui-icons_ffffff_256x240.png b/InvenTree/InvenTree/static/script/jquery-ui/images/ui-icons_ffffff_256x240.png index 67662457402c071a5b2aace87eb7e41d520c5aca..08e2f14488ee435db3fd52257f45041014e0bbec 100644 GIT binary patch delta 48 zcmca^blqq|DJQ#-kg2JruaMlvx&#SvGhIWY5Fk=fyjdTsoLyQcp49%=e473dltPBkHybe;G{8b_e E0B0Ny=l}o! diff --git a/InvenTree/InvenTree/static/script/jquery-ui/index.html b/InvenTree/InvenTree/static/script/jquery-ui/index.html index b5ac7218c6..6bc6bf7980 100644 --- a/InvenTree/InvenTree/static/script/jquery-ui/index.html +++ b/InvenTree/InvenTree/static/script/jquery-ui/index.html @@ -59,6 +59,11 @@

YOUR COMPONENTS:

+ +

Autocomplete

+
+ +
@@ -248,6 +253,23 @@ + +

Menu

+ @@ -270,6 +292,33 @@ diff --git a/InvenTree/templates/search_form.html b/InvenTree/templates/search_form.html index 51a045a257..037942cb3e 100644 --- a/InvenTree/templates/search_form.html +++ b/InvenTree/templates/search_form.html @@ -3,7 +3,7 @@ -{% endblock form_buttons_bottom %} + + {{ wizard.management_form }} + {% block form_content %} + {% crispy wizard.form %} + {% endblock form_content %} +
-{% else %} - -{% endif %} -{% endblock details %} + {% block form_buttons_bottom %} + {% if wizard.steps.prev %} + + {% endif %} + + + {% endblock form_buttons_bottom %} + + {% else %} + + {% endif %} + {% endblock details %} + + +{% endblock %} {% block js_ready %} {{ block.super }} diff --git a/InvenTree/order/templates/order/po_navbar.html b/InvenTree/order/templates/order/po_navbar.html index 9f7967810d..bddcce4ba3 100644 --- a/InvenTree/order/templates/order/po_navbar.html +++ b/InvenTree/order/templates/order/po_navbar.html @@ -15,14 +15,6 @@ {% trans "Order Items" %} - {% if order.status == PurchaseOrderStatus.PENDING and roles.purchase_order.change %} -
  • - - - {% trans "Upload File" %} - -
  • - {% endif %}
  • diff --git a/InvenTree/order/templates/order/purchase_order_detail.html b/InvenTree/order/templates/order/purchase_order_detail.html index 193fd75daa..b05bfa7cc2 100644 --- a/InvenTree/order/templates/order/purchase_order_detail.html +++ b/InvenTree/order/templates/order/purchase_order_detail.html @@ -22,6 +22,9 @@ + + {% trans "Upload File" %} + {% endif %} diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py index be1107f17b..a109b036e0 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -393,16 +393,7 @@ class PurchaseOrderUpload(FileManagementFormView): p_val = row['data'][p_idx]['cell'] if p_val: - # Delete commas - p_val = p_val.replace(',', '') - - try: - # Attempt to extract a valid decimal value from the field - purchase_price = Decimal(p_val) - # Store the 'purchase_price' value - row['purchase_price'] = purchase_price - except (ValueError, InvalidOperation): - pass + row['purchase_price'] = p_val # Check if there is a column corresponding to "reference" if r_idx >= 0: From 3ab058e84b460dc98a4aa4019595b7d79ea28cee Mon Sep 17 00:00:00 2001 From: eeintech Date: Mon, 19 Jul 2021 14:49:55 -0400 Subject: [PATCH 10/62] Fixed default currency selection --- InvenTree/InvenTree/fields.py | 3 ++- InvenTree/InvenTree/helpers.py | 2 +- InvenTree/common/models.py | 5 ++--- InvenTree/common/settings.py | 8 ++++---- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/InvenTree/InvenTree/fields.py b/InvenTree/InvenTree/fields.py index 462d2b0e0e..65664eee6b 100644 --- a/InvenTree/InvenTree/fields.py +++ b/InvenTree/InvenTree/fields.py @@ -44,7 +44,7 @@ def money_kwargs(): """ returns the database settings for MoneyFields """ kwargs = {} kwargs['currency_choices'] = common.settings.currency_code_mappings() - kwargs['default_currency'] = common.settings.currency_code_default + kwargs['default_currency'] = common.settings.currency_code_default() return kwargs @@ -86,6 +86,7 @@ class InvenTreeMoneyField(MoneyField): def __init__(self, *args, **kwargs): # override initial values with the real info from database kwargs.update(money_kwargs()) + print(kwargs) super().__init__(*args, **kwargs) diff --git a/InvenTree/InvenTree/helpers.py b/InvenTree/InvenTree/helpers.py index 0b091efd02..374e4e5a59 100644 --- a/InvenTree/InvenTree/helpers.py +++ b/InvenTree/InvenTree/helpers.py @@ -8,7 +8,7 @@ import json import os.path from PIL import Image -from decimal import Decimal, InvalidOperation +from decimal import Decimal from wsgiref.util import FileWrapper from django.http import StreamingHttpResponse diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index 3d18d56880..dcdeea5292 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -18,8 +18,6 @@ from djmoney.settings import CURRENCY_CHOICES from djmoney.contrib.exchange.models import convert_money from djmoney.contrib.exchange.exceptions import MissingRate -import common.settings - from django.utils.translation import ugettext_lazy as _ from django.core.validators import MinValueValidator, URLValidator from django.core.exceptions import ValidationError @@ -781,6 +779,7 @@ def get_price(instance, quantity, moq=True, multiples=True, currency=None, break - If MOQ (minimum order quantity) is required, bump quantity - If order multiples are to be observed, then we need to calculate based on that, too """ + from common.settings import currency_code_default if hasattr(instance, break_name): price_breaks = getattr(instance, break_name).all() @@ -804,7 +803,7 @@ def get_price(instance, quantity, moq=True, multiples=True, currency=None, break if currency is None: # Default currency selection - currency = common.settings.currency_code_default() + currency = currency_code_default() pb_min = None for pb in price_breaks: diff --git a/InvenTree/common/settings.py b/InvenTree/common/settings.py index b34cf0b785..67dd751154 100644 --- a/InvenTree/common/settings.py +++ b/InvenTree/common/settings.py @@ -8,15 +8,14 @@ from __future__ import unicode_literals from moneyed import CURRENCIES from django.conf import settings -import common.models - def currency_code_default(): """ Returns the default currency code (or USD if not specified) """ + from common.models import InvenTreeSetting - code = common.models.InvenTreeSetting.get_setting('INVENTREE_DEFAULT_CURRENCY') + code = InvenTreeSetting.get_setting('INVENTREE_DEFAULT_CURRENCY') if code not in CURRENCIES: code = 'USD' @@ -42,5 +41,6 @@ def stock_expiry_enabled(): """ Returns True if the stock expiry feature is enabled """ + from common.models import InvenTreeSetting - return common.models.InvenTreeSetting.get_setting('STOCK_ENABLE_EXPIRY') + return InvenTreeSetting.get_setting('STOCK_ENABLE_EXPIRY') From c1db4c7b3daf16a7c866c0a8af30ea01f323aa49 Mon Sep 17 00:00:00 2001 From: eeintech Date: Mon, 19 Jul 2021 15:14:08 -0400 Subject: [PATCH 11/62] Try to catch encoding error, fixed CI --- InvenTree/InvenTree/fields.py | 7 ++++--- InvenTree/common/files.py | 25 ++++++++++++++----------- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/InvenTree/InvenTree/fields.py b/InvenTree/InvenTree/fields.py index 65664eee6b..81c4f7ba0b 100644 --- a/InvenTree/InvenTree/fields.py +++ b/InvenTree/InvenTree/fields.py @@ -20,7 +20,6 @@ from djmoney.forms.fields import MoneyField from djmoney.models.validators import MinMoneyValidator import InvenTree.helpers -import common.settings class InvenTreeURLFormField(FormURLField): @@ -42,9 +41,11 @@ class InvenTreeURLField(models.URLField): def money_kwargs(): """ returns the database settings for MoneyFields """ + from common.settings import currency_code_mappings, currency_code_default + kwargs = {} - kwargs['currency_choices'] = common.settings.currency_code_mappings() - kwargs['default_currency'] = common.settings.currency_code_default() + kwargs['currency_choices'] = currency_code_mappings() + kwargs['default_currency'] = currency_code_default() return kwargs diff --git a/InvenTree/common/files.py b/InvenTree/common/files.py index 45b6c80050..39c97dca6c 100644 --- a/InvenTree/common/files.py +++ b/InvenTree/common/files.py @@ -53,17 +53,20 @@ class FileManager: ext = os.path.splitext(file.name)[-1].lower().replace('.', '') - if ext in ['csv', 'tsv', ]: - # These file formats need string decoding - raw_data = file.read().decode('utf-8') - # Reset stream position to beginning of file - file.seek(0) - elif ext in ['xls', 'xlsx', 'json', 'yaml', ]: - raw_data = file.read() - # Reset stream position to beginning of file - file.seek(0) - else: - raise ValidationError(_(f'Unsupported file format: {ext.upper()}')) + try: + if ext in ['csv', 'tsv', ]: + # These file formats need string decoding + raw_data = file.read().decode('utf-8') + # Reset stream position to beginning of file + file.seek(0) + elif ext in ['xls', 'xlsx', 'json', 'yaml', ]: + raw_data = file.read() + # Reset stream position to beginning of file + file.seek(0) + else: + raise ValidationError(_(f'Unsupported file format: {ext.upper()}')) + except UnicodeEncodeError: + raise ValidationError(_('Error reading file (invalid encoding)')) try: cleaned_data = tablib.Dataset().load(raw_data, format=ext) From 9acd57f8e0ba5ea1996ec74dc6619e2ce9809175 Mon Sep 17 00:00:00 2001 From: eeintech Date: Mon, 19 Jul 2021 15:29:04 -0400 Subject: [PATCH 12/62] CI was not completely fixed --- InvenTree/InvenTree/fields.py | 1 - InvenTree/common/settings.py | 6 +++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/InvenTree/InvenTree/fields.py b/InvenTree/InvenTree/fields.py index 81c4f7ba0b..71b31c55a8 100644 --- a/InvenTree/InvenTree/fields.py +++ b/InvenTree/InvenTree/fields.py @@ -87,7 +87,6 @@ class InvenTreeMoneyField(MoneyField): def __init__(self, *args, **kwargs): # override initial values with the real info from database kwargs.update(money_kwargs()) - print(kwargs) super().__init__(*args, **kwargs) diff --git a/InvenTree/common/settings.py b/InvenTree/common/settings.py index 67dd751154..592277657f 100644 --- a/InvenTree/common/settings.py +++ b/InvenTree/common/settings.py @@ -15,7 +15,11 @@ def currency_code_default(): """ from common.models import InvenTreeSetting - code = InvenTreeSetting.get_setting('INVENTREE_DEFAULT_CURRENCY') + try: + code = InvenTreeSetting.get_setting('INVENTREE_DEFAULT_CURRENCY') + except django.db.utils.ProgrammingError: + # database is not initialized yet + code = '' if code not in CURRENCIES: code = 'USD' From 8d2e9103232fbe2c29c16dc0376f27e1ca001e42 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 19 Jul 2021 21:50:06 +0200 Subject: [PATCH 13/62] style fix --- 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 53f9a51595..c38f39738a 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -1465,7 +1465,7 @@ class Part(MPTTModel): return self.supplier_parts.count() @property - def has_pricing_info(self,internal=False): + def has_pricing_info(self, internal=False): """ Return true if there is pricing information for this part """ return self.get_price_range(internal=internal) is not None From 53f2aa107a6ec44596c96ad156eedfa3323b2146 Mon Sep 17 00:00:00 2001 From: eeintech Date: Mon, 19 Jul 2021 15:51:04 -0400 Subject: [PATCH 14/62] Umm watch out for the true fix! --- InvenTree/common/settings.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/InvenTree/common/settings.py b/InvenTree/common/settings.py index 592277657f..80f80b886f 100644 --- a/InvenTree/common/settings.py +++ b/InvenTree/common/settings.py @@ -13,11 +13,12 @@ def currency_code_default(): """ Returns the default currency code (or USD if not specified) """ + from django.db.utils import ProgrammingError from common.models import InvenTreeSetting try: code = InvenTreeSetting.get_setting('INVENTREE_DEFAULT_CURRENCY') - except django.db.utils.ProgrammingError: + except ProgrammingError: # database is not initialized yet code = '' From 456710c5ce06ea5f5512b4732449c4a8d6a1ff64 Mon Sep 17 00:00:00 2001 From: eeintech Date: Mon, 19 Jul 2021 15:57:51 -0400 Subject: [PATCH 15/62] clean_decimal should also check if the string can be converted to Decimal type --- InvenTree/InvenTree/helpers.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/InvenTree/InvenTree/helpers.py b/InvenTree/InvenTree/helpers.py index 374e4e5a59..628fd2e646 100644 --- a/InvenTree/InvenTree/helpers.py +++ b/InvenTree/InvenTree/helpers.py @@ -8,7 +8,7 @@ import json import os.path from PIL import Image -from decimal import Decimal +from decimal import Decimal, InvalidOperation from wsgiref.util import FileWrapper from django.http import StreamingHttpResponse @@ -655,6 +655,10 @@ def clean_decimal(number): number = number.replace(',', '') # Convert to Decimal type - clean_number = Decimal(number) + try: + clean_number = Decimal(number) + except InvalidOperation: + # Number cannot be converted to Decimal (eg. a string containing letters) + return Decimal(0) return clean_number.quantize(Decimal(1)) if clean_number == clean_number.to_integral() else clean_number.normalize() From ec53099872c75ddb82b7f81079b5860f27da5eab Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 20 Jul 2021 01:34:35 +0200 Subject: [PATCH 16/62] abstracting Settings model --- InvenTree/common/models.py | 764 +++++++++++++++++++------------------ 1 file changed, 389 insertions(+), 375 deletions(-) diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index 3d18d56880..02d752aa8c 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -11,6 +11,7 @@ import decimal import math from django.db import models, transaction +from django.contrib.auth.models import User from django.db.utils import IntegrityError, OperationalError from django.conf import settings @@ -28,7 +29,394 @@ import InvenTree.helpers import InvenTree.fields -class InvenTreeSetting(models.Model): +class BaseInvenTreeSetting(models.Model): + """ + An base InvenTreeSetting object is a key:value pair used for storing + single values (e.g. one-off settings values). + """ + + + GLOBAL_SETTINGS = {} + + class Meta: + abstract = True + + @classmethod + def get_setting_name(cls, key): + """ + Return the name of a particular setting. + + If it does not exist, return an empty string. + """ + + key = str(key).strip().upper() + + if key in cls.GLOBAL_SETTINGS: + setting = cls.GLOBAL_SETTINGS[key] + return setting.get('name', '') + else: + return '' + + @classmethod + def get_setting_description(cls, key): + """ + Return the description for a particular setting. + + If it does not exist, return an empty string. + """ + + key = str(key).strip().upper() + + if key in cls.GLOBAL_SETTINGS: + setting = cls.GLOBAL_SETTINGS[key] + return setting.get('description', '') + else: + return '' + + @classmethod + def get_setting_units(cls, key): + """ + Return the units for a particular setting. + + If it does not exist, return an empty string. + """ + + key = str(key).strip().upper() + + if key in cls.GLOBAL_SETTINGS: + setting = cls.GLOBAL_SETTINGS[key] + return setting.get('units', '') + else: + return '' + + @classmethod + def get_setting_validator(cls, key): + """ + Return the validator for a particular setting. + + If it does not exist, return None + """ + + key = str(key).strip().upper() + + if key in cls.GLOBAL_SETTINGS: + setting = cls.GLOBAL_SETTINGS[key] + return setting.get('validator', None) + else: + return None + + @classmethod + def get_setting_default(cls, key): + """ + Return the default value for a particular setting. + + If it does not exist, return an empty string + """ + + key = str(key).strip().upper() + + if key in cls.GLOBAL_SETTINGS: + setting = cls.GLOBAL_SETTINGS[key] + return setting.get('default', '') + else: + return '' + + @classmethod + def get_setting_choices(cls, key): + """ + Return the validator choices available for a particular setting. + """ + + key = str(key).strip().upper() + + if key in cls.GLOBAL_SETTINGS: + setting = cls.GLOBAL_SETTINGS[key] + choices = setting.get('choices', None) + else: + choices = None + + """ + TODO: + if type(choices) is function: + # Evaluate the function (we expect it will return a list of tuples...) + return choices() + """ + + return choices + + @classmethod + def get_setting_object(cls, key): + """ + Return an InvenTreeSetting object matching the given key. + + - Key is case-insensitive + - Returns None if no match is made + """ + + key = str(key).strip().upper() + + try: + setting = cls.objects.filter(key__iexact=key).first() + except (ValueError, cls.DoesNotExist): + setting = None + except (IntegrityError, OperationalError): + setting = None + + # Setting does not exist! (Try to create it) + if not setting: + + setting = cls(key=key, value=cls.get_setting_default(key)) + + try: + # Wrap this statement in "atomic", so it can be rolled back if it fails + with transaction.atomic(): + setting.save() + except (IntegrityError, OperationalError): + # It might be the case that the database isn't created yet + pass + + return setting + + @classmethod + def get_setting_pk(cls, key): + """ + Return the primary-key value for a given setting. + + If the setting does not exist, return None + """ + + setting = cls.get_setting_object(cls) + + if setting: + return setting.pk + else: + return None + + @classmethod + def get_setting(cls, key, backup_value=None): + """ + Get the value of a particular setting. + If it does not exist, return the backup value (default = None) + """ + + # If no backup value is specified, atttempt to retrieve a "default" value + if backup_value is None: + backup_value = cls.get_setting_default(key) + + setting = cls.get_setting_object(key) + + if setting: + value = setting.value + + # If the particular setting is defined as a boolean, cast the value to a boolean + if setting.is_bool(): + value = InvenTree.helpers.str2bool(value) + + if setting.is_int(): + try: + value = int(value) + except (ValueError, TypeError): + value = backup_value + + else: + value = backup_value + + return value + + @classmethod + def set_setting(cls, key, value, user, create=True): + """ + Set the value of a particular setting. + If it does not exist, option to create it. + + Args: + key: settings key + value: New value + user: User object (must be staff member to update a core setting) + create: If True, create a new setting if the specified key does not exist. + """ + + if user is not None and not user.is_staff: + return + + try: + setting = cls.objects.get(key__iexact=key) + except cls.DoesNotExist: + + if create: + setting = cls(key=key) + else: + return + + # Enforce standard boolean representation + if setting.is_bool(): + value = InvenTree.helpers.str2bool(value) + + setting.value = str(value) + setting.save() + + key = models.CharField(max_length=50, blank=False, unique=True, help_text=_('Settings key (must be unique - case insensitive')) + + value = models.CharField(max_length=200, blank=True, unique=False, help_text=_('Settings value')) + + @property + def name(self): + return self.__class__.get_setting_name(self.key) + + @property + def default_value(self): + return self.__class__.get_setting_default(self.key) + + @property + def description(self): + return self.__class__.get_setting_description(self.key) + + @property + def units(self): + return self.__class__.get_setting_units(self.key) + + def clean(self): + """ + If a validator (or multiple validators) are defined for a particular setting key, + run them against the 'value' field. + """ + + super().clean() + + validator = self.__class__.get_setting_validator(self.key) + + if self.is_bool(): + self.value = InvenTree.helpers.str2bool(self.value) + + if self.is_int(): + try: + self.value = int(self.value) + except (ValueError): + raise ValidationError(_('Must be an integer value')) + + if validator is not None: + self.run_validator(validator) + + def run_validator(self, validator): + """ + Run a validator against the 'value' field for this InvenTreeSetting object. + """ + + if validator is None: + return + + value = self.value + + # Boolean validator + if self.is_bool(): + # Value must "look like" a boolean value + if InvenTree.helpers.is_bool(value): + # Coerce into either "True" or "False" + value = InvenTree.helpers.str2bool(value) + else: + raise ValidationError({ + 'value': _('Value must be a boolean value') + }) + + # Integer validator + if self.is_int(): + + try: + # Coerce into an integer value + value = int(value) + except (ValueError, TypeError): + raise ValidationError({ + 'value': _('Value must be an integer value'), + }) + + # If a list of validators is supplied, iterate through each one + if type(validator) in [list, tuple]: + for v in validator: + self.run_validator(v) + + if callable(validator): + # We can accept function validators with a single argument + validator(self.value) + + def validate_unique(self, exclude=None): + """ Ensure that the key:value pair is unique. + In addition to the base validators, this ensures that the 'key' + is unique, using a case-insensitive comparison. + """ + + super().validate_unique(exclude) + + try: + setting = self.__class__.objects.exclude(id=self.id).filter(key__iexact=self.key) + if setting.exists(): + raise ValidationError({'key': _('Key string must be unique')}) + except self.DoesNotExist: + pass + + def choices(self): + """ + Return the available choices for this setting (or None if no choices are defined) + """ + + return self.__class__.get_setting_choices(self.key) + + def is_bool(self): + """ + Check if this setting is required to be a boolean value + """ + + validator = self.__class__.get_setting_validator(self.key) + + if validator == bool: + return True + + if type(validator) in [list, tuple]: + for v in validator: + if v == bool: + return True + + def as_bool(self): + """ + Return the value of this setting converted to a boolean value. + + Warning: Only use on values where is_bool evaluates to true! + """ + + return InvenTree.helpers.str2bool(self.value) + + def is_int(self): + """ + Check if the setting is required to be an integer value: + """ + + validator = self.__class__.get_setting_validator(self.key) + + if validator == int: + return True + + if type(validator) in [list, tuple]: + for v in validator: + if v == int: + return True + + return False + + def as_int(self): + """ + Return the value of this setting converted to a boolean value. + + If an error occurs, return the default value + """ + + try: + value = int(self.value) + except (ValueError, TypeError): + value = self.default_value() + + return value + + +class InvenTreeSetting(BaseInvenTreeSetting): """ An InvenTreeSetting object is a key:value pair used for storing single values (e.g. one-off settings values). @@ -357,380 +745,6 @@ class InvenTreeSetting(models.Model): verbose_name = "InvenTree Setting" verbose_name_plural = "InvenTree Settings" - @classmethod - def get_setting_name(cls, key): - """ - Return the name of a particular setting. - - If it does not exist, return an empty string. - """ - - key = str(key).strip().upper() - - if key in cls.GLOBAL_SETTINGS: - setting = cls.GLOBAL_SETTINGS[key] - return setting.get('name', '') - else: - return '' - - @classmethod - def get_setting_description(cls, key): - """ - Return the description for a particular setting. - - If it does not exist, return an empty string. - """ - - key = str(key).strip().upper() - - if key in cls.GLOBAL_SETTINGS: - setting = cls.GLOBAL_SETTINGS[key] - return setting.get('description', '') - else: - return '' - - @classmethod - def get_setting_units(cls, key): - """ - Return the units for a particular setting. - - If it does not exist, return an empty string. - """ - - key = str(key).strip().upper() - - if key in cls.GLOBAL_SETTINGS: - setting = cls.GLOBAL_SETTINGS[key] - return setting.get('units', '') - else: - return '' - - @classmethod - def get_setting_validator(cls, key): - """ - Return the validator for a particular setting. - - If it does not exist, return None - """ - - key = str(key).strip().upper() - - if key in cls.GLOBAL_SETTINGS: - setting = cls.GLOBAL_SETTINGS[key] - return setting.get('validator', None) - else: - return None - - @classmethod - def get_setting_default(cls, key): - """ - Return the default value for a particular setting. - - If it does not exist, return an empty string - """ - - key = str(key).strip().upper() - - if key in cls.GLOBAL_SETTINGS: - setting = cls.GLOBAL_SETTINGS[key] - return setting.get('default', '') - else: - return '' - - @classmethod - def get_setting_choices(cls, key): - """ - Return the validator choices available for a particular setting. - """ - - key = str(key).strip().upper() - - if key in cls.GLOBAL_SETTINGS: - setting = cls.GLOBAL_SETTINGS[key] - choices = setting.get('choices', None) - else: - choices = None - - """ - TODO: - if type(choices) is function: - # Evaluate the function (we expect it will return a list of tuples...) - return choices() - """ - - return choices - - @classmethod - def get_setting_object(cls, key): - """ - Return an InvenTreeSetting object matching the given key. - - - Key is case-insensitive - - Returns None if no match is made - """ - - key = str(key).strip().upper() - - try: - setting = InvenTreeSetting.objects.filter(key__iexact=key).first() - except (ValueError, InvenTreeSetting.DoesNotExist): - setting = None - except (IntegrityError, OperationalError): - setting = None - - # Setting does not exist! (Try to create it) - if not setting: - - setting = InvenTreeSetting(key=key, value=InvenTreeSetting.get_setting_default(key)) - - try: - # Wrap this statement in "atomic", so it can be rolled back if it fails - with transaction.atomic(): - setting.save() - except (IntegrityError, OperationalError): - # It might be the case that the database isn't created yet - pass - - return setting - - @classmethod - def get_setting_pk(cls, key): - """ - Return the primary-key value for a given setting. - - If the setting does not exist, return None - """ - - setting = InvenTreeSetting.get_setting_object(cls) - - if setting: - return setting.pk - else: - return None - - @classmethod - def get_setting(cls, key, backup_value=None): - """ - Get the value of a particular setting. - If it does not exist, return the backup value (default = None) - """ - - # If no backup value is specified, atttempt to retrieve a "default" value - if backup_value is None: - backup_value = cls.get_setting_default(key) - - setting = InvenTreeSetting.get_setting_object(key) - - if setting: - value = setting.value - - # If the particular setting is defined as a boolean, cast the value to a boolean - if setting.is_bool(): - value = InvenTree.helpers.str2bool(value) - - if setting.is_int(): - try: - value = int(value) - except (ValueError, TypeError): - value = backup_value - - else: - value = backup_value - - return value - - @classmethod - def set_setting(cls, key, value, user, create=True): - """ - Set the value of a particular setting. - If it does not exist, option to create it. - - Args: - key: settings key - value: New value - user: User object (must be staff member to update a core setting) - create: If True, create a new setting if the specified key does not exist. - """ - - if user is not None and not user.is_staff: - return - - try: - setting = InvenTreeSetting.objects.get(key__iexact=key) - except InvenTreeSetting.DoesNotExist: - - if create: - setting = InvenTreeSetting(key=key) - else: - return - - # Enforce standard boolean representation - if setting.is_bool(): - value = InvenTree.helpers.str2bool(value) - - setting.value = str(value) - setting.save() - - key = models.CharField(max_length=50, blank=False, unique=True, help_text=_('Settings key (must be unique - case insensitive')) - - value = models.CharField(max_length=200, blank=True, unique=False, help_text=_('Settings value')) - - @property - def name(self): - return InvenTreeSetting.get_setting_name(self.key) - - @property - def default_value(self): - return InvenTreeSetting.get_setting_default(self.key) - - @property - def description(self): - return InvenTreeSetting.get_setting_description(self.key) - - @property - def units(self): - return InvenTreeSetting.get_setting_units(self.key) - - def clean(self): - """ - If a validator (or multiple validators) are defined for a particular setting key, - run them against the 'value' field. - """ - - super().clean() - - validator = InvenTreeSetting.get_setting_validator(self.key) - - if self.is_bool(): - self.value = InvenTree.helpers.str2bool(self.value) - - if self.is_int(): - try: - self.value = int(self.value) - except (ValueError): - raise ValidationError(_('Must be an integer value')) - - if validator is not None: - self.run_validator(validator) - - def run_validator(self, validator): - """ - Run a validator against the 'value' field for this InvenTreeSetting object. - """ - - if validator is None: - return - - value = self.value - - # Boolean validator - if self.is_bool(): - # Value must "look like" a boolean value - if InvenTree.helpers.is_bool(value): - # Coerce into either "True" or "False" - value = InvenTree.helpers.str2bool(value) - else: - raise ValidationError({ - 'value': _('Value must be a boolean value') - }) - - # Integer validator - if self.is_int(): - - try: - # Coerce into an integer value - value = int(value) - except (ValueError, TypeError): - raise ValidationError({ - 'value': _('Value must be an integer value'), - }) - - # If a list of validators is supplied, iterate through each one - if type(validator) in [list, tuple]: - for v in validator: - self.run_validator(v) - - if callable(validator): - # We can accept function validators with a single argument - validator(self.value) - - def validate_unique(self, exclude=None): - """ Ensure that the key:value pair is unique. - In addition to the base validators, this ensures that the 'key' - is unique, using a case-insensitive comparison. - """ - - super().validate_unique(exclude) - - try: - setting = InvenTreeSetting.objects.exclude(id=self.id).filter(key__iexact=self.key) - if setting.exists(): - raise ValidationError({'key': _('Key string must be unique')}) - except InvenTreeSetting.DoesNotExist: - pass - - def choices(self): - """ - Return the available choices for this setting (or None if no choices are defined) - """ - - return InvenTreeSetting.get_setting_choices(self.key) - - def is_bool(self): - """ - Check if this setting is required to be a boolean value - """ - - validator = InvenTreeSetting.get_setting_validator(self.key) - - if validator == bool: - return True - - if type(validator) in [list, tuple]: - for v in validator: - if v == bool: - return True - - def as_bool(self): - """ - Return the value of this setting converted to a boolean value. - - Warning: Only use on values where is_bool evaluates to true! - """ - - return InvenTree.helpers.str2bool(self.value) - - def is_int(self): - """ - Check if the setting is required to be an integer value: - """ - - validator = InvenTreeSetting.get_setting_validator(self.key) - - if validator == int: - return True - - if type(validator) in [list, tuple]: - for v in validator: - if v == int: - return True - - return False - - def as_int(self): - """ - Return the value of this setting converted to a boolean value. - - If an error occurs, return the default value - """ - - try: - value = int(self.value) - except (ValueError, TypeError): - value = self.default_value() - - return value - class PriceBreak(models.Model): """ From ce3f7b698d5c1d0010d98b60a1c3df3f8e28eafc Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 20 Jul 2021 01:35:58 +0200 Subject: [PATCH 17/62] InvenTreeUserSettings added --- .../migrations/0011_inventreeusersetting.py | 29 +++++++++++++++++++ InvenTree/common/models.py | 27 +++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 InvenTree/common/migrations/0011_inventreeusersetting.py diff --git a/InvenTree/common/migrations/0011_inventreeusersetting.py b/InvenTree/common/migrations/0011_inventreeusersetting.py new file mode 100644 index 0000000000..0bec691041 --- /dev/null +++ b/InvenTree/common/migrations/0011_inventreeusersetting.py @@ -0,0 +1,29 @@ +# Generated by Django 3.2.4 on 2021-07-19 22:57 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('common', '0010_migrate_currency_setting'), + ] + + operations = [ + migrations.CreateModel( + name='InvenTreeUserSetting', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('key', models.CharField(help_text='Settings key (must be unique - case insensitive', max_length=50, unique=True)), + ('value', models.CharField(blank=True, help_text='Settings value', max_length=200)), + ('user', models.ForeignKey(blank=True, help_text='User', null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='User')), + ], + options={ + 'verbose_name': 'InvenTree User Setting', + 'verbose_name_plural': 'InvenTree User Settings', + }, + ), + ] diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index 02d752aa8c..8907299a44 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -746,6 +746,33 @@ class InvenTreeSetting(BaseInvenTreeSetting): verbose_name_plural = "InvenTree Settings" +class InvenTreeUserSetting(BaseInvenTreeSetting): + """ + An InvenTreeSetting object with a usercontext + """ + + GLOBAL_SETTINGS = { + 'PART_ASSEMBLY': { + 'name': _('Assembly'), + 'description': _('Parts can be assembled from other components by default'), + 'default': False, + 'validator': bool, + }, + } + + class Meta: + verbose_name = "InvenTree User Setting" + verbose_name_plural = "InvenTree User Settings" + + user = models.ForeignKey( + User, + on_delete=models.CASCADE, + blank=True, null=True, + verbose_name=_('User'), + help_text=_('User'), + ) + + class PriceBreak(models.Model): """ Represents a PriceBreak model From 4600b0d33732f6442c1ae1970131ee7b61c8c7e4 Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 20 Jul 2021 11:35:55 +1000 Subject: [PATCH 18/62] Update README.md --- README.md | 9 --------- 1 file changed, 9 deletions(-) diff --git a/README.md b/README.md index c1321557e8..2a23ab608e 100644 --- a/README.md +++ b/README.md @@ -34,15 +34,6 @@ InvenTree is supported by a [companion mobile app](https://inventree.readthedocs # Translation -![de translation](https://img.shields.io/badge/dynamic/json?color=blue&label=de&style=flat&query=%24.progress.0.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-14720186-452300.json) -![es-ES translation](https://img.shields.io/badge/dynamic/json?color=blue&label=es-ES&style=flat&query=%24.progress.1.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-14720186-452300.json) -![fr translation](https://img.shields.io/badge/dynamic/json?color=blue&label=fr&style=flat&query=%24.progress.3.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-14720186-452300.json) -![it translation](https://img.shields.io/badge/dynamic/json?color=blue&label=it&style=flat&query=%24.progress.4.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-14720186-452300.json) -![pl translation](https://img.shields.io/badge/dynamic/json?color=blue&label=pl&style=flat&query=%24.progress.5.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-14720186-452300.json) -![ru translation](https://img.shields.io/badge/dynamic/json?color=blue&label=ru&style=flat&query=%24.progress.6.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-14720186-452300.json) -![tr translation](https://img.shields.io/badge/dynamic/json?color=blue&label=tr&style=flat&query=%24.progress.6.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-14720186-452300.json) -![zh-CN translation](https://img.shields.io/badge/dynamic/json?color=blue&label=zh-CN&style=flat&query=%24.progress.7.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-14720186-452300.json) - Native language translation of the InvenTree web application is [community contributed via crowdin](https://crowdin.com/project/inventree). **Contributions are welcomed and encouraged**. To contribute to the translation effort, navigate to the [InvenTree crowdin project](https://crowdin.com/project/inventree), create a free account, and start making translations suggestions for your language of choice! From b0c4a58f3002f2f0971c6b254af443773b965d4e Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 20 Jul 2021 15:06:09 +1000 Subject: [PATCH 19/62] Fix URL patterns for ManufacturerPart and SupplierPart --- InvenTree/company/urls.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/InvenTree/company/urls.py b/InvenTree/company/urls.py index 901e7f7089..609311bcba 100644 --- a/InvenTree/company/urls.py +++ b/InvenTree/company/urls.py @@ -30,11 +30,9 @@ company_urls = [ manufacturer_part_urls = [ - url(r'^(?P\d+)/', include([ - url('^.*$', views.ManufacturerPartDetail.as_view(template_name='company/manufacturer_part.html'), name='manufacturer-part-detail'), - ])), + url(r'^(?P\d+)/', views.ManufacturerPartDetail.as_view(template_name='company/manufacturer_part.html'), name='manufacturer-part-detail'), ] supplier_part_urls = [ - url('^.*$', views.SupplierPartDetail.as_view(template_name='company/supplier_part.html'), name='supplier-part-detail'), + url(r'^(?P\d+)/', views.SupplierPartDetail.as_view(template_name='company/supplier_part.html'), name='supplier-part-detail'), ] From 289b030f4ebd307c2c7d455dac9830f3662fae9f Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 20 Jul 2021 07:49:21 +0200 Subject: [PATCH 20/62] limit results in response --- InvenTree/InvenTree/static/script/inventree/inventree.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/InvenTree/InvenTree/static/script/inventree/inventree.js b/InvenTree/InvenTree/static/script/inventree/inventree.js index c508942f4d..e5952ea1fc 100644 --- a/InvenTree/InvenTree/static/script/inventree/inventree.js +++ b/InvenTree/InvenTree/static/script/inventree/inventree.js @@ -86,9 +86,9 @@ function inventreeDocReady() { source: function (request, response) { $.ajax({ url: '/api/part/', - data: { search: request.term }, + data: { search: request.term, limit: 4, offset: 0 }, success: function (data) { - var transformed = $.map(data, function (el) { + var transformed = $.map(data.results, function (el) { return { label: el.name, id: el.pk From 84fc2785d6d5c9144e39f1342fbd324caff3aa29 Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 20 Jul 2021 21:26:51 +1000 Subject: [PATCH 21/62] Create a custom Manager class for the Part model - Always perform prefetch_related calls --- InvenTree/part/models.py | 19 +++++++++++++++++++ InvenTree/part/serializers.py | 19 ------------------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 72d388746b..95a8e407b9 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -284,6 +284,23 @@ def match_part_names(match, threshold=80, reverse=True, compare_length=False): return matches +class PartManager(models.Manager): + """ + Defines a custom object manager for the Part model. + + The main purpose of this manager is to reduce the number of database hits, + as the Part model has a large number of ForeignKey fields! + """ + + def get_queryset(self): + + return super().get_queryset().prefetch_related( + 'category', + 'stock_items', + 'builds', + ) + + @cleanup.ignore class Part(MPTTModel): """ The Part object represents an abstract part, the 'concept' of an actual entity. @@ -321,6 +338,8 @@ class Part(MPTTModel): responsible: User who is responsible for this part (optional) """ + objects = PartManager() + class Meta: verbose_name = _("Part") verbose_name_plural = _("Parts") diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index 92dda58590..db543b8602 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -215,25 +215,6 @@ class PartSerializer(InvenTreeModelSerializer): if category_detail is not True: self.fields.pop('category_detail') - @staticmethod - def prefetch_queryset(queryset): - """ - Prefetch related database tables, - to reduce database hits. - """ - - return queryset.prefetch_related( - 'category', - 'category__parts', - 'category__parent', - 'stock_items', - 'bom_items', - 'builds', - 'supplier_parts', - 'supplier_parts__purchase_order_line_items', - 'supplier_parts__purchase_order_line_items__order', - ) - @staticmethod def annotate_queryset(queryset): """ From dbe550a159e75279e8873539c5000c710ad68dd0 Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 20 Jul 2021 21:37:32 +1000 Subject: [PATCH 22/62] Optimizations for PartList API endpoint: - Remove custom list() function - Queryset prefetch now performed by the model manager --- InvenTree/part/api.py | 76 ++++--------------------------------------- 1 file changed, 7 insertions(+), 69 deletions(-) diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index 3a5fee4e3d..3b265b541c 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -536,75 +536,15 @@ class PartList(generics.ListCreateAPIView): kwargs['starred_parts'] = self.starred_parts + try: + params = self.request.query_params + + kwargs['category_detail'] = str2bool(params.get('category_detail', False)) + except AttributeError: + pass + return self.serializer_class(*args, **kwargs) - def list(self, request, *args, **kwargs): - """ - Overide the 'list' method, as the PartCategory objects are - very expensive to serialize! - - So we will serialize them first, and keep them in memory, - so that they do not have to be serialized multiple times... - """ - - queryset = self.filter_queryset(self.get_queryset()) - - page = self.paginate_queryset(queryset) - - if page is not None: - serializer = self.get_serializer(page, many=True) - else: - serializer = self.get_serializer(queryset, many=True) - - data = serializer.data - - # Do we wish to include PartCategory detail? - if str2bool(request.query_params.get('category_detail', False)): - - # Work out which part categories we need to query - category_ids = set() - - for part in data: - cat_id = part['category'] - - if cat_id is not None: - category_ids.add(cat_id) - - # Fetch only the required PartCategory objects from the database - categories = PartCategory.objects.filter(pk__in=category_ids).prefetch_related( - 'parts', - 'parent', - 'children', - ) - - category_map = {} - - # Serialize each PartCategory object - for category in categories: - category_map[category.pk] = part_serializers.CategorySerializer(category).data - - for part in data: - cat_id = part['category'] - - if cat_id is not None and cat_id in category_map.keys(): - detail = category_map[cat_id] - else: - detail = None - - part['category_detail'] = detail - - """ - Determine the response type based on the request. - a) For HTTP requests (e.g. via the browseable API) return a DRF response - b) For AJAX requests, simply return a JSON rendered response. - """ - if page is not None: - return self.get_paginated_response(data) - elif request.is_ajax(): - return JsonResponse(data, safe=False) - else: - return Response(data) - def perform_create(self, serializer): """ We wish to save the user who created this part! @@ -619,8 +559,6 @@ class PartList(generics.ListCreateAPIView): def get_queryset(self, *args, **kwargs): queryset = super().get_queryset(*args, **kwargs) - - queryset = part_serializers.PartSerializer.prefetch_queryset(queryset) queryset = part_serializers.PartSerializer.annotate_queryset(queryset) return queryset From 4199e7567f585bb03fae00dd5404df569cda9523 Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 20 Jul 2021 21:46:27 +1000 Subject: [PATCH 23/62] Remove duplicate annotation call --- InvenTree/part/api.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index 3b265b541c..8c7258c4ee 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -571,10 +571,6 @@ class PartList(generics.ListCreateAPIView): params = self.request.query_params - # Annotate calculated data to the queryset - # (This will be used for further filtering) - queryset = part_serializers.PartSerializer.annotate_queryset(queryset) - queryset = super().filter_queryset(queryset) # Filter by "uses" query - Limit to parts which use the provided part From cb0b7209ec2dea6319d448576c3dc3fd68a0d824 Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 20 Jul 2021 22:12:01 +1000 Subject: [PATCH 24/62] Add custom "list" function back in - Actually does make a significant difference to query speed --- InvenTree/part/api.py | 74 ++++++++++++++++++++++++++++++++++++---- InvenTree/part/models.py | 3 +- 2 files changed, 69 insertions(+), 8 deletions(-) diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index 8c7258c4ee..7506bc09f4 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -536,15 +536,75 @@ class PartList(generics.ListCreateAPIView): kwargs['starred_parts'] = self.starred_parts - try: - params = self.request.query_params - - kwargs['category_detail'] = str2bool(params.get('category_detail', False)) - except AttributeError: - pass - return self.serializer_class(*args, **kwargs) + def list(self, request, *args, **kwargs): + """ + Overide the 'list' method, as the PartCategory objects are + very expensive to serialize! + + So we will serialize them first, and keep them in memory, + so that they do not have to be serialized multiple times... + """ + + queryset = self.filter_queryset(self.get_queryset()) + + page = self.paginate_queryset(queryset) + + if page is not None: + serializer = self.get_serializer(page, many=True) + else: + serializer = self.get_serializer(queryset, many=True) + + data = serializer.data + + # Do we wish to include PartCategory detail? + if str2bool(request.query_params.get('category_detail', False)): + + # Work out which part categories we need to query + category_ids = set() + + for part in data: + cat_id = part['category'] + + if cat_id is not None: + category_ids.add(cat_id) + + # Fetch only the required PartCategory objects from the database + categories = PartCategory.objects.filter(pk__in=category_ids).prefetch_related( + 'parts', + 'parent', + 'children', + ) + + category_map = {} + + # Serialize each PartCategory object + for category in categories: + category_map[category.pk] = part_serializers.CategorySerializer(category).data + + for part in data: + cat_id = part['category'] + + if cat_id is not None and cat_id in category_map.keys(): + detail = category_map[cat_id] + else: + detail = None + + part['category_detail'] = detail + + """ + Determine the response type based on the request. + a) For HTTP requests (e.g. via the browseable API) return a DRF response + b) For AJAX requests, simply return a JSON rendered response. + """ + if page is not None: + return self.get_paginated_response(data) + elif request.is_ajax(): + return JsonResponse(data, safe=False) + else: + return Response(data) + def perform_create(self, serializer): """ We wish to save the user who created this part! diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 95a8e407b9..fa19f8b075 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -296,8 +296,9 @@ class PartManager(models.Manager): return super().get_queryset().prefetch_related( 'category', + 'category__parent', 'stock_items', - 'builds', + 'builds', ) From b04a40308151985f29ee88872c896ce82a6a8787 Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 20 Jul 2021 22:15:49 +1000 Subject: [PATCH 25/62] subclass TreeManager --- InvenTree/part/models.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index fa19f8b075..49be12e283 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -27,6 +27,7 @@ from markdownx.models import MarkdownxField from django_cleanup import cleanup from mptt.models import TreeForeignKey, MPTTModel +from mptt.managers import TreeManager from stdimage.models import StdImageField @@ -284,7 +285,7 @@ def match_part_names(match, threshold=80, reverse=True, compare_length=False): return matches -class PartManager(models.Manager): +class PartManager(TreeManager): """ Defines a custom object manager for the Part model. From 5e2145e1515cc5b4e0524bd47a71db8c0ce1acd5 Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 20 Jul 2021 22:26:43 +1000 Subject: [PATCH 26/62] Bug fix - delete line which don't belong no more --- InvenTree/part/api.py | 1 - 1 file changed, 1 deletion(-) diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index 7506bc09f4..39801070c7 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -361,7 +361,6 @@ class PartDetail(generics.RetrieveUpdateDestroyAPIView): def get_queryset(self, *args, **kwargs): queryset = super().get_queryset(*args, **kwargs) - queryset = part_serializers.PartSerializer.prefetch_queryset(queryset) queryset = part_serializers.PartSerializer.annotate_queryset(queryset) return queryset From 30f94bef41631d20ca959066dc4073115e941f41 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 20 Jul 2021 18:51:27 +0200 Subject: [PATCH 27/62] adding style and picture --- InvenTree/InvenTree/static/css/inventree.css | 7 +++++++ .../static/script/inventree/inventree.js | 19 ++++++++++++++----- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/InvenTree/InvenTree/static/css/inventree.css b/InvenTree/InvenTree/static/css/inventree.css index a71e29ab5d..adb5a41ee6 100644 --- a/InvenTree/InvenTree/static/css/inventree.css +++ b/InvenTree/InvenTree/static/css/inventree.css @@ -1037,3 +1037,10 @@ a.anchor { height: 30px; } +.search-menu { + padding-top: 2rem; +} + +.search-menu .ui-menu-item { + margin-top: 0.5rem; +} diff --git a/InvenTree/InvenTree/static/script/inventree/inventree.js b/InvenTree/InvenTree/static/script/inventree/inventree.js index e5952ea1fc..a9b516a4b0 100644 --- a/InvenTree/InvenTree/static/script/inventree/inventree.js +++ b/InvenTree/InvenTree/static/script/inventree/inventree.js @@ -91,7 +91,8 @@ function inventreeDocReady() { var transformed = $.map(data.results, function (el) { return { label: el.name, - id: el.pk + id: el.pk, + thumbnail: el.thumbnail }; }); response(transformed); @@ -101,11 +102,19 @@ function inventreeDocReady() { } }); }, - minLength: 2, - classes: {'ui-autocomplete': 'dropdown-menu'}, + create: function () { + $(this).data('ui-autocomplete')._renderItem = function (ul, item) { + console.log(item); + return $('
  • ') + .append('' + imageHoverIcon(item.thumbnail) + item.label + '') + .appendTo(ul); + }; + }, select: function( event, ui ) { - window.location = '/part/' + ui.item.id + '/'; - } + window.location = '/part/' + ui.item.id + '/'; + }, + minLength: 2, + classes: {'ui-autocomplete': 'dropdown-menu search-menu'}, }); } From ff07cf5516ccf55a432d639ecb0b4f2fce78ed4a Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 20 Jul 2021 18:52:52 +0200 Subject: [PATCH 28/62] cleanup --- InvenTree/InvenTree/static/script/inventree/inventree.js | 1 - 1 file changed, 1 deletion(-) diff --git a/InvenTree/InvenTree/static/script/inventree/inventree.js b/InvenTree/InvenTree/static/script/inventree/inventree.js index a9b516a4b0..bdcbb82ebd 100644 --- a/InvenTree/InvenTree/static/script/inventree/inventree.js +++ b/InvenTree/InvenTree/static/script/inventree/inventree.js @@ -104,7 +104,6 @@ function inventreeDocReady() { }, create: function () { $(this).data('ui-autocomplete')._renderItem = function (ul, item) { - console.log(item); return $('
  • ') .append('' + imageHoverIcon(item.thumbnail) + item.label + '') .appendTo(ul); From 8ac3d42fd89b85737e54bfa64e1df5f7b89abdb8 Mon Sep 17 00:00:00 2001 From: eeintech Date: Tue, 20 Jul 2021 17:15:01 -0400 Subject: [PATCH 29/62] Improved handling of po items destination --- InvenTree/order/forms.py | 4 +-- .../order/templates/order/receive_parts.html | 13 ++++++++-- InvenTree/order/views.py | 25 +++++++++++++------ 3 files changed, 31 insertions(+), 11 deletions(-) diff --git a/InvenTree/order/forms.py b/InvenTree/order/forms.py index b8a66a825d..e0c500e5e3 100644 --- a/InvenTree/order/forms.py +++ b/InvenTree/order/forms.py @@ -84,9 +84,9 @@ class ReceivePurchaseOrderForm(HelperForm): location = TreeNodeChoiceField( queryset=StockLocation.objects.all(), - required=True, + required=False, label=_("Destination"), - help_text=_("Receive parts to this location"), + help_text=_("Set all received parts listed above to this location (if left blank, use \"Destination\" column value in above table)"), ) class Meta: diff --git a/InvenTree/order/templates/order/receive_parts.html b/InvenTree/order/templates/order/receive_parts.html index bfddf01ce9..847c21b0a3 100644 --- a/InvenTree/order/templates/order/receive_parts.html +++ b/InvenTree/order/templates/order/receive_parts.html @@ -12,7 +12,7 @@ {% load crispy_forms_tags %} -

    {% trans "Select parts to receive against this order" %}

    +

    {% trans "Fill out number of parts received, the status and destination" %}

    @@ -55,7 +55,14 @@
    - {{ line.get_destination }} +
    + +
    {% crispy form %} + +
    {{ form_errors }}
    {% endblock %} \ No newline at end of file diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py index a109b036e0..e8b0dc03e9 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -491,6 +491,7 @@ class PurchaseOrderReceive(AjaxUpdateView): ctx = { 'order': self.order, 'lines': self.lines, + 'stock_locations': StockLocation.objects.all(), } return ctx @@ -543,6 +544,7 @@ class PurchaseOrderReceive(AjaxUpdateView): self.request = request self.order = get_object_or_404(PurchaseOrder, pk=self.kwargs['pk']) + errors = False self.lines = [] self.destination = None @@ -557,12 +559,6 @@ class PurchaseOrderReceive(AjaxUpdateView): except (StockLocation.DoesNotExist, ValueError): pass - errors = False - - if self.destination is None: - errors = True - msg = _("No destination set") - # Extract information on all submitted line items for item in request.POST: if item.startswith('line-'): @@ -587,6 +583,21 @@ class PurchaseOrderReceive(AjaxUpdateView): else: line.status_code = StockStatus.OK + # Check the destination field + line.destination = None + if self.destination: + # If global destination is set, overwrite line value + line.destination = self.destination + else: + destination_key = f'destination-{pk}' + destination = request.POST.get(destination_key, None) + + if destination: + try: + line.destination = StockLocation.objects.get(pk=destination) + except (StockLocation.DoesNotExist, ValueError): + pass + # Check that line matches the order if not line.order == self.order: # TODO - Display a non-field error? @@ -645,7 +656,7 @@ class PurchaseOrderReceive(AjaxUpdateView): self.order.receive_line_item( line, - self.destination, + line.destination, line.receive_quantity, self.request.user, status=line.status_code, From 598ea112110cf1663231384530081470660c1bad Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 21 Jul 2021 09:28:58 +1000 Subject: [PATCH 30/62] Add manager class for StockItem --- InvenTree/part/test_category.py | 2 +- InvenTree/stock/api.py | 2 -- InvenTree/stock/models.py | 26 ++++++++++++++++++++++++++ InvenTree/stock/serializers.py | 23 ----------------------- 4 files changed, 27 insertions(+), 26 deletions(-) diff --git a/InvenTree/part/test_category.py b/InvenTree/part/test_category.py index e616fc2054..75261378b0 100644 --- a/InvenTree/part/test_category.py +++ b/InvenTree/part/test_category.py @@ -99,7 +99,7 @@ class CategoryTest(TestCase): """ Test that the Category parameters are correctly fetched """ # Check number of SQL queries to iterate other parameters - with self.assertNumQueries(3): + with self.assertNumQueries(7): # Prefetch: 3 queries (parts, parameters and parameters_template) fasteners = self.fasteners.prefetch_parts_parameters() # Iterate through all parts and parameters diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index 70ab939ff1..56df35b5c3 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -84,7 +84,6 @@ class StockDetail(generics.RetrieveUpdateDestroyAPIView): def get_queryset(self, *args, **kwargs): queryset = super().get_queryset(*args, **kwargs) - queryset = StockItemSerializer.prefetch_queryset(queryset) queryset = StockItemSerializer.annotate_queryset(queryset) return queryset @@ -637,7 +636,6 @@ class StockList(generics.ListCreateAPIView): queryset = super().get_queryset(*args, **kwargs) - queryset = StockItemSerializer.prefetch_queryset(queryset) queryset = StockItemSerializer.annotate_queryset(queryset) return queryset diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index ee10bd3ed7..c2122f40ac 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -23,6 +23,7 @@ from django.dispatch import receiver from markdownx.models import MarkdownxField from mptt.models import MPTTModel, TreeForeignKey +from mptt.managers import TreeManager from decimal import Decimal, InvalidOperation from datetime import datetime, timedelta @@ -130,6 +131,31 @@ def before_delete_stock_location(sender, instance, using, **kwargs): child.save() +class StockItemManager(TreeManager): + """ + Custom database manager for the StockItem class. + + StockItem querysets will automatically prefetch related fields. + """ + + def get_queryset(self): + + return super().get_queryset().prefetch_related( + 'belongs_to', + 'build', + 'customer', + 'purchase_order', + 'sales_order', + 'supplier_part', + 'supplier_part__supplier', + 'allocations', + 'sales_order_allocations', + 'location', + 'part', + 'tracking_info' + ) + + class StockItem(MPTTModel): """ A StockItem object represents a quantity of physical instances of a part. diff --git a/InvenTree/stock/serializers.py b/InvenTree/stock/serializers.py index a175787c63..41dc959f02 100644 --- a/InvenTree/stock/serializers.py +++ b/InvenTree/stock/serializers.py @@ -70,29 +70,6 @@ class StockItemSerializer(InvenTreeModelSerializer): - Includes serialization for the item location """ - @staticmethod - def prefetch_queryset(queryset): - """ - Prefetch related database tables, - to reduce database hits. - """ - - return queryset.prefetch_related( - 'belongs_to', - 'build', - 'customer', - 'purchase_order', - 'sales_order', - 'supplier_part', - 'supplier_part__supplier', - 'supplier_part__manufacturer_part__manufacturer', - 'allocations', - 'sales_order_allocations', - 'location', - 'part', - 'tracking_info', - ) - @staticmethod def annotate_queryset(queryset): """ From 2d6a78ffb810cafa2e8aff8674618e8c18c9c3ac Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 21 Jul 2021 10:25:16 +1000 Subject: [PATCH 31/62] Add unit test for validation of reverse url lookup --- InvenTree/InvenTree/test_urls.py | 141 +++++++++++++++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 InvenTree/InvenTree/test_urls.py diff --git a/InvenTree/InvenTree/test_urls.py b/InvenTree/InvenTree/test_urls.py new file mode 100644 index 0000000000..90345aeeb4 --- /dev/null +++ b/InvenTree/InvenTree/test_urls.py @@ -0,0 +1,141 @@ +""" +Validate that all URLs specified in template files are correct. +""" + +from django import urls +from django.conf import settings + +from django.test import TestCase +from django.urls import reverse + +import os +import re + +from pathlib import Path + + +class URLTest(TestCase): + + # Need fixture data in the database + fixtures = [ + 'settings', + 'build', + 'company', + 'manufacturer_part', + 'price_breaks', + 'supplier_part', + 'order', + 'sales_order', + 'bom', + 'category', + 'params', + 'part_pricebreaks', + 'part', + 'test_templates', + 'location', + 'stock_tests', + 'stock', + 'users', + ] + + def find_files(self, suffix): + """ + Search for all files in the template directories, + which can have URLs rendered + """ + + template_dirs = [ + ('build', 'templates'), + ('common', 'templates'), + ('company', 'templates'), + ('label', 'templates'), + ('order', 'templates'), + ('part', 'templates'), + ('report', 'templates'), + ('stock', 'templates'), + ('templates', ), + ] + + template_files = [] + + here = os.path.abspath(os.path.dirname(__file__)) + tld = os.path.join(here, '..') + + for directory in template_dirs: + + template_dir = os.path.join(tld, *directory) + + for path in Path(template_dir).rglob(suffix): + + f = os.path.abspath(path) + + if f not in template_files: + template_files.append(f) + + return template_files + + def find_urls(self, input_file): + """ + Search for all instances of {% url %} in supplied template file + """ + + urls = [] + + pattern = "{% url ['\"]([^'\"]+)['\"]([^%]*)%}" + + with open(input_file, 'r') as f: + + data = f.read() + + results = re.findall(pattern, data) + + for result in results: + if len(result) == 2: + urls.append([ + result[0].strip(), + result[1].strip() + ]) + elif len(result) == 1: + urls.append([ + result[0].strip(), + '' + ]) + + return urls + + def reverse_url(self, url_pair): + """ + Perform lookup on the URL + """ + + url, pk = url_pair + + if pk: + # We will assume that there is at least one item in the database + reverse(url, kwargs={pk: 1}) + else: + reverse(url) + + def check_file(self, f): + """ + Run URL checks for the provided file + """ + + urls = self.find_urls(f) + + for url in urls: + self.reverse_url(url) + + def test_html_templates(self): + + template_files = self.find_files("*.html") + + for f in template_files: + self.check_file(f) + + def test_js_templates(self): + + template_files = self.find_files("*.js") + + for f in template_files: + self.check_file(f) From 8cb336f581052aa7c7cb8af0d57344e3bd14ab87 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 21 Jul 2021 10:42:24 +1000 Subject: [PATCH 32/62] PEP fixes --- InvenTree/InvenTree/test_urls.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/InvenTree/InvenTree/test_urls.py b/InvenTree/InvenTree/test_urls.py index 90345aeeb4..0723332d7d 100644 --- a/InvenTree/InvenTree/test_urls.py +++ b/InvenTree/InvenTree/test_urls.py @@ -2,9 +2,6 @@ Validate that all URLs specified in template files are correct. """ -from django import urls -from django.conf import settings - from django.test import TestCase from django.urls import reverse @@ -110,9 +107,13 @@ class URLTest(TestCase): url, pk = url_pair + # TODO: Handle reverse lookup of admin URLs! + if url.startswith("admin:"): + return + if pk: # We will assume that there is at least one item in the database - reverse(url, kwargs={pk: 1}) + reverse(url, kwargs={"pk": 1}) else: reverse(url) From 893628d1b8e905e0c8097ee759ab83e9c196a013 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 21 Jul 2021 10:52:14 +1000 Subject: [PATCH 33/62] URL fixes --- .../company/templates/company/manufacturer_part_navbar.html | 4 ++-- InvenTree/stock/templates/stock/item_base.html | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/InvenTree/company/templates/company/manufacturer_part_navbar.html b/InvenTree/company/templates/company/manufacturer_part_navbar.html index 893374858f..f0af203543 100644 --- a/InvenTree/company/templates/company/manufacturer_part_navbar.html +++ b/InvenTree/company/templates/company/manufacturer_part_navbar.html @@ -24,14 +24,14 @@ {% comment "for later" %}
  • - + {% trans "Stock" %}
  • - + {% trans "Orders" %} diff --git a/InvenTree/stock/templates/stock/item_base.html b/InvenTree/stock/templates/stock/item_base.html index 547806e256..b16d9b0b1a 100644 --- a/InvenTree/stock/templates/stock/item_base.html +++ b/InvenTree/stock/templates/stock/item_base.html @@ -272,7 +272,7 @@ {% trans "Customer" %} - {{ item.customer.name }} + {{ item.customer.name }} {% endif %} {% if item.belongs_to %} From 2ffae368f152d9d0e90c7ba0d1fe25d13794f1d3 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 21 Jul 2021 15:05:14 +1000 Subject: [PATCH 34/62] Add an option to configure number of parts displayed in search preview box --- InvenTree/InvenTree/urls.py | 1 + InvenTree/common/models.py | 7 +++++++ InvenTree/templates/InvenTree/settings/global.html | 7 +++++++ InvenTree/templates/base.html | 2 +- .../script/inventree => templates/js}/inventree.js | 11 +++++++++-- 5 files changed, 25 insertions(+), 3 deletions(-) rename InvenTree/{InvenTree/static/script/inventree => templates/js}/inventree.js (97%) diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index a3af143f92..cd8575177a 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -111,6 +111,7 @@ dynamic_javascript_urls = [ url(r'^company.js', DynamicJsView.as_view(template_name='js/company.js'), name='company.js'), url(r'^filters.js', DynamicJsView.as_view(template_name='js/filters.js'), name='filters.js'), url(r'^forms.js', DynamicJsView.as_view(template_name='js/forms.js'), name='forms.js'), + url(r'^inventree.js', DynamicJsView.as_view(template_name='js/inventree.js'), name='inventree.js'), url(r'^label.js', DynamicJsView.as_view(template_name='js/label.js'), name='label.js'), url(r'^model_renderers.js', DynamicJsView.as_view(template_name='js/model_renderers.js'), name='model_renderers.js'), url(r'^modals.js', DynamicJsView.as_view(template_name='js/modals.js'), name='modals.js'), diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index dcdeea5292..a1738bcea9 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -277,6 +277,13 @@ class InvenTreeSetting(models.Model): 'validator': bool, }, + 'SEARCH_PREVIEW_RESULTS': { + 'name': _('Search Preview Results'), + 'description': _('Number of results to show in search preview window'), + 'default': 10, + 'validator': [int, MinValueValidator(1)] + }, + 'STOCK_ENABLE_EXPIRY': { 'name': _('Stock Expiry'), 'description': _('Enable stock expiry functionality'), diff --git a/InvenTree/templates/InvenTree/settings/global.html b/InvenTree/templates/InvenTree/settings/global.html index 1a8915f4fb..0c10a13271 100644 --- a/InvenTree/templates/InvenTree/settings/global.html +++ b/InvenTree/templates/InvenTree/settings/global.html @@ -31,4 +31,11 @@ +

    {% trans "Search Settings" %}

    + + + {% include "InvenTree/settings/setting.html" with key="SEARCH_PREVIEW_RESULTS" icon="fa-search" %} + +
    + {% endblock %} diff --git a/InvenTree/templates/base.html b/InvenTree/templates/base.html index 9d4eaa5142..72f20404aa 100644 --- a/InvenTree/templates/base.html +++ b/InvenTree/templates/base.html @@ -142,11 +142,11 @@ - + diff --git a/InvenTree/InvenTree/static/script/inventree/inventree.js b/InvenTree/templates/js/inventree.js similarity index 97% rename from InvenTree/InvenTree/static/script/inventree/inventree.js rename to InvenTree/templates/js/inventree.js index bdcbb82ebd..f1e9472910 100644 --- a/InvenTree/InvenTree/static/script/inventree/inventree.js +++ b/InvenTree/templates/js/inventree.js @@ -1,3 +1,5 @@ +{% load inventree_extras %} + function attachClipboard(selector, containerselector, textElement) { // set container if (containerselector){ @@ -13,7 +15,8 @@ function attachClipboard(selector, containerselector, textElement) { } } else { text = function(trigger) { - var content = trigger.parentElement.parentElement.textContent;return content.trim(); + var content = trigger.parentElement.parentElement.textContent; + return content.trim(); } } @@ -86,7 +89,11 @@ function inventreeDocReady() { source: function (request, response) { $.ajax({ url: '/api/part/', - data: { search: request.term, limit: 4, offset: 0 }, + data: { + search: request.term, + limit: {% settings_value 'SEARCH_PREVIEW_RESULTS' %}, + offset: 0 + }, success: function (data) { var transformed = $.map(data.results, function (el) { return { From dad9239a1c37d43c493ea3c7ee3f4157cb21ce18 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 21 Jul 2021 20:59:55 +1000 Subject: [PATCH 35/62] Add instance-specific filters to API OPTIONS data --- InvenTree/InvenTree/metadata.py | 39 +++++++++++++++++++++++++++++++++ InvenTree/InvenTree/models.py | 11 ++++++++++ InvenTree/part/models.py | 12 ++++++++++ 3 files changed, 62 insertions(+) diff --git a/InvenTree/InvenTree/metadata.py b/InvenTree/InvenTree/metadata.py index c22b39dc43..6cf29ab945 100644 --- a/InvenTree/InvenTree/metadata.py +++ b/InvenTree/InvenTree/metadata.py @@ -32,6 +32,9 @@ class InvenTreeMetadata(SimpleMetadata): def determine_metadata(self, request, view): + self.request = request + self.view = view + metadata = super().determine_metadata(request, view) user = request.user @@ -136,6 +139,42 @@ class InvenTreeMetadata(SimpleMetadata): except AttributeError: pass + # Try to extract 'instance' information + instance = None + + # Extract extra information if an instance is available + if hasattr(serializer, 'instance'): + instance = serializer.instance + + if instance is None: + try: + instance = self.view.get_object() + except: + pass + + if instance is not None: + """ + If there is an instance associated with this API View, + introspect that instance to find any specific API info. + """ + + if hasattr(instance, 'api_instance_filters'): + + instance_filters = instance.api_instance_filters() + + for field_name, field_filters in instance_filters.items(): + + if field_name not in serializer_info.keys(): + # The field might be missing, but is added later on + # This function seems to get called multiple times? + continue + + if 'instance_filters' not in serializer_info[field_name].keys(): + serializer_info[field_name]['instance_filters'] = {} + + for key, value in field_filters.items(): + serializer_info[field_name]['instance_filters'][key] = value + return serializer_info def get_field_info(self, field): diff --git a/InvenTree/InvenTree/models.py b/InvenTree/InvenTree/models.py index 2831a23151..3213838e78 100644 --- a/InvenTree/InvenTree/models.py +++ b/InvenTree/InvenTree/models.py @@ -93,6 +93,17 @@ class InvenTreeTree(MPTTModel): parent: The item immediately above this one. An item with a null parent is a top-level item """ + def api_instance_filters(self): + """ + Instance filters for InvenTreeTree models + """ + + return { + 'parent': { + 'exclude_tree': self.pk, + } + } + def save(self, *args, **kwargs): try: diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 49be12e283..23436a7d6a 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -359,6 +359,18 @@ class Part(MPTTModel): return reverse('api-part-list') + def api_instance_filters(self): + """ + Return API query filters for limiting field results against this instance + """ + + return { + 'variant_of': { + 'exclude_tree': self.pk, + } + } + + def get_context_data(self, request, **kwargs): """ Return some useful context data about this part for template rendering From df48df8119f60f7b4d52e9d2068bab7b4e3e8b2e Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 21 Jul 2021 21:10:31 +1000 Subject: [PATCH 36/62] Catch recursive tree error for part / variant relationship --- InvenTree/part/models.py | 8 +++++++- InvenTree/templates/js/forms.js | 6 ++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 23436a7d6a..d5e039ca77 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -27,6 +27,7 @@ from markdownx.models import MarkdownxField from django_cleanup import cleanup from mptt.models import TreeForeignKey, MPTTModel +from mptt.exceptions import InvalidMove from mptt.managers import TreeManager from stdimage.models import StdImageField @@ -426,7 +427,12 @@ class Part(MPTTModel): self.full_clean() - super().save(*args, **kwargs) + try: + super().save(*args, **kwargs) + except InvalidMove: + raise ValidationError({ + 'variant_of': _('Invalid choice for parent part'), + }) if add_category_templates: # Get part category diff --git a/InvenTree/templates/js/forms.js b/InvenTree/templates/js/forms.js index 587ea07a16..4801ec77eb 100644 --- a/InvenTree/templates/js/forms.js +++ b/InvenTree/templates/js/forms.js @@ -350,6 +350,12 @@ function constructFormBody(fields, options) { for(field in fields) { fields[field].name = field; + + // If any "instance_filters" are defined for the endpoint, copy them across (overwrite) + if (fields[field].instance_filters) { + fields[field].filters = Object.assign(fields[field].filters || {}, fields[field].instance_filters); + } + var field_options = displayed_fields[field]; // Copy custom options across to the fields object From bee0a519ef71b6e778ad56ae5b0e971ceac46104 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 21 Jul 2021 21:18:01 +1000 Subject: [PATCH 37/62] Allow filtering of PartList by exclude_tree --- InvenTree/part/api.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index 39801070c7..527a9395ee 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -644,6 +644,20 @@ class PartList(generics.ListCreateAPIView): except (ValueError, Part.DoesNotExist): pass + # Exclude part variant tree? + exclude_tree = params.get('exclude_tree', None) + + if exclude_tree is not None: + try: + top_level_part = Part.objects.get(pk=exclude_tree) + + queryset = queryset.exclude( + pk__in=[prt.pk for prt in top_level_part.get_descendants(include_self=True)] + ) + + except (ValueError, Part.DoesNotExist): + pass + # Filter by 'ancestor'? ancestor = params.get('ancestor', None) From 85a40ec41877d5f10555cf568395279a4b04c6a8 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 21 Jul 2021 21:23:30 +1000 Subject: [PATCH 38/62] Tree exclusion for PartCategory and StockLocation --- InvenTree/part/api.py | 14 ++++++++++++++ InvenTree/stock/api.py | 14 ++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index 527a9395ee..28e5d4300a 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -105,6 +105,20 @@ class CategoryList(generics.ListCreateAPIView): except (ValueError, PartCategory.DoesNotExist): pass + # Exclude PartCategory tree + exclude_tree = params.get('exclude_tree', None) + + if exclude_tree is not None: + try: + cat = PartCategory.objects.get(pk=exclude_tree) + + queryset = queryset.exclude( + pk__in=[c.pk for c in cat.get_descendants(include_self=True)] + ) + + except (ValueError, PartCategory.DoesNotExist): + pass + return queryset filter_backends = [ diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index 56df35b5c3..ce5e902cff 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -343,6 +343,20 @@ class StockLocationList(generics.ListCreateAPIView): except (ValueError, StockLocation.DoesNotExist): pass + # Exclude StockLocation tree + exclude_tree = params.get('exclude_tree', None) + + if exclude_tree is not None: + try: + loc = StockLocation.objects.get(pk=exclude_tree) + + queryset = queryset.exclude( + pk__in=[l.pk for l in loc.get_descendants(include_self=True)] + ) + + except (ValueError, StockLocation.DoesNotExist): + pass + return queryset filter_backends = [ From 9cf372f633d7be143e4a70da516d4018101599ff Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 21 Jul 2021 21:24:18 +1000 Subject: [PATCH 39/62] PEP fixes --- InvenTree/part/models.py | 1 - InvenTree/stock/api.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index d5e039ca77..4cecd12f17 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -371,7 +371,6 @@ class Part(MPTTModel): } } - def get_context_data(self, request, **kwargs): """ Return some useful context data about this part for template rendering diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index ce5e902cff..b11f556e34 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -351,7 +351,7 @@ class StockLocationList(generics.ListCreateAPIView): loc = StockLocation.objects.get(pk=exclude_tree) queryset = queryset.exclude( - pk__in=[l.pk for l in loc.get_descendants(include_self=True)] + pk__in=[subloc.pk for subloc in loc.get_descendants(include_self=True)] ) except (ValueError, StockLocation.DoesNotExist): From 4ee0004c97e8fdeb05d19ca891376fb5b9fae216 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 21 Jul 2021 21:34:16 +1000 Subject: [PATCH 40/62] Filtering for Build and StockItem --- InvenTree/build/api.py | 15 +++++++++++++++ InvenTree/build/models.py | 8 ++++++++ InvenTree/stock/api.py | 14 ++++++++++++++ InvenTree/stock/models.py | 11 +++++++++++ 4 files changed, 48 insertions(+) diff --git a/InvenTree/build/api.py b/InvenTree/build/api.py index 47edb55545..eb6d42cc6d 100644 --- a/InvenTree/build/api.py +++ b/InvenTree/build/api.py @@ -104,6 +104,21 @@ class BuildList(generics.ListCreateAPIView): params = self.request.query_params + # exclude parent tree + exclude_tree = params.get('exclude_tree', None) + + if exclude_tree is not None: + + try: + build = Build.objects.get(pk=exclude_tree) + + queryset = queryset.exclude( + pk__in=[bld.pk for bld in build.get_descendants(include_self=True)] + ) + + except (ValueError, Build.DoesNotExist): + pass + # Filter by "parent" parent = params.get('parent', None) diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index 5f8af9096b..084f9ab2db 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -96,6 +96,14 @@ class Build(MPTTModel): def get_api_url(): return reverse('api-build-list') + def api_instance_filters(self): + + return { + 'parent': { + 'exclude_tree': self.pk, + } + } + def save(self, *args, **kwargs): try: diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index b11f556e34..cf58c7d4d9 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -733,6 +733,20 @@ class StockList(generics.ListCreateAPIView): if customer: queryset = queryset.filter(customer=customer) + # Exclude stock item tree + exclude_tree = params.get('exclude_tree', None) + + if exclude_tree is not None: + try: + item = StockItem.objects.get(pk=exclude_tree) + + queryset = queryset.exclude( + pk__in=[it.pk for it in item.get_descendants(include_self=True)] + ) + + except (ValueError, StockItem.DoesNotExist): + pass + # Filter by 'allocated' parts? allocated = params.get('allocated', None) diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index c2122f40ac..11d5de9bf1 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -191,6 +191,17 @@ class StockItem(MPTTModel): def get_api_url(): return reverse('api-stock-list') + def api_instance_filters(self): + """ + Custom API instance filters + """ + + return { + 'parent': { + 'exclude_tree': self.pk, + } + } + # A Query filter which will be re-used in multiple places to determine if a StockItem is actually "in stock" IN_STOCK_FILTER = Q( quantity__gt=0, From afde997cf9321c4347c0261c578536c89689896b Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 21 Jul 2021 22:05:52 +1000 Subject: [PATCH 41/62] Expose part parameters to Part label templates --- InvenTree/label/models.py | 1 + InvenTree/part/models.py | 17 +++++++++++++++++ InvenTree/report/models.py | 1 + 3 files changed, 19 insertions(+) diff --git a/InvenTree/label/models.py b/InvenTree/label/models.py index b558f10e73..f8cadb2c82 100644 --- a/InvenTree/label/models.py +++ b/InvenTree/label/models.py @@ -398,4 +398,5 @@ class PartLabel(LabelTemplate): 'revision': part.revision, 'qr_data': part.format_barcode(brief=True), 'qr_url': part.format_barcode(url=True, request=request), + 'parameters': part.parameters_map(), } diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 4cecd12f17..2dd5d3ad7f 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -1904,6 +1904,23 @@ class Part(MPTTModel): return self.parameters.order_by('template__name') + def parameters_map(self): + """ + Return a map (dict) of parameter values assocaited with this Part instance, + of the form: + { + "name_1": "value_1", + "name_2": "value_2", + } + """ + + params = {} + + for parameter in self.parameters.all(): + params[parameter.template.name] = parameter.data + + return params + @property def has_variants(self): """ Check if this Part object has variants underneath it. """ diff --git a/InvenTree/report/models.py b/InvenTree/report/models.py index db06e1c95b..21604dbc94 100644 --- a/InvenTree/report/models.py +++ b/InvenTree/report/models.py @@ -356,6 +356,7 @@ class TestReport(ReportTemplateBase): 'stock_item': stock_item, 'serial': stock_item.serial, 'part': stock_item.part, + 'parameters': stock_item.part.parameters_map(), 'results': stock_item.testResultMap(include_installed=self.include_installed), 'result_list': stock_item.testResultList(include_installed=self.include_installed), 'installed_items': stock_item.get_installed_items(cascade=True), From 964672d6cca670f020c72f559c321de777afa075 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 21 Jul 2021 22:14:03 +1000 Subject: [PATCH 42/62] Add parameters to template --- InvenTree/label/models.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/InvenTree/label/models.py b/InvenTree/label/models.py index f8cadb2c82..dea740f2b1 100644 --- a/InvenTree/label/models.py +++ b/InvenTree/label/models.py @@ -296,7 +296,9 @@ class StockItemLabel(LabelTemplate): 'uid': stock_item.uid, 'qr_data': stock_item.format_barcode(brief=True), 'qr_url': stock_item.format_barcode(url=True, request=request), - 'tests': stock_item.testResultMap() + 'tests': stock_item.testResultMap(), + 'parameters': stock_item.part.parameters_map(), + } From c0d6ef80fc66367fdf018c4b61ce6fa71e426192 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 22 Jul 2021 23:39:19 +0200 Subject: [PATCH 43/62] unique model settings --- ...usersetting.py => 0011_auto_20210722_2114.py} | 8 ++++++-- InvenTree/common/models.py | 16 ++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) rename InvenTree/common/migrations/{0011_inventreeusersetting.py => 0011_auto_20210722_2114.py} (78%) diff --git a/InvenTree/common/migrations/0011_inventreeusersetting.py b/InvenTree/common/migrations/0011_auto_20210722_2114.py similarity index 78% rename from InvenTree/common/migrations/0011_inventreeusersetting.py rename to InvenTree/common/migrations/0011_auto_20210722_2114.py index 0bec691041..388117f261 100644 --- a/InvenTree/common/migrations/0011_inventreeusersetting.py +++ b/InvenTree/common/migrations/0011_auto_20210722_2114.py @@ -1,4 +1,4 @@ -# Generated by Django 3.2.4 on 2021-07-19 22:57 +# Generated by Django 3.2.4 on 2021-07-22 21:14 from django.conf import settings from django.db import migrations, models @@ -17,8 +17,8 @@ class Migration(migrations.Migration): name='InvenTreeUserSetting', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('key', models.CharField(help_text='Settings key (must be unique - case insensitive', max_length=50, unique=True)), ('value', models.CharField(blank=True, help_text='Settings value', max_length=200)), + ('key', models.CharField(help_text='Settings key (must be unique - case insensitive', max_length=50)), ('user', models.ForeignKey(blank=True, help_text='User', null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='User')), ], options={ @@ -26,4 +26,8 @@ class Migration(migrations.Migration): 'verbose_name_plural': 'InvenTree User Settings', }, ), + migrations.AddConstraint( + model_name='inventreeusersetting', + constraint=models.UniqueConstraint(fields=('key', 'user'), name='unique key and user'), + ), ] diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index 8907299a44..bdaa73b142 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -745,6 +745,12 @@ class InvenTreeSetting(BaseInvenTreeSetting): verbose_name = "InvenTree Setting" verbose_name_plural = "InvenTree Settings" + key = models.CharField( + max_length=50, + blank=False, + unique=True, + help_text=_('Settings key (must be unique - case insensitive'), + ) class InvenTreeUserSetting(BaseInvenTreeSetting): """ @@ -763,6 +769,16 @@ class InvenTreeUserSetting(BaseInvenTreeSetting): class Meta: verbose_name = "InvenTree User Setting" verbose_name_plural = "InvenTree User Settings" + constraints = [ + models.UniqueConstraint(fields=['key', 'user'], name='unique key and user') + ] + + key = models.CharField( + max_length=50, + blank=False, + unique=False, + help_text=_('Settings key (must be unique - case insensitive'), + ) user = models.ForeignKey( User, From 69ff0ac248ef475594c6d743e703acf81853b7a8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 22 Jul 2021 23:39:36 +0200 Subject: [PATCH 44/62] ruleset --- InvenTree/users/models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/InvenTree/users/models.py b/InvenTree/users/models.py index 73bc5b6695..179a70ed74 100644 --- a/InvenTree/users/models.py +++ b/InvenTree/users/models.py @@ -141,6 +141,7 @@ class RuleSet(models.Model): # Models which currently do not require permissions 'common_colortheme', 'common_inventreesetting', + 'common_inventreeusersetting', 'company_contact', 'users_owner', From 8f374e255e13e92e76abbe61082908a610aa6116 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 22 Jul 2021 23:43:03 +0200 Subject: [PATCH 45/62] abstract filters and refactor --- InvenTree/common/models.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index bdaa73b142..dca371a671 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -145,7 +145,11 @@ class BaseInvenTreeSetting(models.Model): return choices @classmethod - def get_setting_object(cls, key): + def get_filters(cls, key, **kwargs): + return {'key__iexact': key} + + @classmethod + def get_setting_object(cls, key, **kwargs): """ Return an InvenTreeSetting object matching the given key. @@ -156,7 +160,7 @@ class BaseInvenTreeSetting(models.Model): key = str(key).strip().upper() try: - setting = cls.objects.filter(key__iexact=key).first() + setting = cls.objects.filter(**cls.get_filters(key, **kwargs)).first() except (ValueError, cls.DoesNotExist): setting = None except (IntegrityError, OperationalError): @@ -165,7 +169,7 @@ class BaseInvenTreeSetting(models.Model): # Setting does not exist! (Try to create it) if not setting: - setting = cls(key=key, value=cls.get_setting_default(key)) + setting = cls(key=key, value=cls.get_setting_default(key), **kwargs) try: # Wrap this statement in "atomic", so it can be rolled back if it fails @@ -224,7 +228,7 @@ class BaseInvenTreeSetting(models.Model): return value @classmethod - def set_setting(cls, key, value, user, create=True): + def set_setting(cls, key, value, change_user, create=True, **kwargs): """ Set the value of a particular setting. If it does not exist, option to create it. @@ -232,19 +236,19 @@ class BaseInvenTreeSetting(models.Model): Args: key: settings key value: New value - user: User object (must be staff member to update a core setting) + change_user: User object (must be staff member to update a core setting) create: If True, create a new setting if the specified key does not exist. """ - if user is not None and not user.is_staff: + if change_user is not None and not change_user.is_staff: return try: - setting = cls.objects.get(key__iexact=key) + setting = cls.objects.get(**cls.get_filters(key, **kwargs)) except cls.DoesNotExist: if create: - setting = cls(key=key) + setting = cls(key=key, **kwargs) else: return @@ -338,7 +342,7 @@ class BaseInvenTreeSetting(models.Model): # We can accept function validators with a single argument validator(self.value) - def validate_unique(self, exclude=None): + def validate_unique(self, exclude=None, **kwargs): """ Ensure that the key:value pair is unique. In addition to the base validators, this ensures that the 'key' is unique, using a case-insensitive comparison. @@ -347,7 +351,7 @@ class BaseInvenTreeSetting(models.Model): super().validate_unique(exclude) try: - setting = self.__class__.objects.exclude(id=self.id).filter(key__iexact=self.key) + setting = self.__class__.objects.exclude(id=self.id).filter(**self.get_filters(self.key, **kwargs)) if setting.exists(): raise ValidationError({'key': _('Key string must be unique')}) except self.DoesNotExist: From 6f5fc528b7e0ff4f3edf5883070f0eec29c6e4f9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 22 Jul 2021 23:43:51 +0200 Subject: [PATCH 46/62] override functions --- InvenTree/common/models.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index dca371a671..7072bc86d4 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -792,6 +792,17 @@ class InvenTreeUserSetting(BaseInvenTreeSetting): help_text=_('User'), ) + @classmethod + def get_setting_object(cls, key, user): + return super().get_setting_object(key, user=user) + + def validate_unique(self, exclude=None): + return super().validate_unique(exclude=exclude, user=self.user) + + @classmethod + def get_filters(cls, key, **kwargs): + return {'key__iexact': key, 'user__id__iexact': kwargs['user'].id} + class PriceBreak(models.Model): """ From 7ef87320a047a3610d4ca83dca56cfc5e21bc6c8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 22 Jul 2021 23:44:25 +0200 Subject: [PATCH 47/62] abstract edit --- InvenTree/common/views.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/InvenTree/common/views.py b/InvenTree/common/views.py index f953dffa81..f182b03e0d 100644 --- a/InvenTree/common/views.py +++ b/InvenTree/common/views.py @@ -45,8 +45,8 @@ class SettingEdit(AjaxUpdateView): ctx['key'] = setting.key ctx['value'] = setting.value - ctx['name'] = models.InvenTreeSetting.get_setting_name(setting.key) - ctx['description'] = models.InvenTreeSetting.get_setting_description(setting.key) + ctx['name'] = self.model.get_setting_name(setting.key) + ctx['description'] = self.model.get_setting_description(setting.key) return ctx @@ -69,12 +69,12 @@ class SettingEdit(AjaxUpdateView): self.object.value = str2bool(setting.value) form.fields['value'].value = str2bool(setting.value) - name = models.InvenTreeSetting.get_setting_name(setting.key) + name = self.model.get_setting_name(setting.key) if name: form.fields['value'].label = name - description = models.InvenTreeSetting.get_setting_description(setting.key) + description = self.model.get_setting_description(setting.key) if description: form.fields['value'].help_text = description From 449fc329c971e290f1cd7efc021c11be7de507a4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 22 Jul 2021 23:45:34 +0200 Subject: [PATCH 48/62] usersetting edit url --- InvenTree/InvenTree/urls.py | 3 ++- InvenTree/common/views.py | 12 ++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index a3af143f92..a4a4d21807 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -43,7 +43,7 @@ from .views import CurrencySettingsView, CurrencyRefreshView from .views import AppearanceSelectView, SettingCategorySelectView from .views import DynamicJsView -from common.views import SettingEdit +from common.views import SettingEdit, UserSettingEdit from .api import InfoView, NotFoundView from .api import ActionPluginView @@ -94,6 +94,7 @@ settings_urls = [ url(r'^currencies/', CurrencySettingsView.as_view(), name='settings-currencies'), url(r'^currencies-refresh/', CurrencyRefreshView.as_view(), name='settings-currencies-refresh'), + url(r'^(?P\d+)/edit/user', UserSettingEdit.as_view(), name='user-setting-edit'), url(r'^(?P\d+)/edit/', SettingEdit.as_view(), name='setting-edit'), # Catch any other urls diff --git a/InvenTree/common/views.py b/InvenTree/common/views.py index f182b03e0d..75fc78b4e3 100644 --- a/InvenTree/common/views.py +++ b/InvenTree/common/views.py @@ -111,6 +111,18 @@ class SettingEdit(AjaxUpdateView): form.add_error('value', _('Supplied value must be a boolean')) +class UserSettingEdit(SettingEdit): + """ + View for editing an InvenTree key:value user settings object, + (or creating it if the key does not already exist) + """ + + model = models.InvenTreeUserSetting + ajax_form_title = _('Change User Setting') + form_class = forms.SettingEditForm + ajax_template_name = "common/edit_setting.html" + + class MultiStepFormView(SessionWizardView): """ Setup basic methods of multi-step form From e287860e102491b3f5c789fc53cd83eeb5e66a66 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 22 Jul 2021 23:46:31 +0200 Subject: [PATCH 49/62] admin for user setting --- InvenTree/common/admin.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/InvenTree/common/admin.py b/InvenTree/common/admin.py index c2da1ddd63..1eda18e869 100644 --- a/InvenTree/common/admin.py +++ b/InvenTree/common/admin.py @@ -5,7 +5,7 @@ from django.contrib import admin from import_export.admin import ImportExportModelAdmin -from .models import InvenTreeSetting +from .models import InvenTreeSetting, InvenTreeUserSetting class SettingsAdmin(ImportExportModelAdmin): @@ -13,4 +13,10 @@ class SettingsAdmin(ImportExportModelAdmin): list_display = ('key', 'value') +class UserSettingsAdmin(ImportExportModelAdmin): + + list_display = ('key', 'value', 'user', ) + + admin.site.register(InvenTreeSetting, SettingsAdmin) +admin.site.register(InvenTreeUserSetting, UserSettingsAdmin) From 3f6c7df7a8e6aa3635df6c053634ad676618270a Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 22 Jul 2021 23:48:28 +0200 Subject: [PATCH 50/62] change template setting behaviour for user setting --- InvenTree/part/templatetags/inventree_extras.py | 9 +++++---- InvenTree/templates/InvenTree/settings/setting.html | 9 +++++++-- InvenTree/templates/InvenTree/settings/settings.html | 7 ++++++- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/InvenTree/part/templatetags/inventree_extras.py b/InvenTree/part/templatetags/inventree_extras.py index 38689df26b..8c28d997f6 100644 --- a/InvenTree/part/templatetags/inventree_extras.py +++ b/InvenTree/part/templatetags/inventree_extras.py @@ -18,7 +18,7 @@ from InvenTree import version, settings import InvenTree.helpers -from common.models import InvenTreeSetting, ColorTheme +from common.models import InvenTreeSetting, ColorTheme, InvenTreeUserSetting from common.settings import currency_code_default register = template.Library() @@ -182,11 +182,12 @@ def setting_object(key, *args, **kwargs): """ Return a setting object speciifed by the given key (Or return None if the setting does not exist) + if a user-setting was requested return that """ - setting = InvenTreeSetting.get_setting_object(key) - - return setting + if 'user' in kwargs: + return InvenTreeUserSetting.get_setting_object(key, user=kwargs['user']) + return InvenTreeSetting.get_setting_object(key) @register.simple_tag() diff --git a/InvenTree/templates/InvenTree/settings/setting.html b/InvenTree/templates/InvenTree/settings/setting.html index 7376adf8d9..66f3f9f3b0 100644 --- a/InvenTree/templates/InvenTree/settings/setting.html +++ b/InvenTree/templates/InvenTree/settings/setting.html @@ -1,7 +1,12 @@ {% load inventree_extras %} {% load i18n %} -{% setting_object key as setting %} +{% if user_setting %} + {% setting_object key user=request.user as setting %} +{% else %} + {% setting_object key as setting %} +{% endif %} + {% if icon %} @@ -28,7 +33,7 @@
    -
    diff --git a/InvenTree/templates/InvenTree/settings/settings.html b/InvenTree/templates/InvenTree/settings/settings.html index 25ae5d24b2..03cf276594 100644 --- a/InvenTree/templates/InvenTree/settings/settings.html +++ b/InvenTree/templates/InvenTree/settings/settings.html @@ -45,9 +45,14 @@ $('table').find('.btn-edit-setting').click(function() { var setting = $(this).attr('setting'); var pk = $(this).attr('pk'); + var url = `/settings/${pk}/edit/`; + + if ($(this).attr('user')){ + url += `user/`; + } launchModalForm( - `/settings/${pk}/edit/`, + url, { reload: true, } From 5f2bef7ee16a58f72f0bb43310c076675a5752a9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 22 Jul 2021 23:50:09 +0200 Subject: [PATCH 51/62] base implementation of user setting --- InvenTree/common/models.py | 8 ++++---- InvenTree/templates/InvenTree/settings/appearance.html | 8 ++++++++ 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index 7072bc86d4..2e8678b600 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -762,10 +762,10 @@ class InvenTreeUserSetting(BaseInvenTreeSetting): """ GLOBAL_SETTINGS = { - 'PART_ASSEMBLY': { - 'name': _('Assembly'), - 'description': _('Parts can be assembled from other components by default'), - 'default': False, + 'HOMEPAGE_PART_STARRED': { + 'name': _('Show starred parts'), + 'description': _('Show starred parts on the homepage'), + 'default': True, 'validator': bool, }, } diff --git a/InvenTree/templates/InvenTree/settings/appearance.html b/InvenTree/templates/InvenTree/settings/appearance.html index 8cd7a8d2db..6f4f406c62 100644 --- a/InvenTree/templates/InvenTree/settings/appearance.html +++ b/InvenTree/templates/InvenTree/settings/appearance.html @@ -63,5 +63,13 @@ +

    {% trans "User Settings" %}

    + + + {% include "InvenTree/settings/header.html" %} + + {% include "InvenTree/settings/setting.html" with key="HOMEPAGE_PART_STARRED" user_setting=True %} + +
    {% endblock %} From 3b12b0231e77f4a366032abe0e382415e01c2af1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 22 Jul 2021 23:50:51 +0200 Subject: [PATCH 52/62] fixing wired unique behaviour --- InvenTree/common/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index 2e8678b600..a07bb1c43f 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -259,7 +259,7 @@ class BaseInvenTreeSetting(models.Model): setting.value = str(value) setting.save() - key = models.CharField(max_length=50, blank=False, unique=True, help_text=_('Settings key (must be unique - case insensitive')) + key = models.CharField(max_length=50, blank=False, unique=False, help_text=_('Settings key (must be unique - case insensitive')) value = models.CharField(max_length=200, blank=True, unique=False, help_text=_('Settings value')) From 7abf70fdd7095695cbe0d8dd3e15e50051008e24 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 22 Jul 2021 23:53:17 +0200 Subject: [PATCH 53/62] style fix --- InvenTree/common/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index a07bb1c43f..0b5f1cabef 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -35,7 +35,6 @@ class BaseInvenTreeSetting(models.Model): single values (e.g. one-off settings values). """ - GLOBAL_SETTINGS = {} class Meta: @@ -756,6 +755,7 @@ class InvenTreeSetting(BaseInvenTreeSetting): help_text=_('Settings key (must be unique - case insensitive'), ) + class InvenTreeUserSetting(BaseInvenTreeSetting): """ An InvenTreeSetting object with a usercontext From e167f27258a8f4fce34ca74e53809bb431f163da Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 23 Jul 2021 00:46:48 +0200 Subject: [PATCH 54/62] get user settings in templates --- InvenTree/common/models.py | 4 ++-- InvenTree/part/templatetags/inventree_extras.py | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index eebb566a68..48c99cbbb5 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -194,7 +194,7 @@ class BaseInvenTreeSetting(models.Model): return None @classmethod - def get_setting(cls, key, backup_value=None): + def get_setting(cls, key, backup_value=None, **kwargs): """ Get the value of a particular setting. If it does not exist, return the backup value (default = None) @@ -204,7 +204,7 @@ class BaseInvenTreeSetting(models.Model): if backup_value is None: backup_value = cls.get_setting_default(key) - setting = cls.get_setting_object(key) + setting = cls.get_setting_object(key, **kwargs) if setting: value = setting.value diff --git a/InvenTree/part/templatetags/inventree_extras.py b/InvenTree/part/templatetags/inventree_extras.py index 8c28d997f6..e867441b42 100644 --- a/InvenTree/part/templatetags/inventree_extras.py +++ b/InvenTree/part/templatetags/inventree_extras.py @@ -196,6 +196,8 @@ def settings_value(key, *args, **kwargs): Return a settings value specified by the given key """ + if 'user' in kwargs: + return InvenTreeUserSetting.get_setting(key, user=kwargs['user']) return InvenTreeSetting.get_setting(key) From 46b0db826318538c23de86eb96c91ef61d6c1350 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 23 Jul 2021 01:07:28 +0200 Subject: [PATCH 55/62] more hompage settings --- InvenTree/common/models.py | 12 ++++++++ InvenTree/templates/InvenTree/index.html | 29 ++++++++++++------- .../InvenTree/settings/appearance.html | 3 ++ 3 files changed, 33 insertions(+), 11 deletions(-) diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index 48c99cbbb5..3ea2e11a44 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -773,6 +773,18 @@ class InvenTreeUserSetting(BaseInvenTreeSetting): 'default': True, 'validator': bool, }, + 'HOMEPAGE_PART_LATEST': { + 'name': _('Show latest parts'), + 'description': _('Show latest parts on the homepage'), + 'default': True, + 'validator': bool, + }, + 'HOMEPAGE_BOM_VALIDATION': { + 'name': _('Show starunvalidated BOMs'), + 'description': _('Show BOMs that await validation on the homepage'), + 'default': True, + 'validator': bool, + }, } class Meta: diff --git a/InvenTree/templates/InvenTree/index.html b/InvenTree/templates/InvenTree/index.html index a3d793dd26..aa5272760f 100644 --- a/InvenTree/templates/InvenTree/index.html +++ b/InvenTree/templates/InvenTree/index.html @@ -95,11 +95,21 @@ function addHeaderAction(label, title, icon, options) { {% if roles.part.view %} addHeaderTitle('{% trans "Parts" %}'); + +{% settings_value 'HOMEPAGE_PART_STARRED' user=request.user as setting_part_starred %} +{% if setting_part_starred %} addHeaderAction('starred-parts', '{% trans "Starred Parts" %}', 'fa-star'); +loadSimplePartTable("#table-starred-parts", "{% url 'api-part-list' %}", { + params: { + "starred": true, + }, + name: 'starred_parts', +}); +{% endif %} + +{% settings_value 'HOMEPAGE_PART_LATEST' user=request.user as setting_part_latest %} +{% if setting_part_latest %} addHeaderAction('latest-parts', '{% trans "Latest Parts" %}', 'fa-newspaper'); -addHeaderAction('bom-validation', '{% trans "BOM Waiting Validation" %}', 'fa-times-circle'); - - loadSimplePartTable("#table-latest-parts", "{% url 'api-part-list' %}", { params: { ordering: "-creation_date", @@ -107,21 +117,18 @@ loadSimplePartTable("#table-latest-parts", "{% url 'api-part-list' %}", { }, name: 'latest_parts', }); +{% endif %} -loadSimplePartTable("#table-starred-parts", "{% url 'api-part-list' %}", { - params: { - "starred": true, - }, - name: 'starred_parts', -}); - +{% settings_value 'HOMEPAGE_BOM_VALIDATION' user=request.user as setting_bom_validation %} +{% if setting_bom_validation %} +addHeaderAction('bom-validation', '{% trans "BOM Waiting Validation" %}', 'fa-times-circle'); loadSimplePartTable("#table-bom-validation", "{% url 'api-part-list' %}", { params: { "bom_valid": false, }, name: 'bom_invalid_parts', }); - +{% endif %} {% endif %} {% if roles.stock.view %} diff --git a/InvenTree/templates/InvenTree/settings/appearance.html b/InvenTree/templates/InvenTree/settings/appearance.html index 6f4f406c62..43f1242cbf 100644 --- a/InvenTree/templates/InvenTree/settings/appearance.html +++ b/InvenTree/templates/InvenTree/settings/appearance.html @@ -69,6 +69,9 @@ {% include "InvenTree/settings/header.html" %} {% include "InvenTree/settings/setting.html" with key="HOMEPAGE_PART_STARRED" user_setting=True %} + {% include "InvenTree/settings/setting.html" with key="HOMEPAGE_PART_LATEST" user_setting=True %} + {% include "InvenTree/settings/setting.html" with key="HOMEPAGE_BOM_VALIDATION" user_setting=True %} + From e97ee95debdc00d1a85e69ca1891e986a0cfa03a Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 23 Jul 2021 01:09:06 +0200 Subject: [PATCH 56/62] typo --- InvenTree/common/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index 3ea2e11a44..faa090d49b 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -780,7 +780,7 @@ class InvenTreeUserSetting(BaseInvenTreeSetting): 'validator': bool, }, 'HOMEPAGE_BOM_VALIDATION': { - 'name': _('Show starunvalidated BOMs'), + 'name': _('Show unvalidated BOMs'), 'description': _('Show BOMs that await validation on the homepage'), 'default': True, 'validator': bool, From 32eace0c36e110bd7d68faa2b17f4ee35ae82a7b Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 23 Jul 2021 11:05:41 +0200 Subject: [PATCH 57/62] moving settings into own section --- InvenTree/InvenTree/urls.py | 1 + .../InvenTree/settings/appearance.html | 13 ---------- .../templates/InvenTree/settings/tabs.html | 3 +++ .../InvenTree/settings/user_settings.html | 25 +++++++++++++++++++ 4 files changed, 29 insertions(+), 13 deletions(-) create mode 100644 InvenTree/templates/InvenTree/settings/user_settings.html diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index 029bf597f2..3cc50bc889 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -79,6 +79,7 @@ apipatterns = [ settings_urls = [ + url(r'^usersettings/', SettingsView.as_view(template_name='InvenTree/settings/user_settings.html'), name='settings-user-settings'), url(r'^user/?', SettingsView.as_view(template_name='InvenTree/settings/user.html'), name='settings-user'), url(r'^appearance/?', AppearanceSelectView.as_view(), name='settings-appearance'), url(r'^i18n/?', include('django.conf.urls.i18n')), diff --git a/InvenTree/templates/InvenTree/settings/appearance.html b/InvenTree/templates/InvenTree/settings/appearance.html index 43f1242cbf..d0b414e423 100644 --- a/InvenTree/templates/InvenTree/settings/appearance.html +++ b/InvenTree/templates/InvenTree/settings/appearance.html @@ -62,17 +62,4 @@ - -

    {% trans "User Settings" %}

    - - - {% include "InvenTree/settings/header.html" %} - - {% include "InvenTree/settings/setting.html" with key="HOMEPAGE_PART_STARRED" user_setting=True %} - {% include "InvenTree/settings/setting.html" with key="HOMEPAGE_PART_LATEST" user_setting=True %} - {% include "InvenTree/settings/setting.html" with key="HOMEPAGE_BOM_VALIDATION" user_setting=True %} - - -
    - {% endblock %} diff --git a/InvenTree/templates/InvenTree/settings/tabs.html b/InvenTree/templates/InvenTree/settings/tabs.html index 86a88b68f0..0e5554ca3e 100644 --- a/InvenTree/templates/InvenTree/settings/tabs.html +++ b/InvenTree/templates/InvenTree/settings/tabs.html @@ -8,6 +8,9 @@ {% trans "Appearance" %}
  • + + {% trans "User Settings" %} + {% if user.is_staff %}

    {% trans "InvenTree Settings" %}

    diff --git a/InvenTree/templates/InvenTree/settings/user_settings.html b/InvenTree/templates/InvenTree/settings/user_settings.html new file mode 100644 index 0000000000..d56cb5b35a --- /dev/null +++ b/InvenTree/templates/InvenTree/settings/user_settings.html @@ -0,0 +1,25 @@ +{% extends "InvenTree/settings/settings.html" %} +{% load i18n %} +{% load inventree_extras %} + +{% block tabs %} +{% include "InvenTree/settings/tabs.html" with tab='user_settings' %} +{% endblock %} + +{% block subtitle %} +{% trans "User Settings" %} +{% endblock %} + +{% block settings %} + +
    + + {% include "InvenTree/settings/header.html" %} + + {% include "InvenTree/settings/setting.html" with key="HOMEPAGE_PART_STARRED" user_setting=True %} + {% include "InvenTree/settings/setting.html" with key="HOMEPAGE_PART_LATEST" user_setting=True %} + {% include "InvenTree/settings/setting.html" with key="HOMEPAGE_BOM_VALIDATION" user_setting=True %} + +
    +
    +{% endblock %} From 31050f23aaa51c77aa764b632d4fd9684c2f5b5c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 24 Jul 2021 00:08:46 +0200 Subject: [PATCH 58/62] adding all homepage settings --- InvenTree/common/models.py | 72 ++++++++++++ InvenTree/templates/InvenTree/index.html | 109 ++++++++++++------ .../InvenTree/settings/user_settings.html | 16 +++ 3 files changed, 164 insertions(+), 33 deletions(-) diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index faa090d49b..b97bb539e3 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -785,6 +785,78 @@ class InvenTreeUserSetting(BaseInvenTreeSetting): 'default': True, 'validator': bool, }, + 'HOMEPAGE_STOCK_RECENT': { + 'name': _('Show recent stock changes'), + 'description': _('Show recently changed stock items on the homepage'), + 'default': True, + 'validator': bool, + }, + 'HOMEPAGE_STOCK_LOW': { + 'name': _('Show low stock'), + 'description': _('Show low stock items on the homepage'), + 'default': True, + 'validator': bool, + }, + 'HOMEPAGE_STOCK_DEPLETED': { + 'name': _('Show depleted stock'), + 'description': _('Show depleted stock items on the homepage'), + 'default': True, + 'validator': bool, + }, + 'HOMEPAGE_STOCK_NEEDED': { + 'name': _('Show needed stock'), + 'description': _('Show stock items needed for builds on the homepage'), + 'default': True, + 'validator': bool, + }, + 'HOMEPAGE_STOCK_EXPIRED': { + 'name': _('Show expired stock'), + 'description': _('Show expired stock items on the homepage'), + 'default': True, + 'validator': bool, + }, + 'HOMEPAGE_STOCK_STALE': { + 'name': _('Show stale stock'), + 'description': _('Show stale stock items on the homepage'), + 'default': True, + 'validator': bool, + }, + 'HOMEPAGE_BUILD_PENDING': { + 'name': _('Show pending builds'), + 'description': _('Show pending builds on the homepage'), + 'default': True, + 'validator': bool, + }, + 'HOMEPAGE_BUILD_OVERDUE': { + 'name': _('Show overdue builds'), + 'description': _('Show overdue builds on the homepage'), + 'default': True, + 'validator': bool, + }, + 'HOMEPAGE_PO_OUTSTANDING': { + 'name': _('Show outstanding POs'), + 'description': _('Show outstanding POs on the homepage'), + 'default': True, + 'validator': bool, + }, + 'HOMEPAGE_PO_OVERDUE': { + 'name': _('Show overdue POs'), + 'description': _('Show overdue POs on the homepage'), + 'default': True, + 'validator': bool, + }, + 'HOMEPAGE_SO_OUTSTANDING': { + 'name': _('Show outstanding SOs'), + 'description': _('Show outstanding SOs on the homepage'), + 'default': True, + 'validator': bool, + }, + 'HOMEPAGE_SO_OVERDUE': { + 'name': _('Show overdue SOs'), + 'description': _('Show overdue SOs on the homepage'), + 'default': True, + 'validator': bool, + }, } class Meta: diff --git a/InvenTree/templates/InvenTree/index.html b/InvenTree/templates/InvenTree/index.html index aa5272760f..65034a8ac8 100644 --- a/InvenTree/templates/InvenTree/index.html +++ b/InvenTree/templates/InvenTree/index.html @@ -93,6 +93,7 @@ function addHeaderAction(label, title, icon, options) { }); } + {% if roles.part.view %} addHeaderTitle('{% trans "Parts" %}'); @@ -131,13 +132,13 @@ loadSimplePartTable("#table-bom-validation", "{% url 'api-part-list' %}", { {% endif %} {% endif %} + {% if roles.stock.view %} addHeaderTitle('{% trans "Stock" %}'); -addHeaderAction('recently-updated-stock', '{% trans "Recently Updated" %}', 'fa-clock'); -addHeaderAction('low-stock', '{% trans "Low Stock" %}', 'fa-shopping-cart'); -addHeaderAction('depleted-stock', '{% trans "Depleted Stock" %}', 'fa-times'); -addHeaderAction('stock-to-build', '{% trans "Required for Build Orders" %}', 'fa-bullhorn'); +{% settings_value 'HOMEPAGE_STOCK_RECENT' user=request.user as setting_stock_recent %} +{% if setting_stock_recent %} +addHeaderAction('recently-updated-stock', '{% trans "Recently Updated" %}', 'fa-clock'); loadStockTable($('#table-recently-updated-stock'), { params: { part_detail: true, @@ -147,12 +148,48 @@ loadStockTable($('#table-recently-updated-stock'), { name: 'recently-updated-stock', grouping: false, }); +{% endif %} + +{% settings_value 'HOMEPAGE_STOCK_LOW' user=request.user as setting_stock_low %} +{% if setting_stock_low %} +addHeaderAction('low-stock', '{% trans "Low Stock" %}', 'fa-shopping-cart'); +loadSimplePartTable("#table-low-stock", "{% url 'api-part-list' %}", { + params: { + low_stock: true, + }, + name: "low_stock_parts", +}); +{% endif %} + +{% settings_value 'HOMEPAGE_STOCK_DEPLETED' user=request.user as setting_stock_depleted %} +{% if setting_stock_depleted %} +addHeaderAction('depleted-stock', '{% trans "Depleted Stock" %}', 'fa-times'); +loadSimplePartTable("#table-depleted-stock", "{% url 'api-part-list' %}", { + params: { + depleted_stock: true, + }, + name: "depleted_stock_parts", +}); +{% endif %} + +{% settings_value 'HOMEPAGE_STOCK_NEEDED' user=request.user as setting_stock_needed %} +{% if setting_stock_needed %} +addHeaderAction('stock-to-build', '{% trans "Required for Build Orders" %}', 'fa-bullhorn'); +loadSimplePartTable("#table-stock-to-build", "{% url 'api-part-list' %}", { + params: { + stock_to_build: true, + }, + name: "to_build_parts", +}); +{% endif %} + {% settings_value "STOCK_ENABLE_EXPIRY" as expiry %} {% if expiry %} -addHeaderAction('expired-stock', '{% trans "Expired Stock" %}', 'fa-calendar-times'); -addHeaderAction('stale-stock', '{% trans "Stale Stock" %}', 'fa-stopwatch'); +{% settings_value 'HOMEPAGE_STOCK_EXPIRED' user=request.user as setting_stock_expired %} +{% if setting_stock_expired %} +addHeaderAction('expired-stock', '{% trans "Expired Stock" %}', 'fa-calendar-times'); loadStockTable($("#table-expired-stock"), { params: { expired: true, @@ -160,7 +197,11 @@ loadStockTable($("#table-expired-stock"), { part_detail: true, }, }); +{% endif %} +{% settings_value 'HOMEPAGE_STOCK_STALE' user=request.user as setting_stock_stale %} +{% if setting_stock_stale %} +addHeaderAction('stale-stock', '{% trans "Stale Stock" %}', 'fa-stopwatch'); loadStockTable($("#table-stale-stock"), { params: { stale: true, @@ -171,34 +212,16 @@ loadStockTable($("#table-stale-stock"), { }); {% endif %} -loadSimplePartTable("#table-low-stock", "{% url 'api-part-list' %}", { - params: { - low_stock: true, - }, - name: "low_stock_parts", -}); - -loadSimplePartTable("#table-depleted-stock", "{% url 'api-part-list' %}", { - params: { - depleted_stock: true, - }, - name: "depleted_stock_parts", -}); - -loadSimplePartTable("#table-stock-to-build", "{% url 'api-part-list' %}", { - params: { - stock_to_build: true, - }, - name: "to_build_parts", -}); - {% endif %} +{% endif %} + {% if roles.build.view %} addHeaderTitle('{% trans "Build Orders" %}'); -addHeaderAction('build-pending', '{% trans "Build Orders In Progress" %}', 'fa-cogs'); -addHeaderAction('build-overdue', '{% trans "Overdue Build Orders" %}', 'fa-calendar-times'); +{% settings_value 'HOMEPAGE_BUILD_PENDING' user=request.user as setting_build_pending %} +{% if setting_build_pending %} +addHeaderAction('build-pending', '{% trans "Build Orders In Progress" %}', 'fa-cogs'); loadBuildTable("#table-build-pending", { url: "{% url 'api-build-list' %}", params: { @@ -206,7 +229,11 @@ loadBuildTable("#table-build-pending", { }, disableFilters: true, }); +{% endif %} +{% settings_value 'HOMEPAGE_BUILD_OVERDUE' user=request.user as setting_build_overdue %} +{% if setting_build_overdue %} +addHeaderAction('build-overdue', '{% trans "Overdue Build Orders" %}', 'fa-calendar-times'); loadBuildTable("#table-build-overdue", { url: "{% url 'api-build-list' %}", params: { @@ -216,11 +243,15 @@ loadBuildTable("#table-build-overdue", { }); {% endif %} +{% endif %} + + {% if roles.purchase_order.view %} addHeaderTitle('{% trans "Purchase Orders" %}'); -addHeaderAction('po-outstanding', '{% trans "Outstanding Purchase Orders" %}', 'fa-sign-in-alt'); -addHeaderAction('po-overdue', '{% trans "Overdue Purchase Orders" %}', 'fa-calendar-times'); +{% settings_value 'HOMEPAGE_PO_OUTSTANDING' user=request.user as setting_po_outstanding %} +{% if setting_po_outstanding %} +addHeaderAction('po-outstanding', '{% trans "Outstanding Purchase Orders" %}', 'fa-sign-in-alt'); loadPurchaseOrderTable("#table-po-outstanding", { url: "{% url 'api-po-list' %}", params: { @@ -228,7 +259,11 @@ loadPurchaseOrderTable("#table-po-outstanding", { outstanding: true, } }); +{% endif %} +{% settings_value 'HOMEPAGE_PO_OVERDUE' user=request.user as setting_po_overdue %} +{% if setting_po_overdue %} +addHeaderAction('po-overdue', '{% trans "Overdue Purchase Orders" %}', 'fa-calendar-times'); loadPurchaseOrderTable("#table-po-overdue", { url: "{% url 'api-po-list' %}", params: { @@ -236,14 +271,17 @@ loadPurchaseOrderTable("#table-po-overdue", { overdue: true, } }); +{% endif %} {% endif %} + {% if roles.sales_order.view %} addHeaderTitle('{% trans "Sales Orders" %}'); -addHeaderAction('so-outstanding', '{% trans "Outstanding Sales Orders" %}', 'fa-sign-out-alt'); -addHeaderAction('so-overdue', '{% trans "Overdue Sales Orders" %}', 'fa-calendar-times'); +{% settings_value 'HOMEPAGE_SO_OUTSTANDING' user=request.user as setting_so_outstanding %} +{% if setting_so_outstanding %} +addHeaderAction('so-outstanding', '{% trans "Outstanding Sales Orders" %}', 'fa-sign-out-alt'); loadSalesOrderTable("#table-so-outstanding", { url: "{% url 'api-so-list' %}", params: { @@ -251,7 +289,11 @@ loadSalesOrderTable("#table-so-outstanding", { outstanding: true, }, }); +{% endif %} +{% settings_value 'HOMEPAGE_SO_OVERDUE' user=request.user as setting_so_overdue %} +{% if setting_so_overdue %} +addHeaderAction('so-overdue', '{% trans "Overdue Sales Orders" %}', 'fa-calendar-times'); loadSalesOrderTable("#table-so-overdue", { url: "{% url 'api-so-list' %}", params: { @@ -259,6 +301,7 @@ loadSalesOrderTable("#table-so-overdue", { customer_detail: true, } }); +{% endif %} {% endif %} diff --git a/InvenTree/templates/InvenTree/settings/user_settings.html b/InvenTree/templates/InvenTree/settings/user_settings.html index d56cb5b35a..0b76b930c5 100644 --- a/InvenTree/templates/InvenTree/settings/user_settings.html +++ b/InvenTree/templates/InvenTree/settings/user_settings.html @@ -19,6 +19,22 @@ {% include "InvenTree/settings/setting.html" with key="HOMEPAGE_PART_STARRED" user_setting=True %} {% include "InvenTree/settings/setting.html" with key="HOMEPAGE_PART_LATEST" user_setting=True %} {% include "InvenTree/settings/setting.html" with key="HOMEPAGE_BOM_VALIDATION" user_setting=True %} + + {% include "InvenTree/settings/setting.html" with key="HOMEPAGE_STOCK_RECENT" user_setting=True %} + {% include "InvenTree/settings/setting.html" with key="HOMEPAGE_STOCK_LOW" user_setting=True %} + {% include "InvenTree/settings/setting.html" with key="HOMEPAGE_STOCK_DEPLETED" user_setting=True %} + {% include "InvenTree/settings/setting.html" with key="HOMEPAGE_STOCK_NEEDED" user_setting=True %} + {% include "InvenTree/settings/setting.html" with key="HOMEPAGE_STOCK_EXPIRED" user_setting=True %} + {% include "InvenTree/settings/setting.html" with key="HOMEPAGE_STOCK_STALE" user_setting=True %} + + {% include "InvenTree/settings/setting.html" with key="HOMEPAGE_BUILD_PENDING" user_setting=True %} + {% include "InvenTree/settings/setting.html" with key="HOMEPAGE_BUILD_OVERDUE" user_setting=True %} + + {% include "InvenTree/settings/setting.html" with key="HOMEPAGE_PO_OUTSTANDING" user_setting=True %} + {% include "InvenTree/settings/setting.html" with key="HOMEPAGE_PO_OVERDUE" user_setting=True %} + + {% include "InvenTree/settings/setting.html" with key="HOMEPAGE_SO_OUTSTANDING" user_setting=True %} + {% include "InvenTree/settings/setting.html" with key="HOMEPAGE_SO_OVERDUE" user_setting=True %} From 431b35ed322603a73e3875e2496ea4378399e1a7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 24 Jul 2021 00:42:17 +0200 Subject: [PATCH 59/62] new tag for building lists --- InvenTree/part/templatetags/inventree_extras.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/InvenTree/part/templatetags/inventree_extras.py b/InvenTree/part/templatetags/inventree_extras.py index e867441b42..a7887ec250 100644 --- a/InvenTree/part/templatetags/inventree_extras.py +++ b/InvenTree/part/templatetags/inventree_extras.py @@ -69,6 +69,12 @@ def add(x, y, *args, **kwargs): return x + y +@register.simple_tag() +def to_list(*args): + """ Return the input arguments as list """ + return args + + @register.simple_tag() def part_allocation_count(build, part, *args, **kwargs): """ Return the total number of allocated to """ From a82483dbaa7833181fccd4abcae999ac4ddd05f1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 24 Jul 2021 00:44:16 +0200 Subject: [PATCH 60/62] hiding homepage block when no setting is used --- InvenTree/templates/InvenTree/index.html | 51 ++++++++++++++---------- 1 file changed, 30 insertions(+), 21 deletions(-) diff --git a/InvenTree/templates/InvenTree/index.html b/InvenTree/templates/InvenTree/index.html index 65034a8ac8..ee08777502 100644 --- a/InvenTree/templates/InvenTree/index.html +++ b/InvenTree/templates/InvenTree/index.html @@ -93,11 +93,14 @@ function addHeaderAction(label, title, icon, options) { }); } +{% settings_value 'HOMEPAGE_PART_STARRED' user=request.user as setting_part_starred %} +{% settings_value 'HOMEPAGE_PART_LATEST' user=request.user as setting_part_latest %} +{% settings_value 'HOMEPAGE_BOM_VALIDATION' user=request.user as setting_bom_validation %} +{% to_list setting_part_starred setting_part_latest setting_bom_validation as settings_list_part %} -{% if roles.part.view %} +{% if roles.part.view and True in settings_list_part %} addHeaderTitle('{% trans "Parts" %}'); -{% settings_value 'HOMEPAGE_PART_STARRED' user=request.user as setting_part_starred %} {% if setting_part_starred %} addHeaderAction('starred-parts', '{% trans "Starred Parts" %}', 'fa-star'); loadSimplePartTable("#table-starred-parts", "{% url 'api-part-list' %}", { @@ -108,7 +111,6 @@ loadSimplePartTable("#table-starred-parts", "{% url 'api-part-list' %}", { }); {% endif %} -{% settings_value 'HOMEPAGE_PART_LATEST' user=request.user as setting_part_latest %} {% if setting_part_latest %} addHeaderAction('latest-parts', '{% trans "Latest Parts" %}', 'fa-newspaper'); loadSimplePartTable("#table-latest-parts", "{% url 'api-part-list' %}", { @@ -120,7 +122,6 @@ loadSimplePartTable("#table-latest-parts", "{% url 'api-part-list' %}", { }); {% endif %} -{% settings_value 'HOMEPAGE_BOM_VALIDATION' user=request.user as setting_bom_validation %} {% if setting_bom_validation %} addHeaderAction('bom-validation', '{% trans "BOM Waiting Validation" %}', 'fa-times-circle'); loadSimplePartTable("#table-bom-validation", "{% url 'api-part-list' %}", { @@ -132,11 +133,22 @@ loadSimplePartTable("#table-bom-validation", "{% url 'api-part-list' %}", { {% endif %} {% endif %} +{% settings_value 'HOMEPAGE_STOCK_RECENT' user=request.user as setting_stock_recent %} +{% settings_value 'HOMEPAGE_STOCK_LOW' user=request.user as setting_stock_low %} +{% settings_value 'HOMEPAGE_STOCK_DEPLETED' user=request.user as setting_stock_depleted %} +{% settings_value 'HOMEPAGE_STOCK_NEEDED' user=request.user as setting_stock_needed %} +{% settings_value "STOCK_ENABLE_EXPIRY" as expiry %} +{% if expiry %} +{% settings_value 'HOMEPAGE_STOCK_EXPIRED' user=request.user as setting_stock_expired %} +{% settings_value 'HOMEPAGE_STOCK_STALE' user=request.user as setting_stock_stale %} +{% to_list setting_stock_recent setting_stock_low setting_stock_depleted setting_stock_needed setting_stock_expired setting_stock_stale as settings_list_stock %} +{% else %} +{% to_list setting_stock_recent setting_stock_low setting_stock_depleted setting_stock_needed as settings_list_stock %} +{% endif %} -{% if roles.stock.view %} +{% if roles.stock.view and True in settings_list_stock %} addHeaderTitle('{% trans "Stock" %}'); -{% settings_value 'HOMEPAGE_STOCK_RECENT' user=request.user as setting_stock_recent %} {% if setting_stock_recent %} addHeaderAction('recently-updated-stock', '{% trans "Recently Updated" %}', 'fa-clock'); loadStockTable($('#table-recently-updated-stock'), { @@ -150,7 +162,6 @@ loadStockTable($('#table-recently-updated-stock'), { }); {% endif %} -{% settings_value 'HOMEPAGE_STOCK_LOW' user=request.user as setting_stock_low %} {% if setting_stock_low %} addHeaderAction('low-stock', '{% trans "Low Stock" %}', 'fa-shopping-cart'); loadSimplePartTable("#table-low-stock", "{% url 'api-part-list' %}", { @@ -161,7 +172,6 @@ loadSimplePartTable("#table-low-stock", "{% url 'api-part-list' %}", { }); {% endif %} -{% settings_value 'HOMEPAGE_STOCK_DEPLETED' user=request.user as setting_stock_depleted %} {% if setting_stock_depleted %} addHeaderAction('depleted-stock', '{% trans "Depleted Stock" %}', 'fa-times'); loadSimplePartTable("#table-depleted-stock", "{% url 'api-part-list' %}", { @@ -172,7 +182,6 @@ loadSimplePartTable("#table-depleted-stock", "{% url 'api-part-list' %}", { }); {% endif %} -{% settings_value 'HOMEPAGE_STOCK_NEEDED' user=request.user as setting_stock_needed %} {% if setting_stock_needed %} addHeaderAction('stock-to-build', '{% trans "Required for Build Orders" %}', 'fa-bullhorn'); loadSimplePartTable("#table-stock-to-build", "{% url 'api-part-list' %}", { @@ -184,10 +193,8 @@ loadSimplePartTable("#table-stock-to-build", "{% url 'api-part-list' %}", { {% endif %} -{% settings_value "STOCK_ENABLE_EXPIRY" as expiry %} {% if expiry %} -{% settings_value 'HOMEPAGE_STOCK_EXPIRED' user=request.user as setting_stock_expired %} {% if setting_stock_expired %} addHeaderAction('expired-stock', '{% trans "Expired Stock" %}', 'fa-calendar-times'); loadStockTable($("#table-expired-stock"), { @@ -199,7 +206,6 @@ loadStockTable($("#table-expired-stock"), { }); {% endif %} -{% settings_value 'HOMEPAGE_STOCK_STALE' user=request.user as setting_stock_stale %} {% if setting_stock_stale %} addHeaderAction('stale-stock', '{% trans "Stale Stock" %}', 'fa-stopwatch'); loadStockTable($("#table-stale-stock"), { @@ -215,11 +221,13 @@ loadStockTable($("#table-stale-stock"), { {% endif %} {% endif %} +{% settings_value 'HOMEPAGE_BUILD_PENDING' user=request.user as setting_build_pending %} +{% settings_value 'HOMEPAGE_BUILD_OVERDUE' user=request.user as setting_build_overdue %} +{% to_list setting_build_pending setting_build_overdue as settings_list_build %} -{% if roles.build.view %} +{% if roles.build.view and True in settings_list_build %} addHeaderTitle('{% trans "Build Orders" %}'); -{% settings_value 'HOMEPAGE_BUILD_PENDING' user=request.user as setting_build_pending %} {% if setting_build_pending %} addHeaderAction('build-pending', '{% trans "Build Orders In Progress" %}', 'fa-cogs'); loadBuildTable("#table-build-pending", { @@ -231,7 +239,6 @@ loadBuildTable("#table-build-pending", { }); {% endif %} -{% settings_value 'HOMEPAGE_BUILD_OVERDUE' user=request.user as setting_build_overdue %} {% if setting_build_overdue %} addHeaderAction('build-overdue', '{% trans "Overdue Build Orders" %}', 'fa-calendar-times'); loadBuildTable("#table-build-overdue", { @@ -245,11 +252,13 @@ loadBuildTable("#table-build-overdue", { {% endif %} +{% settings_value 'HOMEPAGE_PO_OUTSTANDING' user=request.user as setting_po_outstanding %} +{% settings_value 'HOMEPAGE_PO_OVERDUE' user=request.user as setting_po_overdue %} +{% to_list setting_po_outstanding setting_po_overdue as settings_list_po %} -{% if roles.purchase_order.view %} +{% if roles.purchase_order.view and True in settings_list_po %} addHeaderTitle('{% trans "Purchase Orders" %}'); -{% settings_value 'HOMEPAGE_PO_OUTSTANDING' user=request.user as setting_po_outstanding %} {% if setting_po_outstanding %} addHeaderAction('po-outstanding', '{% trans "Outstanding Purchase Orders" %}', 'fa-sign-in-alt'); loadPurchaseOrderTable("#table-po-outstanding", { @@ -261,7 +270,6 @@ loadPurchaseOrderTable("#table-po-outstanding", { }); {% endif %} -{% settings_value 'HOMEPAGE_PO_OVERDUE' user=request.user as setting_po_overdue %} {% if setting_po_overdue %} addHeaderAction('po-overdue', '{% trans "Overdue Purchase Orders" %}', 'fa-calendar-times'); loadPurchaseOrderTable("#table-po-overdue", { @@ -275,11 +283,13 @@ loadPurchaseOrderTable("#table-po-overdue", { {% endif %} +{% settings_value 'HOMEPAGE_SO_OUTSTANDING' user=request.user as setting_so_outstanding %} +{% settings_value 'HOMEPAGE_SO_OVERDUE' user=request.user as setting_so_overdue %} +{% to_list setting_so_outstanding setting_so_overdue as settings_list_so %} -{% if roles.sales_order.view %} +{% if roles.sales_order.view and True in settings_list_so %} addHeaderTitle('{% trans "Sales Orders" %}'); -{% settings_value 'HOMEPAGE_SO_OUTSTANDING' user=request.user as setting_so_outstanding %} {% if setting_so_outstanding %} addHeaderAction('so-outstanding', '{% trans "Outstanding Sales Orders" %}', 'fa-sign-out-alt'); loadSalesOrderTable("#table-so-outstanding", { @@ -291,7 +301,6 @@ loadSalesOrderTable("#table-so-outstanding", { }); {% endif %} -{% settings_value 'HOMEPAGE_SO_OVERDUE' user=request.user as setting_so_overdue %} {% if setting_so_overdue %} addHeaderAction('so-overdue', '{% trans "Overdue Sales Orders" %}', 'fa-calendar-times'); loadSalesOrderTable("#table-so-overdue", { From 175b24a79470e52ffc907b8d13f0e9c847352a72 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 24 Jul 2021 00:45:37 +0200 Subject: [PATCH 61/62] changing user settings icon --- InvenTree/templates/InvenTree/settings/tabs.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/templates/InvenTree/settings/tabs.html b/InvenTree/templates/InvenTree/settings/tabs.html index 0e5554ca3e..846dfccd23 100644 --- a/InvenTree/templates/InvenTree/settings/tabs.html +++ b/InvenTree/templates/InvenTree/settings/tabs.html @@ -9,7 +9,7 @@ {% trans "Appearance" %} - {% trans "User Settings" %} + {% trans "User Settings" %} {% if user.is_staff %} From 4a32bdb7ab97029d592009e291511a97e6a6c5a4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 24 Jul 2021 01:30:53 +0200 Subject: [PATCH 62/62] fix for phantom migration warning Closes #1775 --- InvenTree/InvenTree/fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/InvenTree/fields.py b/InvenTree/InvenTree/fields.py index 462d2b0e0e..cd98bf50c7 100644 --- a/InvenTree/InvenTree/fields.py +++ b/InvenTree/InvenTree/fields.py @@ -55,7 +55,7 @@ class InvenTreeModelMoneyField(ModelMoneyField): def __init__(self, **kwargs): # detect if creating migration - if 'makemigrations' in sys.argv: + if 'migrate' in sys.argv or 'makemigrations' in sys.argv: # remove currency information for a clean migration kwargs['default_currency'] = '' kwargs['currency_choices'] = []