From c6fd2281d689309007b06c46560e6f446ee931bd Mon Sep 17 00:00:00 2001 From: rgilham Date: Thu, 24 Jun 2021 02:13:55 +0200 Subject: [PATCH 01/40] 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/40] 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/40] 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/40] 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/40] 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/40] 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/40] 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/40] 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 4600b0d33732f6442c1ae1970131ee7b61c8c7e4 Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 20 Jul 2021 11:35:55 +1000 Subject: [PATCH 16/40] 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 17/40] 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 18/40] 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 19/40] 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 20/40] 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 21/40] 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 22/40] 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 23/40] 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 24/40] 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 25/40] 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 26/40] 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 27/40] 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 28/40] 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 29/40] 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 30/40] 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 31/40] 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 32/40] 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 33/40] 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 34/40] 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 35/40] 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 36/40] 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 37/40] 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 38/40] 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 39/40] 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 40/40] 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(), + }