mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-10-31 13:15:43 +00:00 
			
		
		
		
	| @@ -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') | ||||
|   | ||||
| @@ -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 """ | ||||
|  | ||||
|   | ||||
| @@ -22,8 +22,8 @@ Automatically allocate stock to this build? | ||||
| <tr> | ||||
|     <td> | ||||
|         <a class='hover-icon'> | ||||
|             <img class='hover-img-thumb' src='{{ item.stock_item.part.image.url }}'> | ||||
|             <img class='hover-img-large' src='{{ item.stock_item.part.image.url }}'> | ||||
|             <img class='hover-img-thumb' src='{% if item.stock_item.part.image %}{{ item.stock_item.part.image.url }}{% endif %}'> | ||||
|             <img class='hover-img-large' src='{% if item.stock_item.part.image %}{{ item.stock_item.part.image.url }}{% endif %}'> | ||||
|         </a> | ||||
|     </td> | ||||
|     <td> | ||||
|   | ||||
| @@ -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" %} | ||||
| {% endblock %} | ||||
| @@ -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) | ||||
|  | ||||
|   | ||||
| @@ -157,6 +157,7 @@ class PartList(generics.ListCreateAPIView): | ||||
|         '$name', | ||||
|         'description', | ||||
|         '$IPN', | ||||
|         'keywords', | ||||
|     ] | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -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', | ||||
|         ] | ||||
|  | ||||
|  | ||||
|   | ||||
							
								
								
									
										18
									
								
								InvenTree/part/migrations/0023_part_keywords.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								InvenTree/part/migrations/0023_part_keywords.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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), | ||||
|         ), | ||||
|     ] | ||||
| @@ -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), | ||||
|         ), | ||||
|     ] | ||||
| @@ -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, | ||||
|   | ||||
| @@ -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', | ||||
|   | ||||
| @@ -35,16 +35,22 @@ | ||||
|             <td>Part name</td> | ||||
|             <td>{{ part.full_name }}</td> | ||||
|         </tr> | ||||
|         <tr> | ||||
|             <td>Description</td> | ||||
|             <td>{{ part.description }}</td> | ||||
|         </tr> | ||||
|         {% if part.IPN %} | ||||
|         <tr> | ||||
|             <td>IPN</td> | ||||
|             <td>{{ part.IPN }}</td> | ||||
|         </tr> | ||||
|         {% endif %} | ||||
|         <tr> | ||||
|             <td>Description</td> | ||||
|             <td>{{ part.description }}</td> | ||||
|         </tr> | ||||
|         {% if part.keywords %} | ||||
|         <tr> | ||||
|             <td>Keywords</td> | ||||
|             <td>{{ part.keywords }}</td> | ||||
|         </tr> | ||||
|         {% endif %} | ||||
|         {% if part.URL %} | ||||
|         <tr> | ||||
|             <td>URL</td> | ||||
|   | ||||
| @@ -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 | ||||
|  | ||||
|   | ||||
| @@ -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 = "<span class='label label-success'>" + value + "</span>"; | ||||
|                 } | ||||
|                 else | ||||
|                 { | ||||
|                     if (!value) { | ||||
|                         value = 'No Stock'; | ||||
|                     } | ||||
|                     text = "<span class='label label-warning'>" + value + "</span>"; | ||||
|                 } | ||||
|                  | ||||
|                 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 = "<span class='label label-success'>" + value + "</span>"; | ||||
|                 } | ||||
|                 else | ||||
|                 { | ||||
|                     text = "<span class='label label-warning'>" + value + "</span>"; | ||||
|                 } | ||||
|                  | ||||
|                 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, | ||||
|   | ||||
| @@ -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 + "<span class='label label-warning' style='float: right;'>INACTIVE</span>"; | ||||
|                     } | ||||
| @@ -160,7 +159,7 @@ function loadPartTable(table, url, options={}) { | ||||
|                         return renderLink(value, row.url + 'stock/'); | ||||
|                     } | ||||
|                     else { | ||||
|                         return "<span class='label label-warning'>No stock</span>"; | ||||
|                         return "<span class='label label-warning'>No Stock</span>"; | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user