diff --git a/InvenTree/InvenTree/helpers.py b/InvenTree/InvenTree/helpers.py index 6f6953ccb5..d5508b7db2 100644 --- a/InvenTree/InvenTree/helpers.py +++ b/InvenTree/InvenTree/helpers.py @@ -357,6 +357,8 @@ def extract_serial_numbers(serials, expected_quantity): - Serial numbers must be positive - Serial numbers can be split by whitespace / newline / commma chars - Serial numbers can be supplied as an inclusive range using hyphen char e.g. 10-20 + - Serial numbers can be supplied as + for getting all expecteded numbers starting from + - Serial numbers can be supplied as + for getting numbers starting from Args: expected_quantity: The number of (unique) serial numbers we expect @@ -369,6 +371,13 @@ def extract_serial_numbers(serials, expected_quantity): numbers = [] errors = [] + # helpers + def number_add(n): + if n in numbers: + errors.append(_('Duplicate serial: {n}').format(n=n)) + else: + numbers.append(n) + try: expected_quantity = int(expected_quantity) except ValueError: @@ -395,10 +404,7 @@ def extract_serial_numbers(serials, expected_quantity): if a < b: for n in range(a, b + 1): - if n in numbers: - errors.append(_('Duplicate serial: {n}').format(n=n)) - else: - numbers.append(n) + number_add(n) else: errors.append(_("Invalid group: {g}").format(g=group)) @@ -409,6 +415,31 @@ def extract_serial_numbers(serials, expected_quantity): errors.append(_("Invalid group: {g}").format(g=group)) continue + # plus signals either + # 1: 'start+': expected number of serials, starting at start + # 2: 'start+number': number of serials, starting at start + elif '+' in group: + items = group.split('+') + + # case 1, 2 + if len(items) == 2: + start = int(items[0]) + + # case 2 + if bool(items[1]): + end = start + int(items[1]) + 1 + + # case 1 + else: + end = start + expected_quantity + + for n in range(start, end): + number_add(n) + # no case + else: + errors.append(_("Invalid group: {g}").format(g=group)) + continue + else: if group in numbers: errors.append(_("Duplicate serial: {g}".format(g=group))) diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index 4fd8efae1a..cc61748372 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -491,7 +491,7 @@ LANGUAGES = [ ('en', _('English')), ('fr', _('French')), ('de', _('German')), - ('pk', _('Polish')), + ('pl', _('Polish')), ('tr', _('Turkish')), ] diff --git a/InvenTree/InvenTree/tests.py b/InvenTree/InvenTree/tests.py index 8465473901..af812fe8a3 100644 --- a/InvenTree/InvenTree/tests.py +++ b/InvenTree/InvenTree/tests.py @@ -244,6 +244,14 @@ class TestSerialNumberExtraction(TestCase): self.assertIn(3, sn) self.assertIn(13, sn) + sn = e("1+", 10) + self.assertEqual(len(sn), 10) + self.assertEqual(sn, [_ for _ in range(1, 11)]) + + sn = e("4, 1+2", 4) + self.assertEqual(len(sn), 4) + self.assertEqual(sn, ["4", 1, 2, 3]) + def test_failures(self): e = helpers.extract_serial_numbers diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index da7799397e..c57e82addc 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -39,7 +39,7 @@ from rest_framework.documentation import include_docs_urls from .views import IndexView, SearchView, DatabaseStatsView from .views import SettingsView, EditUserView, SetPasswordView -from .views import ColorThemeSelectView, SettingCategorySelectView +from .views import AppearanceSelectView, SettingCategorySelectView from .views import DynamicJsView from common.views import SettingEdit @@ -79,7 +79,8 @@ apipatterns = [ settings_urls = [ url(r'^user/?', SettingsView.as_view(template_name='InvenTree/settings/user.html'), name='settings-user'), - url(r'^theme/?', ColorThemeSelectView.as_view(), name='settings-theme'), + url(r'^appearance/?', AppearanceSelectView.as_view(), name='settings-appearance'), + url(r'^i18n/?', include('django.conf.urls.i18n')), url(r'^global/?', SettingsView.as_view(template_name='InvenTree/settings/global.html'), name='settings-global'), url(r'^report/?', SettingsView.as_view(template_name='InvenTree/settings/report.html'), name='settings-report'), diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py index 5a080d7cdc..def4b34781 100644 --- a/InvenTree/InvenTree/views.py +++ b/InvenTree/InvenTree/views.py @@ -769,12 +769,12 @@ class SettingsView(TemplateView): return ctx -class ColorThemeSelectView(FormView): +class AppearanceSelectView(FormView): """ View for selecting a color theme """ form_class = ColorThemeSelectForm - success_url = reverse_lazy('settings-theme') - template_name = "InvenTree/settings/theme.html" + success_url = reverse_lazy('settings-appearance') + template_name = "InvenTree/settings/appearance.html" def get_user_theme(self): """ Get current user color theme """ @@ -788,7 +788,7 @@ class ColorThemeSelectView(FormView): def get_initial(self): """ Select current user color theme as initial choice """ - initial = super(ColorThemeSelectView, self).get_initial() + initial = super(AppearanceSelectView, self).get_initial() user_theme = self.get_user_theme() if user_theme: diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index 16c0e5bb7f..c5da505f43 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -996,14 +996,28 @@ class Build(MPTTModel): @property def required_parts(self): - """ Returns a dict of parts required to build this part (BOM) """ + """ Returns a list of parts required to build this part (BOM) """ parts = [] - for item in self.part.bom_items.all().prefetch_related('sub_part'): + for item in self.bom_items: parts.append(item.sub_part) return parts + @property + def required_parts_to_complete_build(self): + """ Returns a list of parts required to complete the full build """ + parts = [] + + for bom_item in self.bom_items: + # Get remaining quantity needed + required_quantity_to_complete_build = self.remaining * bom_item.quantity + # Compare to net stock + if bom_item.sub_part.net_stock < required_quantity_to_complete_build: + parts.append(bom_item.sub_part) + + return parts + def availableStockItems(self, part, output): """ Returns stock items which are available for allocation to this build. diff --git a/InvenTree/build/templates/build/complete_output.html b/InvenTree/build/templates/build/complete_output.html index 3436aea8de..d03885774f 100644 --- a/InvenTree/build/templates/build/complete_output.html +++ b/InvenTree/build/templates/build/complete_output.html @@ -40,8 +40,8 @@
{% trans "The following items will be created" %}
-
- {% include "hover_image.html" with image=build.part.image hover=True %} +
+ {% include "hover_image.html" with image=build.part.image %} {% if output.serialized %} {{ output.part.full_name }} - {% trans "Serial Number" %} {{ output.serial }} {% else %} diff --git a/InvenTree/build/views.py b/InvenTree/build/views.py index b41f975a26..66815cae70 100644 --- a/InvenTree/build/views.py +++ b/InvenTree/build/views.py @@ -157,6 +157,17 @@ class BuildOutputCreate(AjaxUpdateView): quantity = form.cleaned_data.get('output_quantity', None) serials = form.cleaned_data.get('serial_numbers', None) + if quantity: + build = self.get_object() + + # Check that requested output don't exceed build remaining quantity + maximum_output = int(build.remaining - build.incomplete_count) + if quantity > maximum_output: + form.add_error( + 'output_quantity', + _('Maximum output quantity is ') + str(maximum_output), + ) + # Check that the serial numbers are valid if serials: try: @@ -212,7 +223,7 @@ class BuildOutputCreate(AjaxUpdateView): # Calculate the required quantity quantity = max(0, build.remaining - build.incomplete_count) - initials['output_quantity'] = quantity + initials['output_quantity'] = int(quantity) return initials diff --git a/InvenTree/company/api.py b/InvenTree/company/api.py index 66557b783b..494b3652b2 100644 --- a/InvenTree/company/api.py +++ b/InvenTree/company/api.py @@ -131,7 +131,7 @@ class ManufacturerPartList(generics.ListCreateAPIView): params = self.request.query_params # Filter by manufacturer - manufacturer = params.get('company', None) + manufacturer = params.get('manufacturer', None) if manufacturer is not None: queryset = queryset.filter(manufacturer=manufacturer) diff --git a/InvenTree/company/models.py b/InvenTree/company/models.py index 3ea50b1622..c743bf4434 100644 --- a/InvenTree/company/models.py +++ b/InvenTree/company/models.py @@ -675,4 +675,4 @@ class SupplierPriceBreak(common.models.PriceBreak): db_table = 'part_supplierpricebreak' def __str__(self): - return f'{self.part.MPN} - {self.price} @ {self.quantity}' + return f'{self.part.SKU} - {self.price} @ {self.quantity}' diff --git a/InvenTree/company/serializers.py b/InvenTree/company/serializers.py index 35e84aac1e..335a351583 100644 --- a/InvenTree/company/serializers.py +++ b/InvenTree/company/serializers.py @@ -192,10 +192,11 @@ class SupplierPartSerializer(InvenTreeModelSerializer): manufacturer_id = self.initial_data.get('manufacturer', None) MPN = self.initial_data.get('MPN', None) - if manufacturer_id or MPN: - kwargs = {'manufacturer': manufacturer_id, - 'MPN': MPN, - } + if manufacturer_id and MPN: + kwargs = { + 'manufacturer': manufacturer_id, + 'MPN': MPN, + } supplier_part.save(**kwargs) return supplier_part diff --git a/InvenTree/company/test_api.py b/InvenTree/company/test_api.py index a65beb4dc2..c43280c76c 100644 --- a/InvenTree/company/test_api.py +++ b/InvenTree/company/test_api.py @@ -100,7 +100,7 @@ class ManufacturerTest(InvenTreeAPITestCase): self.assertEqual(response.data['MPN'], 'MPN_TEST') # Filter by manufacturer - data = {'company': 7} + data = {'manufacturer': 7} response = self.get(url, data) self.assertEqual(len(response.data), 3) diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index 0c22b7978b..ea70c3b56a 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -337,14 +337,16 @@ class PurchaseOrder(Order): raise ValidationError({"status": _("Lines can only be received against an order marked as 'Placed'")}) try: + if not (quantity % 1 == 0): + raise ValidationError({"quantity": _("Quantity must be an integer")}) + if quantity < 0: + raise ValidationError({"quantity": _("Quantity must be a positive number")}) quantity = int(quantity) - if quantity <= 0: - raise ValidationError({"quantity": _("Quantity must be greater than zero")}) - except ValueError: + except (ValueError, TypeError): raise ValidationError({"quantity": _("Invalid quantity provided")}) # Create a new stock item - if line.part: + if line.part and quantity > 0: stock = stock_models.StockItem( part=line.part.part, supplier_part=line.part, diff --git a/InvenTree/order/templates/order/order_base.html b/InvenTree/order/templates/order/order_base.html index 5dfc30796f..b29b62a4ad 100644 --- a/InvenTree/order/templates/order/order_base.html +++ b/InvenTree/order/templates/order/order_base.html @@ -171,11 +171,35 @@ $("#edit-order").click(function() { ); }); +$("#receive-order").click(function() { + launchModalForm("{% url 'po-receive' order.id %}", { + reload: true, + secondary: [ + { + field: 'location', + label: '{% trans "New Location" %}', + title: '{% trans "Create new stock location" %}', + url: "{% url 'stock-location-create' %}", + }, + ] + }); +}); + +$("#complete-order").click(function() { + launchModalForm("{% url 'po-complete' order.id %}", { + reload: true, + }); +}); + $("#cancel-order").click(function() { launchModalForm("{% url 'po-cancel' order.id %}", { reload: true, }); }); +$("#export-order").click(function() { + location.href = "{% url 'po-export' order.id %}"; +}); + {% endblock %} \ No newline at end of file diff --git a/InvenTree/order/templates/order/order_cancel.html b/InvenTree/order/templates/order/order_cancel.html index 91707ae737..7cdb03ae20 100644 --- a/InvenTree/order/templates/order/order_cancel.html +++ b/InvenTree/order/templates/order/order_cancel.html @@ -4,6 +4,8 @@ {% block pre_form_content %} -{% trans "Cancelling this order means that the order will no longer be editable." %} +
+ {% trans "Cancelling this order means that the order and line items will no longer be editable." %} +
{% endblock %} \ No newline at end of file diff --git a/InvenTree/order/templates/order/order_complete.html b/InvenTree/order/templates/order/order_complete.html index 5c4ece7f1a..ef35841f9d 100644 --- a/InvenTree/order/templates/order/order_complete.html +++ b/InvenTree/order/templates/order/order_complete.html @@ -6,9 +6,9 @@ {% trans 'Mark this order as complete?' %} {% if not order.is_complete %} -
- {% trans 'This order has line items which have not been marked as received.' %} - {% trans 'Marking this order as complete will remove these line items.' %} +
+ {% trans 'This order has line items which have not been marked as received.' %}
+ {% trans 'Completing this order means that the order and line items will no longer be editable.' %}
{% endif %} diff --git a/InvenTree/order/templates/order/order_issue.html b/InvenTree/order/templates/order/order_issue.html index bb3c3eea8a..058a7b529c 100644 --- a/InvenTree/order/templates/order/order_issue.html +++ b/InvenTree/order/templates/order/order_issue.html @@ -4,6 +4,8 @@ {% block pre_form_content %} -{% trans 'After placing this purchase order, line items will no longer be editable.' %} +
+ {% trans 'After placing this purchase order, line items will no longer be editable.' %} +
{% endblock %} \ No newline at end of file diff --git a/InvenTree/order/templates/order/purchase_order_detail.html b/InvenTree/order/templates/order/purchase_order_detail.html index 93b7ffbd8e..f6231371ac 100644 --- a/InvenTree/order/templates/order/purchase_order_detail.html +++ b/InvenTree/order/templates/order/purchase_order_detail.html @@ -35,31 +35,6 @@ {{ block.super }} - -$("#receive-order").click(function() { - launchModalForm("{% url 'po-receive' order.id %}", { - reload: true, - secondary: [ - { - field: 'location', - label: '{% trans "New Location" %}', - title: '{% trans "Create new stock location" %}', - url: "{% url 'stock-location-create' %}", - }, - ] - }); -}); - -$("#complete-order").click(function() { - launchModalForm("{% url 'po-complete' order.id %}", { - reload: true, - }); -}); - -$("#export-order").click(function() { - location.href = "{% url 'po-export' order.id %}"; -}); - {% if order.status == PurchaseOrderStatus.PENDING %} $('#new-po-line').click(function() { launchModalForm("{% url 'po-line-item-create' %}", @@ -261,5 +236,4 @@ $("#po-table").inventreeTable({ ] }); - {% endblock %} \ No newline at end of file diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index a2d7609bed..6b26365b27 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -7,7 +7,7 @@ from __future__ import unicode_literals from django_filters.rest_framework import DjangoFilterBackend from django.http import JsonResponse -from django.db.models import Q, F, Count, Prefetch, Sum +from django.db.models import Q, F, Count from django.utils.translation import ugettext_lazy as _ from rest_framework import status @@ -635,29 +635,15 @@ class PartList(generics.ListCreateAPIView): # TODO: Need to figure out a cheaper way of making this filter query if stock_to_build is not None: - # Filter only active parts - queryset = queryset.filter(active=True) - # Prefetch current active builds - build_active_queryset = Build.objects.filter(status__in=BuildStatus.ACTIVE_CODES) - build_active_prefetch = Prefetch('builds', - queryset=build_active_queryset, - to_attr='current_builds') - parts = queryset.prefetch_related(build_active_prefetch) - + # Get active builds + builds = Build.objects.filter(status__in=BuildStatus.ACTIVE_CODES) # Store parts with builds needing stock - parts_need_stock = [] + parts_needed_to_complete_builds = [] + # Filter required parts + for build in builds: + parts_needed_to_complete_builds += [part.pk for part in build.required_parts_to_complete_build] - # Find parts with active builds - # where any subpart's stock is lower than quantity being built - for part in parts: - if part.current_builds: - builds_ids = [build.id for build in part.current_builds] - total_build_quantity = build_active_queryset.filter(pk__in=builds_ids).aggregate(quantity=Sum('quantity'))['quantity'] - - if part.can_build < total_build_quantity: - parts_need_stock.append(part.pk) - - queryset = queryset.filter(pk__in=parts_need_stock) + queryset = queryset.filter(pk__in=parts_needed_to_complete_builds) # Optionally limit the maximum number of returned results # e.g. for displaying "recent part" list diff --git a/InvenTree/part/templatetags/inventree_extras.py b/InvenTree/part/templatetags/inventree_extras.py index a92d95c766..536f25cb5b 100644 --- a/InvenTree/part/templatetags/inventree_extras.py +++ b/InvenTree/part/templatetags/inventree_extras.py @@ -116,6 +116,12 @@ def inventree_docs_url(*args, **kwargs): return "https://inventree.readthedocs.io/" +@register.simple_tag() +def inventree_credits_url(*args, **kwargs): + """ Return URL for InvenTree credits site """ + return "https://inventree.readthedocs.io/en/latest/credits/" + + @register.simple_tag() def setting_object(key, *args, **kwargs): """ diff --git a/InvenTree/templates/InvenTree/settings/appearance.html b/InvenTree/templates/InvenTree/settings/appearance.html new file mode 100644 index 0000000000..8cd7a8d2db --- /dev/null +++ b/InvenTree/templates/InvenTree/settings/appearance.html @@ -0,0 +1,67 @@ +{% extends "InvenTree/settings/settings.html" %} +{% load i18n %} +{% load inventree_extras %} + +{% block tabs %} +{% include "InvenTree/settings/tabs.html" with tab='theme' %} +{% endblock %} + +{% block subtitle %} +{% trans "Theme Settings" %} +{% endblock %} + +{% block settings %} + +
+
+

{% trans "Color Themes" %}

+
+
+ +
+ {% csrf_token %} + {% load crispy_forms_tags %} + {% crispy form %} +
+ +{% if invalid_color_theme %} + +{% endif %} + + +
+
+

{% trans "Language" %}

+
+
+ + + +
+
{% csrf_token %} + +
+ +
+
+ +
+
+
+ + +{% endblock %} diff --git a/InvenTree/templates/InvenTree/settings/tabs.html b/InvenTree/templates/InvenTree/settings/tabs.html index bb660519b8..3f8be0a313 100644 --- a/InvenTree/templates/InvenTree/settings/tabs.html +++ b/InvenTree/templates/InvenTree/settings/tabs.html @@ -6,7 +6,7 @@ {% trans "Account" %} - {% trans "Theme" %} + {% trans "Appearance" %} {% if user.is_staff %} diff --git a/InvenTree/templates/InvenTree/settings/theme.html b/InvenTree/templates/InvenTree/settings/theme.html deleted file mode 100644 index d7b006d13c..0000000000 --- a/InvenTree/templates/InvenTree/settings/theme.html +++ /dev/null @@ -1,36 +0,0 @@ -{% extends "InvenTree/settings/settings.html" %} -{% load i18n %} -{% load inventree_extras %} - -{% block tabs %} -{% include "InvenTree/settings/tabs.html" with tab='theme' %} -{% endblock %} - -{% block subtitle %} -{% trans "Theme Settings" %} -{% endblock %} - -{% block settings %} - -
-
-

{% trans "Color Themes" %}

-
-
- -
- {% csrf_token %} - {% load crispy_forms_tags %} - {% crispy form %} -
- -{% if invalid_color_theme %} - -{% endif %} - -{% endblock %} diff --git a/InvenTree/templates/about.html b/InvenTree/templates/about.html index 15ff588576..b1fb5ccd07 100644 --- a/InvenTree/templates/about.html +++ b/InvenTree/templates/about.html @@ -58,6 +58,11 @@ {% trans "View Code on GitHub" %} {% inventree_github_url %} + + + {% trans "Credits" %} + {% inventree_credits_url %} + {% trans "Mobile App" %} diff --git a/InvenTree/templates/stock_table.html b/InvenTree/templates/stock_table.html index 1f54767f02..d917bac72a 100644 --- a/InvenTree/templates/stock_table.html +++ b/InvenTree/templates/stock_table.html @@ -16,7 +16,7 @@ {% if owner_control.value == "True" and user in owners or user.is_superuser or owner_control.value == "False" %} - {% if roles.stock.add %} + {% if not read_only and roles.stock.add %} @@ -44,6 +44,7 @@ {% endif %}
+ {% if not read_only %} {% if roles.stock.change or roles.stock.delete %}
{% endif %} {% endif %} + {% endif %}
diff --git a/README.md b/README.md index 6a665e9654..5f25960b9a 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,10 @@ To contribute to the translation effort, navigate to the [InvenTree crowdin proj For InvenTree documentation, refer to the [InvenTree documentation website](https://inventree.readthedocs.io/en/latest/). +# Credits + +The credits for all used packages are part of the [InvenTree documentation website](https://inventree.readthedocs.io/en/latest/credits/). + ## Getting Started Refer to the [getting started guide](https://inventree.readthedocs.io/en/latest/start/install/) for installation and setup instructions. diff --git a/tasks.py b/tasks.py index c4eda5e5bf..3065d97243 100644 --- a/tasks.py +++ b/tasks.py @@ -248,6 +248,7 @@ def content_excludes(): "django_q.schedule", "django_q.task", "django_q.ormq", + "users.owner", ] output = ""