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):
# 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')

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):
""" Form for adding a new BuildItem to a Build """

View File

@ -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>

View File

@ -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 %}

View File

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

View File

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

View File

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

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):
""" 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,

View File

@ -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',

View File

@ -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>

View File

@ -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

View File

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

View File

@ -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>";
}
}
}