Merge pull request #332 from SchrodingersGat/improvements

Improvements
This commit is contained in:
Oliver 2019-05-14 18:36:55 +10:00 committed by GitHub
commit 57645486cc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 150 additions and 63 deletions

View File

@ -8,7 +8,7 @@ from django.utils.translation import gettext_lazy as _
def validate_part_name(value): def validate_part_name(value):
# Prevent some illegal characters in part names # Prevent some illegal characters in part names
for c in ['/', '\\', '|', '#', '$']: for c in ['|', '#', '$']:
if c in str(value): if c in str(value):
raise ValidationError( raise ValidationError(
_('Invalid character in part name') _('Invalid character in part name')

View File

@ -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): class EditBuildItemForm(HelperForm):
""" Form for adding a new BuildItem to a Build """ """ Form for adding a new BuildItem to a Build """

View File

@ -22,8 +22,8 @@ Automatically allocate stock to this build?
<tr> <tr>
<td> <td>
<a class='hover-icon'> <a class='hover-icon'>
<img class='hover-img-thumb' 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='{{ item.stock_item.part.image.url }}'> <img class='hover-img-large' src='{% if item.stock_item.part.image %}{{ item.stock_item.part.image.url }}{% endif %}'>
</a> </a>
</td> </td>
<td> <td>

View File

@ -1,3 +1,7 @@
{% extends "modal_form.html" %}
{% block pre_form_content %}
Are you sure you wish to cancel this build? Are you sure you wish to cancel this build?
{% include "modal_csrf.html" %} {% endblock %}

View File

@ -5,8 +5,6 @@ Django views for interacting with Build objects
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import unicode_literals from __future__ import unicode_literals
from django.shortcuts import get_object_or_404
from django.views.generic import DetailView, ListView from django.views.generic import DetailView, ListView
from django.forms import HiddenInput from django.forms import HiddenInput
@ -15,7 +13,8 @@ from .models import Build, BuildItem
from . import forms from . import forms
from stock.models import StockLocation, StockItem 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): class BuildIndex(ListView):
@ -41,31 +40,41 @@ class BuildIndex(ListView):
return context return context
class BuildCancel(AjaxView): class BuildCancel(AjaxUpdateView):
""" View to cancel a Build. """ View to cancel a Build.
Provides a cancellation information dialog Provides a cancellation information dialog
""" """
model = Build model = Build
ajax_template_name = 'build/cancel.html' ajax_template_name = 'build/cancel.html'
ajax_form_title = 'Cancel Build' ajax_form_title = 'Cancel Build'
context_object_name = 'build' context_object_name = 'build'
fields = [] form_class = forms.CancelBuildForm
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
""" Handle POST request. Mark the build status as CANCELLED """ """ Handle POST request. Mark the build status as CANCELLED """
build = get_object_or_404(Build, pk=self.kwargs['pk']) build = self.get_object()
form = self.get_form()
valid = form.is_valid()
confirm = str2bool(request.POST.get('confirm_cancel', False))
if confirm:
build.cancelBuild(request.user) build.cancelBuild(request.user)
else:
form.errors['confirm_cancel'] = ['Confirm build cancellation']
valid = False
return self.renderJsonResponse(request, None) data = {
'form_valid': valid,
def get_data(self):
""" Provide JSON context data. """
return {
'danger': 'Build was cancelled' 'danger': 'Build was cancelled'
} }
return self.renderJsonResponse(request, form, data=data)
class BuildAutoAllocate(AjaxUpdateView): class BuildAutoAllocate(AjaxUpdateView):
""" View to auto-allocate parts for a build. """ View to auto-allocate parts for a build.
@ -90,7 +99,7 @@ class BuildAutoAllocate(AjaxUpdateView):
context['build'] = build context['build'] = build
context['allocations'] = build.getAutoAllocations() context['allocations'] = build.getAutoAllocations()
except Build.DoesNotExist: except Build.DoesNotExist:
context['error'] = 'No matching buidl found' context['error'] = 'No matching build found'
return context return context
@ -217,7 +226,7 @@ class BuildComplete(AjaxUpdateView):
form = self.get_form() form = self.get_form()
confirm = request.POST.get('confirm', False) confirm = str2bool(request.POST.get('confirm', False))
loc_id = request.POST.get('location', None) loc_id = request.POST.get('location', None)

View File

@ -157,6 +157,7 @@ class PartList(generics.ListCreateAPIView):
'$name', '$name',
'description', 'description',
'$IPN', '$IPN',
'keywords',
] ]

View File

@ -92,9 +92,10 @@ class EditPartForm(HelperForm):
'confirm_creation', 'confirm_creation',
'category', 'category',
'name', 'name',
'IPN',
'variant', 'variant',
'description', 'description',
'IPN', 'keywords',
'URL', 'URL',
'default_location', 'default_location',
'default_supplier', 'default_supplier',
@ -118,7 +119,8 @@ class EditCategoryForm(HelperForm):
'parent', 'parent',
'name', 'name',
'description', 'description',
'default_location' 'default_location',
'default_keywords',
] ]

View 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),
),
]

View File

@ -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),
),
]

View File

@ -37,6 +37,12 @@ from company.models import Company
class PartCategory(InvenTreeTree): class PartCategory(InvenTreeTree):
""" PartCategory provides hierarchical organization of Part objects. """ 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( default_location = models.ForeignKey(
@ -46,6 +52,8 @@ class PartCategory(InvenTreeTree):
help_text='Default location for parts in this category' 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): def get_absolute_url(self):
return reverse('category-detail', kwargs={'pk': self.id}) return reverse('category-detail', kwargs={'pk': self.id})
@ -179,8 +187,9 @@ class Part(models.Model):
Attributes: Attributes:
name: Brief name for this part name: Brief name for this part
variant: Optional variant number for this part - Must be unique for the part name 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 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) IPN: Internal part number (optional)
URL: Link to an external page with more information about this part (e.g. internal Wiki) URL: Link to an external page with more information about this part (e.g. internal Wiki)
image: Image of this part 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') 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', category = models.ForeignKey(PartCategory, related_name='parts',
null=True, blank=True, null=True, blank=True,
on_delete=models.DO_NOTHING, on_delete=models.DO_NOTHING,

View File

@ -62,15 +62,16 @@ class PartSerializer(serializers.ModelSerializer):
fields = [ fields = [
'pk', 'pk',
'url', # Link to the part detail page 'url', # Link to the part detail page
'full_name',
'name',
'variant',
'image_url',
'IPN',
'URL', # Link to an external URL (optional)
'description',
'category', 'category',
'category_name', 'category_name',
'image_url',
'full_name',
'name',
'IPN',
'variant',
'description',
'keywords',
'URL',
'total_stock', 'total_stock',
'available_stock', 'available_stock',
'units', 'units',

View File

@ -35,16 +35,22 @@
<td>Part name</td> <td>Part name</td>
<td>{{ part.full_name }}</td> <td>{{ part.full_name }}</td>
</tr> </tr>
<tr>
<td>Description</td>
<td>{{ part.description }}</td>
</tr>
{% if part.IPN %} {% if part.IPN %}
<tr> <tr>
<td>IPN</td> <td>IPN</td>
<td>{{ part.IPN }}</td> <td>{{ part.IPN }}</td>
</tr> </tr>
{% endif %} {% 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 %} {% if part.URL %}
<tr> <tr>
<td>URL</td> <td>URL</td>

View File

@ -339,7 +339,9 @@ class PartCreate(AjaxCreateView):
if self.get_category_id(): if self.get_category_id():
try: 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: except PartCategory.DoesNotExist:
pass pass

View File

@ -89,11 +89,11 @@ function loadBomTable(table, options) {
// Part column // Part column
cols.push( cols.push(
{ {
field: 'sub_part_detail', field: 'sub_part_detail.full_name',
title: 'Part', title: 'Part',
sortable: true, sortable: true,
formatter: function(value, row, index, field) { 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);
} }
} }
); );
@ -116,6 +116,34 @@ function loadBomTable(table, options) {
} }
); );
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 // Part notes
cols.push( 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) // Configure the table (bootstrap-table)
table.bootstrapTable({ table.bootstrapTable({
@ -172,6 +175,7 @@ function loadBomTable(table, options) {
queryParams: function(p) { queryParams: function(p) {
return { return {
part: options.parent_id, part: options.parent_id,
ordering: 'name',
} }
}, },
columns: cols, columns: cols,

View File

@ -119,13 +119,12 @@ function loadPartTable(table, url, options={}) {
visible: false, visible: false,
}, },
{ {
field: 'name', field: 'full_name',
title: 'Part', title: 'Part',
sortable: true, sortable: true,
formatter: function(value, row, index, field) { 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) { if (!row.active) {
display = display + "<span class='label label-warning' style='float: right;'>INACTIVE</span>"; 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/'); return renderLink(value, row.url + 'stock/');
} }
else { else {
return "<span class='label label-warning'>No stock</span>"; return "<span class='label label-warning'>No Stock</span>";
} }
} }
} }