diff --git a/InvenTree/InvenTree/validators.py b/InvenTree/InvenTree/validators.py index 88da882e10..f3fd9ef306 100644 --- a/InvenTree/InvenTree/validators.py +++ b/InvenTree/InvenTree/validators.py @@ -8,7 +8,7 @@ from django.utils.translation import gettext_lazy as _ def validate_part_name(value): # Prevent some illegal characters in part names - for c in ['/', '\\', '|', '#', '$']: + for c in ['|', '#', '$']: if c in str(value): raise ValidationError( _('Invalid character in part name') diff --git a/InvenTree/build/forms.py b/InvenTree/build/forms.py index 66ec98ac77..01024230f4 100644 --- a/InvenTree/build/forms.py +++ b/InvenTree/build/forms.py @@ -58,6 +58,18 @@ class CompleteBuildForm(HelperForm): ] +class CancelBuildForm(HelperForm): + """ Form for cancelling a build """ + + confirm_cancel = forms.BooleanField(required=False, help_text='Confirm build cancellation') + + class Meta: + model = Build + fields = [ + 'confirm_cancel' + ] + + class EditBuildItemForm(HelperForm): """ Form for adding a new BuildItem to a Build """ diff --git a/InvenTree/build/templates/build/auto_allocate.html b/InvenTree/build/templates/build/auto_allocate.html index f850d094b2..dc2160a006 100644 --- a/InvenTree/build/templates/build/auto_allocate.html +++ b/InvenTree/build/templates/build/auto_allocate.html @@ -22,8 +22,8 @@ Automatically allocate stock to this build? - - + + diff --git a/InvenTree/build/templates/build/cancel.html b/InvenTree/build/templates/build/cancel.html index d273a14ff5..d7e4d51b10 100644 --- a/InvenTree/build/templates/build/cancel.html +++ b/InvenTree/build/templates/build/cancel.html @@ -1,3 +1,7 @@ +{% extends "modal_form.html" %} + +{% block pre_form_content %} + Are you sure you wish to cancel this build? -{% include "modal_csrf.html" %} \ No newline at end of file +{% endblock %} \ No newline at end of file diff --git a/InvenTree/build/views.py b/InvenTree/build/views.py index 85d07858fb..7cef486d55 100644 --- a/InvenTree/build/views.py +++ b/InvenTree/build/views.py @@ -5,8 +5,6 @@ Django views for interacting with Build objects # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.shortcuts import get_object_or_404 - from django.views.generic import DetailView, ListView from django.forms import HiddenInput @@ -15,7 +13,8 @@ from .models import Build, BuildItem from . import forms from stock.models import StockLocation, StockItem -from InvenTree.views import AjaxView, AjaxUpdateView, AjaxCreateView, AjaxDeleteView +from InvenTree.views import AjaxUpdateView, AjaxCreateView, AjaxDeleteView +from InvenTree.helpers import str2bool class BuildIndex(ListView): @@ -41,31 +40,41 @@ class BuildIndex(ListView): return context -class BuildCancel(AjaxView): +class BuildCancel(AjaxUpdateView): """ View to cancel a Build. Provides a cancellation information dialog """ + model = Build ajax_template_name = 'build/cancel.html' ajax_form_title = 'Cancel Build' context_object_name = 'build' - fields = [] + form_class = forms.CancelBuildForm def post(self, request, *args, **kwargs): """ Handle POST request. Mark the build status as CANCELLED """ - build = get_object_or_404(Build, pk=self.kwargs['pk']) + build = self.get_object() - build.cancelBuild(request.user) + form = self.get_form() - return self.renderJsonResponse(request, None) + valid = form.is_valid() - def get_data(self): - """ Provide JSON context data. """ - return { + confirm = str2bool(request.POST.get('confirm_cancel', False)) + + if confirm: + build.cancelBuild(request.user) + else: + form.errors['confirm_cancel'] = ['Confirm build cancellation'] + valid = False + + data = { + 'form_valid': valid, 'danger': 'Build was cancelled' } + return self.renderJsonResponse(request, form, data=data) + class BuildAutoAllocate(AjaxUpdateView): """ View to auto-allocate parts for a build. @@ -90,7 +99,7 @@ class BuildAutoAllocate(AjaxUpdateView): context['build'] = build context['allocations'] = build.getAutoAllocations() except Build.DoesNotExist: - context['error'] = 'No matching buidl found' + context['error'] = 'No matching build found' return context @@ -217,7 +226,7 @@ class BuildComplete(AjaxUpdateView): form = self.get_form() - confirm = request.POST.get('confirm', False) + confirm = str2bool(request.POST.get('confirm', False)) loc_id = request.POST.get('location', None) diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index 0a2bdbfef6..e671b49e4f 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -157,6 +157,7 @@ class PartList(generics.ListCreateAPIView): '$name', 'description', '$IPN', + 'keywords', ] diff --git a/InvenTree/part/forms.py b/InvenTree/part/forms.py index 580ed737a4..88c6c11385 100644 --- a/InvenTree/part/forms.py +++ b/InvenTree/part/forms.py @@ -92,9 +92,10 @@ class EditPartForm(HelperForm): 'confirm_creation', 'category', 'name', + 'IPN', 'variant', 'description', - 'IPN', + 'keywords', 'URL', 'default_location', 'default_supplier', @@ -118,7 +119,8 @@ class EditCategoryForm(HelperForm): 'parent', 'name', 'description', - 'default_location' + 'default_location', + 'default_keywords', ] diff --git a/InvenTree/part/migrations/0023_part_keywords.py b/InvenTree/part/migrations/0023_part_keywords.py new file mode 100644 index 0000000000..4752d80740 --- /dev/null +++ b/InvenTree/part/migrations/0023_part_keywords.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2 on 2019-05-14 07:15 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('part', '0022_auto_20190512_1246'), + ] + + operations = [ + migrations.AddField( + model_name='part', + name='keywords', + field=models.CharField(blank=True, help_text='Part keywords to improve visibility in search results', max_length=250), + ), + ] diff --git a/InvenTree/part/migrations/0024_partcategory_default_keywords.py b/InvenTree/part/migrations/0024_partcategory_default_keywords.py new file mode 100644 index 0000000000..317d982f7d --- /dev/null +++ b/InvenTree/part/migrations/0024_partcategory_default_keywords.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2 on 2019-05-14 07:27 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('part', '0023_part_keywords'), + ] + + operations = [ + migrations.AddField( + model_name='partcategory', + name='default_keywords', + field=models.CharField(blank=True, help_text='Default keywords for parts in this category', max_length=250), + ), + ] diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 5a609c0e59..3aeb94700a 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -37,6 +37,12 @@ from company.models import Company class PartCategory(InvenTreeTree): """ PartCategory provides hierarchical organization of Part objects. + + Attributes: + name: Name of this category + parent: Parent category + default_location: Default storage location for parts in this category or child categories + default_keywords: Default keywords for parts created in this category """ default_location = models.ForeignKey( @@ -46,6 +52,8 @@ class PartCategory(InvenTreeTree): help_text='Default location for parts in this category' ) + default_keywords = models.CharField(blank=True, max_length=250, help_text='Default keywords for parts in this category') + def get_absolute_url(self): return reverse('category-detail', kwargs={'pk': self.id}) @@ -179,8 +187,9 @@ class Part(models.Model): Attributes: name: Brief name for this part variant: Optional variant number for this part - Must be unique for the part name - description: Longer form description of the part category: The PartCategory to which this part belongs + description: Longer form description of the part + keywords: Optional keywords for improving part search results IPN: Internal part number (optional) URL: Link to an external page with more information about this part (e.g. internal Wiki) image: Image of this part @@ -250,6 +259,8 @@ class Part(models.Model): description = models.CharField(max_length=250, blank=False, help_text='Part description') + keywords = models.CharField(max_length=250, blank=True, help_text='Part keywords to improve visibility in search results') + category = models.ForeignKey(PartCategory, related_name='parts', null=True, blank=True, on_delete=models.DO_NOTHING, diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index 957bfa5951..37ccb639a0 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -62,15 +62,16 @@ class PartSerializer(serializers.ModelSerializer): fields = [ 'pk', 'url', # Link to the part detail page - 'full_name', - 'name', - 'variant', - 'image_url', - 'IPN', - 'URL', # Link to an external URL (optional) - 'description', 'category', 'category_name', + 'image_url', + 'full_name', + 'name', + 'IPN', + 'variant', + 'description', + 'keywords', + 'URL', 'total_stock', 'available_stock', 'units', diff --git a/InvenTree/part/templates/part/detail.html b/InvenTree/part/templates/part/detail.html index c0f823ce13..a7bad6cae4 100644 --- a/InvenTree/part/templates/part/detail.html +++ b/InvenTree/part/templates/part/detail.html @@ -35,16 +35,22 @@ Part name {{ part.full_name }} - - Description - {{ part.description }} - {% if part.IPN %} IPN {{ part.IPN }} {% endif %} + + Description + {{ part.description }} + + {% if part.keywords %} + + Keywords + {{ part.keywords }} + + {% endif %} {% if part.URL %} URL diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 8650b85fb4..fef88197f7 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -339,7 +339,9 @@ class PartCreate(AjaxCreateView): if self.get_category_id(): try: - initials['category'] = PartCategory.objects.get(pk=self.get_category_id()) + category = PartCategory.objects.get(pk=self.get_category_id()) + initials['category'] = category + initials['keywords'] = category.default_keywords except PartCategory.DoesNotExist: pass diff --git a/InvenTree/static/script/inventree/bom.js b/InvenTree/static/script/inventree/bom.js index eaa1d40d3d..46bdc249ef 100644 --- a/InvenTree/static/script/inventree/bom.js +++ b/InvenTree/static/script/inventree/bom.js @@ -89,11 +89,11 @@ function loadBomTable(table, options) { // Part column cols.push( { - field: 'sub_part_detail', + field: 'sub_part_detail.full_name', title: 'Part', sortable: true, formatter: function(value, row, index, field) { - return imageHoverIcon(value.image_url) + renderLink(value.full_name, value.url); + return imageHoverIcon(row.sub_part_detail.image_url) + renderLink(row.sub_part_detail.full_name, row.sub_part_detail.url); } } ); @@ -115,6 +115,34 @@ function loadBomTable(table, options) { sortable: true, } ); + + if (!options.editable) { + cols.push( + { + field: 'sub_part_detail.available_stock', + title: 'Available', + searchable: false, + sortable: true, + formatter: function(value, row, index, field) { + var text = ""; + + if (row.quantity < row.sub_part_detail.available_stock) + { + text = "" + value + ""; + } + else + { + if (!value) { + value = 'No Stock'; + } + text = "" + value + ""; + } + + return renderLink(text, row.sub_part_detail.url + "stock/"); + } + } + ); + } // Part notes cols.push( @@ -137,31 +165,6 @@ function loadBomTable(table, options) { }); } - else { - cols.push( - { - field: 'sub_part_detail.available_stock', - title: 'Available', - searchable: false, - sortable: true, - formatter: function(value, row, index, field) { - var text = ""; - - if (row.quantity < row.sub_part_detail.available_stock) - { - text = "" + value + ""; - } - else - { - text = "" + value + ""; - } - - return renderLink(text, row.sub_part.url + "stock/"); - } - } - ); - } - // Configure the table (bootstrap-table) table.bootstrapTable({ @@ -172,6 +175,7 @@ function loadBomTable(table, options) { queryParams: function(p) { return { part: options.parent_id, + ordering: 'name', } }, columns: cols, diff --git a/InvenTree/static/script/inventree/part.js b/InvenTree/static/script/inventree/part.js index c66fe405b2..51d86f1c7a 100644 --- a/InvenTree/static/script/inventree/part.js +++ b/InvenTree/static/script/inventree/part.js @@ -119,13 +119,12 @@ function loadPartTable(table, url, options={}) { visible: false, }, { - field: 'name', + field: 'full_name', title: 'Part', sortable: true, formatter: function(value, row, index, field) { - var name = row.full_name; - var display = imageHoverIcon(row.image_url) + renderLink(name, row.url); + var display = imageHoverIcon(row.image_url) + renderLink(value, row.url); if (!row.active) { display = display + "INACTIVE"; } @@ -160,7 +159,7 @@ function loadPartTable(table, url, options={}) { return renderLink(value, row.url + 'stock/'); } else { - return "No stock"; + return "No Stock"; } } }