Merge pull request #1778 from SchrodingersGat/build-forms

Refactor BuildOrderEdit form
This commit is contained in:
Oliver 2021-07-09 09:02:39 +10:00 committed by GitHub
commit 433098ce6e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 431 additions and 368 deletions

View File

@ -153,6 +153,11 @@ class InvenTreeMetadata(SimpleMetadata):
if 'default' not in field_info and not field.default == empty:
field_info['default'] = field.get_default()
# Force non-nullable fields to read as "required"
# (even if there is a default value!)
if not field.allow_null and not (hasattr(field, 'allow_blank') and field.allow_blank):
field_info['required'] = True
# Introspect writable related fields
if field_info['type'] == 'field' and not field_info['read_only']:
@ -166,7 +171,12 @@ class InvenTreeMetadata(SimpleMetadata):
if model:
# Mark this field as "related", and point to the URL where we can get the data!
field_info['type'] = 'related field'
field_info['api_url'] = model.get_api_url()
field_info['model'] = model._meta.model_name
# Special case for 'user' model
if field_info['model'] == 'user':
field_info['api_url'] = '/api/user/'
else:
field_info['api_url'] = model.get_api_url()
return field_info

View File

@ -5,11 +5,13 @@ JSON API for the Build app
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django_filters.rest_framework import DjangoFilterBackend
from django.conf.urls import url, include
from rest_framework import filters
from rest_framework import generics
from django.conf.urls import url, include
from django_filters.rest_framework import DjangoFilterBackend
from django_filters import rest_framework as rest_filters
from InvenTree.api import AttachmentMixin
from InvenTree.helpers import str2bool, isNull
@ -19,6 +21,36 @@ from .models import Build, BuildItem, BuildOrderAttachment
from .serializers import BuildAttachmentSerializer, BuildSerializer, BuildItemSerializer
class BuildFilter(rest_filters.FilterSet):
"""
Custom filterset for BuildList API endpoint
"""
status = rest_filters.NumberFilter(label='Status')
active = rest_filters.BooleanFilter(label='Build is active', method='filter_active')
def filter_active(self, queryset, name, value):
if str2bool(value):
queryset = queryset.filter(status__in=BuildStatus.ACTIVE_CODES)
else:
queryset = queryset.exclude(status__in=BuildStatus.ACTIVE_CODES)
return queryset
overdue = rest_filters.BooleanFilter(label='Build is overdue', method='filter_overdue')
def filter_overdue(self, queryset, name, value):
if str2bool(value):
queryset = queryset.filter(Build.OVERDUE_FILTER)
else:
queryset = queryset.exclude(Build.OVERDUE_FILTER)
return queryset
class BuildList(generics.ListCreateAPIView):
""" API endpoint for accessing a list of Build objects.
@ -28,6 +60,7 @@ class BuildList(generics.ListCreateAPIView):
queryset = Build.objects.all()
serializer_class = BuildSerializer
filterset_class = BuildFilter
filter_backends = [
DjangoFilterBackend,
@ -97,34 +130,6 @@ class BuildList(generics.ListCreateAPIView):
except (ValueError, Build.DoesNotExist):
pass
# Filter by build status?
status = params.get('status', None)
if status is not None:
queryset = queryset.filter(status=status)
# Filter by "pending" status
active = params.get('active', None)
if active is not None:
active = str2bool(active)
if active:
queryset = queryset.filter(status__in=BuildStatus.ACTIVE_CODES)
else:
queryset = queryset.exclude(status__in=BuildStatus.ACTIVE_CODES)
# Filter by "overdue" status?
overdue = params.get('overdue', None)
if overdue is not None:
overdue = str2bool(overdue)
if overdue:
queryset = queryset.filter(Build.OVERDUE_FILTER)
else:
queryset = queryset.exclude(Build.OVERDUE_FILTER)
# Filter by associated part?
part = params.get('part', None)

View File

@ -0,0 +1,20 @@
# Generated by Django 3.2.4 on 2021-07-08 14:14
import InvenTree.validators
import build.models
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('build', '0029_auto_20210601_1525'),
]
operations = [
migrations.AlterField(
model_name='build',
name='reference',
field=models.CharField(default=build.models.get_next_build_number, help_text='Build Order Reference', max_length=64, unique=True, validators=[InvenTree.validators.validate_build_order_reference], verbose_name='Reference'),
),
]

View File

@ -21,6 +21,7 @@ from django.core.validators import MinValueValidator
from markdownx.models import MarkdownxField
from mptt.models import MPTTModel, TreeForeignKey
from mptt.exceptions import InvalidMove
from InvenTree.status_codes import BuildStatus, StockStatus, StockHistoryCode
from InvenTree.helpers import increment, getSetting, normalize, MakeBarcode
@ -37,6 +38,35 @@ from part import models as PartModels
from users import models as UserModels
def get_next_build_number():
"""
Returns the next available BuildOrder reference number
"""
if Build.objects.count() == 0:
return
build = Build.objects.exclude(reference=None).last()
attempts = set([build.reference])
reference = build.reference
while 1:
reference = increment(reference)
if reference in attempts:
# Escape infinite recursion
return reference
if Build.objects.filter(reference=reference).exists():
attempts.add(reference)
else:
break
return reference
class Build(MPTTModel):
""" A Build object organises the creation of new StockItem objects from other existing StockItem objects.
@ -60,11 +90,20 @@ class Build(MPTTModel):
responsible: User (or group) responsible for completing the build
"""
OVERDUE_FILTER = Q(status__in=BuildStatus.ACTIVE_CODES) & ~Q(target_date=None) & Q(target_date__lte=datetime.now().date())
@staticmethod
def get_api_url():
return reverse('api-build-list')
OVERDUE_FILTER = Q(status__in=BuildStatus.ACTIVE_CODES) & ~Q(target_date=None) & Q(target_date__lte=datetime.now().date())
def save(self, *args, **kwargs):
try:
super().save(*args, **kwargs)
except InvalidMove:
raise ValidationError({
'parent': _('Invalid choice for parent build'),
})
class Meta:
verbose_name = _("Build Order")
@ -130,6 +169,7 @@ class Build(MPTTModel):
blank=False,
help_text=_('Build Order Reference'),
verbose_name=_('Reference'),
default=get_next_build_number,
validators=[
validate_build_order_reference
]

View File

@ -62,7 +62,7 @@ class BuildSerializer(InvenTreeModelSerializer):
return queryset
def __init__(self, *args, **kwargs):
part_detail = kwargs.pop('part_detail', False)
part_detail = kwargs.pop('part_detail', True)
super().__init__(*args, **kwargs)
@ -75,9 +75,12 @@ class BuildSerializer(InvenTreeModelSerializer):
'pk',
'url',
'title',
'batch',
'creation_date',
'completed',
'completion_date',
'destination',
'parent',
'part',
'part_detail',
'overdue',
@ -87,6 +90,7 @@ class BuildSerializer(InvenTreeModelSerializer):
'status',
'status_text',
'target_date',
'take_from',
'notes',
'link',
'issued_by',

View File

@ -196,10 +196,7 @@ src="{% static 'img/blank_image.png' %}"
});
$("#build-edit").click(function () {
launchModalForm("{% url 'build-edit' build.id %}",
{
reload: true
});
editBuildOrder({{ build.pk }});
});
$("#build-cancel").click(function() {

View File

@ -5,10 +5,11 @@ from django.test import TestCase
from django.core.exceptions import ValidationError
from django.db.utils import IntegrityError
from build.models import Build, BuildItem
from InvenTree import status_codes as status
from build.models import Build, BuildItem, get_next_build_number
from stock.models import StockItem
from part.models import Part, BomItem
from InvenTree import status_codes as status
class BuildTest(TestCase):
@ -80,8 +81,14 @@ class BuildTest(TestCase):
quantity=2
)
ref = get_next_build_number()
if ref is None:
ref = "0001"
# Create a "Build" object to make 10x objects
self.build = Build.objects.create(
reference=ref,
title="This is a build",
part=self.assembly,
quantity=10

View File

@ -252,23 +252,6 @@ class TestBuildViews(TestCase):
self.assertIn(build.title, content)
def test_build_create(self):
""" Test the build creation view (ajax form) """
url = reverse('build-create')
# Create build without specifying part
response = self.client.get(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertEqual(response.status_code, 200)
# Create build with valid part
response = self.client.get(url, {'part': 1}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertEqual(response.status_code, 200)
# Create build with invalid part
response = self.client.get(url, {'part': 9999}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertEqual(response.status_code, 200)
def test_build_allocate(self):
""" Test the part allocation view for a Build """

View File

@ -7,7 +7,6 @@ from django.conf.urls import url, include
from . import views
build_detail_urls = [
url(r'^edit/', views.BuildUpdate.as_view(), name='build-edit'),
url(r'^allocate/', views.BuildAllocate.as_view(), name='build-allocate'),
url(r'^cancel/', views.BuildCancel.as_view(), name='build-cancel'),
url(r'^delete/', views.BuildDelete.as_view(), name='build-delete'),
@ -36,8 +35,6 @@ build_urls = [
url('^new/', views.BuildItemCreate.as_view(), name='build-item-create'),
])),
url(r'new/', views.BuildCreate.as_view(), name='build-create'),
url(r'^(?P<pk>\d+)/', include(build_detail_urls)),
url(r'.*$', views.BuildIndex.as_view(), name='build-index'),

View File

@ -667,126 +667,6 @@ class BuildAllocate(InvenTreeRoleMixin, DetailView):
return context
class BuildCreate(AjaxCreateView):
"""
View to create a new Build object
"""
model = Build
context_object_name = 'build'
form_class = forms.EditBuildForm
ajax_form_title = _('New Build Order')
ajax_template_name = 'modal_form.html'
def get_form(self):
form = super().get_form()
if form['part'].value():
form.fields['part'].widget = HiddenInput()
return form
def get_initial(self):
""" Get initial parameters for Build creation.
If 'part' is specified in the GET query, initialize the Build with the specified Part
"""
initials = super(BuildCreate, self).get_initial().copy()
initials['parent'] = self.request.GET.get('parent', None)
# User has provided a SalesOrder ID
initials['sales_order'] = self.request.GET.get('sales_order', None)
initials['quantity'] = self.request.GET.get('quantity', 1)
part = self.request.GET.get('part', None)
if part:
try:
part = Part.objects.get(pk=part)
# User has provided a Part ID
initials['part'] = part
initials['destination'] = part.get_default_location()
to_order = part.quantity_to_order
if to_order < 1:
to_order = 1
initials['quantity'] = to_order
except (ValueError, Part.DoesNotExist):
pass
initials['reference'] = Build.getNextBuildNumber()
# Pre-fill the issued_by user
initials['issued_by'] = self.request.user
return initials
def get_data(self):
return {
'success': _('Created new build'),
}
def validate(self, build, form, **kwargs):
"""
Perform extra form validation.
- If part is trackable, check that either batch or serial numbers are calculated
By this point form.is_valid() has been executed
"""
pass
class BuildUpdate(AjaxUpdateView):
""" View for editing a Build object """
model = Build
form_class = forms.EditBuildForm
context_object_name = 'build'
ajax_form_title = _('Edit Build Order Details')
ajax_template_name = 'modal_form.html'
def get_form(self):
form = super().get_form()
build = self.get_object()
# Fields which are included in the form, but hidden
hidden = [
'parent',
'sales_order',
]
if build.is_complete:
# Fields which cannot be edited once the build has been completed
hidden += [
'part',
'quantity',
'batch',
'take_from',
'destination',
]
for field in hidden:
form.fields[field].widget = HiddenInput()
return form
def get_data(self):
return {
'info': _('Edited build'),
}
class BuildDelete(AjaxDeleteView):
""" View to delete a build """

View File

@ -43,8 +43,10 @@ def get_next_po_number():
attempts = set([order.reference])
reference = order.reference
while 1:
reference = increment(order.reference)
reference = increment(reference)
if reference in attempts:
# Escape infinite recursion
@ -70,8 +72,10 @@ def get_next_so_number():
attempts = set([order.reference])
reference = order.reference
while 1:
reference = increment(order.reference)
reference = increment(reference)
if reference in attempts:
# Escape infinite recursion

View File

@ -1115,10 +1115,10 @@ part_api_urls = [
# Base URL for PartParameter API endpoints
url(r'^parameter/', include([
url(r'^template/$', PartParameterTemplateList.as_view(), name='api-part-param-template-list'),
url(r'^template/$', PartParameterTemplateList.as_view(), name='api-part-parameter-template-list'),
url(r'^(?P<pk>\d+)/', PartParameterDetail.as_view(), name='api-part-param-detail'),
url(r'^.*$', PartParameterList.as_view(), name='api-part-param-list'),
url(r'^(?P<pk>\d+)/', PartParameterDetail.as_view(), name='api-part-parameter-detail'),
url(r'^.*$', PartParameterList.as_view(), name='api-part-parameter-list'),
])),
url(r'^thumbs/', include([

View File

@ -2164,7 +2164,7 @@ class PartParameterTemplate(models.Model):
@staticmethod
def get_api_url():
return reverse('api-part-param-template-list')
return reverse('api-part-parameter-template-list')
def __str__(self):
s = str(self.name)
@ -2205,7 +2205,7 @@ class PartParameter(models.Model):
@staticmethod
def get_api_url():
return reverse('api-part-param-list')
return reverse('api-part-parameter-list')
def __str__(self):
# String representation of a PartParameter (used in the admin interface)

View File

@ -508,19 +508,6 @@ class BomItemSerializer(InvenTreeModelSerializer):
]
class PartParameterSerializer(InvenTreeModelSerializer):
""" JSON serializers for the PartParameter model """
class Meta:
model = PartParameter
fields = [
'pk',
'part',
'template',
'data'
]
class PartParameterTemplateSerializer(InvenTreeModelSerializer):
""" JSON serializer for the PartParameterTemplate model """
@ -533,6 +520,22 @@ class PartParameterTemplateSerializer(InvenTreeModelSerializer):
]
class PartParameterSerializer(InvenTreeModelSerializer):
""" JSON serializers for the PartParameter model """
template_detail = PartParameterTemplateSerializer(source='template', many=False, read_only=True)
class Meta:
model = PartParameter
fields = [
'pk',
'part',
'template',
'template_detail',
'data'
]
class CategoryParameterTemplateSerializer(InvenTreeModelSerializer):
""" Serializer for PartCategoryParameterTemplate """

View File

@ -34,9 +34,7 @@
{{ block.super }}
$("#start-build").click(function() {
newBuildOrder({
data: {
part: {{ part.id }},
}
part: {{ part.pk }},
});
});

View File

@ -1,5 +0,0 @@
{% extends "modal_delete_form.html" %}
{% block pre_form_content %}
Are you sure you want to remove this parameter?
{% endblock %}

View File

@ -21,54 +21,43 @@
</div>
</div>
<table id='param-table' class='table table-condensed table-striped' data-toolbar='#button-toolbar'>
<thead>
<tr>
<th data-field='name' data-serachable='true'>{% trans "Name" %}</th>
<th data-field='value' data-searchable='true'>{% trans "Value" %}</th>
<th data-field='units' data-searchable='true'>{% trans "Units" %}</th>
</tr>
</thead>
<tbody>
{% for param in part.get_parameters %}
<tr>
<td>{{ param.template.name }}</td>
<td>{{ param.data }}</td>
<td>
{{ param.template.units }}
<div class='btn-group' style='float: right;'>
{% if roles.part.change %}
<button title='{% trans "Edit" %}' class='btn btn-default btn-glyph param-edit' url="{% url 'part-param-edit' param.id %}" type='button'><span class='fas fa-edit'/></button>
{% endif %}
{% if roles.part.change %}
<button title='{% trans "Delete" %}' class='btn btn-default btn-glyph param-delete' url="{% url 'part-param-delete' param.id %}" type='button'><span class='fas fa-trash-alt icon-red'/></button>
{% endif %}
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<table id='parameter-table' class='table table-condensed table-striped' data-toolbar="#button-toolbar"></table>
{% endblock %}
{% block js_ready %}
{{ block.super }}
loadPartParameterTable(
'#parameter-table',
'{% url "api-part-parameter-list" %}',
{
params: {
part: {{ part.pk }},
}
}
);
$('#param-table').inventreeTable({
});
{% if roles.part.add %}
$('#param-create').click(function() {
launchModalForm("{% url 'part-param-create' %}?part={{ part.id }}", {
reload: true,
secondary: [{
field: 'template',
label: '{% trans "New Template" %}',
title: '{% trans "Create New Parameter Template" %}',
url: "{% url 'part-param-template-create' %}"
}],
constructForm('{% url "api-part-parameter-list" %}', {
method: 'POST',
fields: {
part: {
value: {{ part.pk }},
hidden: true,
},
template: {},
data: {},
},
title: '{% trans "Add Parameter" %}',
onSuccess: function() {
$('#parameter-table').bootstrapTable('refresh');
}
});
});
{% endif %}

View File

@ -805,7 +805,7 @@ class PartParameterTest(InvenTreeAPITestCase):
Test for listing part parameters
"""
url = reverse('api-part-param-list')
url = reverse('api-part-parameter-list')
response = self.client.get(url, format='json')
@ -838,7 +838,7 @@ class PartParameterTest(InvenTreeAPITestCase):
Test that we can create a param via the API
"""
url = reverse('api-part-param-list')
url = reverse('api-part-parameter-list')
response = self.client.post(
url,
@ -860,7 +860,7 @@ class PartParameterTest(InvenTreeAPITestCase):
Tests for the PartParameter detail endpoint
"""
url = reverse('api-part-param-detail', kwargs={'pk': 5})
url = reverse('api-part-parameter-detail', kwargs={'pk': 5})
response = self.client.get(url)

View File

@ -33,10 +33,6 @@ part_parameter_urls = [
url(r'^template/new/', views.PartParameterTemplateCreate.as_view(), name='part-param-template-create'),
url(r'^template/(?P<pk>\d+)/edit/', views.PartParameterTemplateEdit.as_view(), name='part-param-template-edit'),
url(r'^template/(?P<pk>\d+)/delete/', views.PartParameterTemplateDelete.as_view(), name='part-param-template-edit'),
url(r'^new/', views.PartParameterCreate.as_view(), name='part-param-create'),
url(r'^(?P<pk>\d+)/edit/', views.PartParameterEdit.as_view(), name='part-param-edit'),
url(r'^(?P<pk>\d+)/delete/', views.PartParameterDelete.as_view(), name='part-param-delete'),
]
part_detail_urls = [

View File

@ -32,7 +32,7 @@ from rapidfuzz import fuzz
from decimal import Decimal, InvalidOperation
from .models import PartCategory, Part, PartRelated
from .models import PartParameterTemplate, PartParameter
from .models import PartParameterTemplate
from .models import PartCategoryParameterTemplate
from .models import BomItem
from .models import match_part_names
@ -2257,78 +2257,6 @@ class PartParameterTemplateDelete(AjaxDeleteView):
ajax_form_title = _("Delete Part Parameter Template")
class PartParameterCreate(AjaxCreateView):
""" View for creating a new PartParameter """
model = PartParameter
form_class = part_forms.EditPartParameterForm
ajax_form_title = _('Create Part Parameter')
def get_initial(self):
initials = {}
part_id = self.request.GET.get('part', None)
if part_id:
try:
initials['part'] = Part.objects.get(pk=part_id)
except (Part.DoesNotExist, ValueError):
pass
return initials
def get_form(self):
""" Return the form object.
- Hide the 'Part' field (specified in URL)
- Limit the 'Template' options (to avoid duplicates)
"""
form = super().get_form()
part_id = self.request.GET.get('part', None)
if part_id:
try:
part = Part.objects.get(pk=part_id)
form.fields['part'].widget = HiddenInput()
query = form.fields['template'].queryset
query = query.exclude(id__in=[param.template.id for param in part.parameters.all()])
form.fields['template'].queryset = query
except (Part.DoesNotExist, ValueError):
pass
return form
class PartParameterEdit(AjaxUpdateView):
""" View for editing a PartParameter """
model = PartParameter
form_class = part_forms.EditPartParameterForm
ajax_form_title = _('Edit Part Parameter')
def get_form(self):
form = super().get_form()
return form
class PartParameterDelete(AjaxDeleteView):
""" View for deleting a PartParameter """
model = PartParameter
ajax_template_name = 'part/param_delete.html'
ajax_form_title = _('Delete Part Parameter')
class CategoryDetail(InvenTreeRoleMixin, DetailView):
""" Detail view for PartCategory """

View File

@ -100,7 +100,7 @@ class StockTest(TestCase):
# And there should be *no* items being build
self.assertEqual(part.quantity_being_built, 0)
build = Build.objects.create(part=part, title='A test build', quantity=1)
build = Build.objects.create(reference='12345', part=part, title='A test build', quantity=1)
# Add some stock items which are "building"
for i in range(10):

View File

@ -75,7 +75,7 @@
{{ block.super }}
$("#param-table").inventreeTable({
url: "{% url 'api-part-param-template-list' %}",
url: "{% url 'api-part-parameter-template-list' %}",
queryParams: {
ordering: 'name',
},

View File

@ -1,34 +1,72 @@
{% load i18n %}
{% load inventree_extras %}
function buildFormFields() {
return {
reference: {
prefix: "{% settings_value 'BUILDORDER_REFERENCE_PREFIX' %}",
},
title: {},
part: {},
quantity: {},
parent: {
filters: {
part_detail: true,
}
},
batch: {},
target_date: {},
take_from: {},
destination: {},
link: {
icon: 'fa-link',
},
issued_by: {
icon: 'fa-user',
},
responsible: {
icon: 'fa-users',
},
};
}
function editBuildOrder(pk, options={}) {
var fields = buildFormFields();
constructForm(`/api/build/${pk}/`, {
fields: fields,
reload: true,
title: '{% trans "Edit Build Order" %}',
});
}
function newBuildOrder(options={}) {
/* Launch modal form to create a new BuildOrder.
*/
launchModalForm(
"{% url 'build-create' %}",
{
follow: true,
data: options.data || {},
callback: [
{
field: 'part',
action: function(value) {
inventreeGet(
`/api/part/${value}/`, {},
{
success: function(response) {
var fields = buildFormFields();
//enableField('serial_numbers', response.trackable);
//clearField('serial_numbers');
}
}
);
},
}
],
}
)
if (options.part) {
fields.part.value = options.part;
}
if (options.quantity) {
fields.quantity.value = options.quantity;
}
if (options.parent) {
fields.parent.value = options.parent;
}
constructForm(`/api/build/`, {
fields: fields,
follow: true,
method: 'POST',
title: '{% trans "Create Build Order" %}'
});
}
@ -384,14 +422,10 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
var idx = $(this).closest('tr').attr('data-index');
var row = $(table).bootstrapTable('getData')[idx];
// Launch form to create a new build order
launchModalForm('{% url "build-create" %}', {
follow: true,
data: {
part: pk,
parent: buildId,
quantity: requiredQuantity(row) - sumAllocations(row),
}
newBuildOrder({
part: pk,
parent: buildId,
quantity: requiredQuantity(row) - sumAllocations(row),
});
});
@ -1092,13 +1126,9 @@ function loadBuildPartsTable(table, options={}) {
var idx = $(this).closest('tr').attr('data-index');
var row = $(table).bootstrapTable('getData')[idx];
// Launch form to create a new build order
launchModalForm('{% url "build-create" %}', {
follow: true,
data: {
part: pk,
parent: options.build,
}
newBuildOrder({
part: pk,
parent: options.build,
});
});
}

View File

@ -511,6 +511,10 @@ function insertConfirmButton(options) {
*/
function submitFormData(fields, options) {
// Immediately disable the "submit" button,
// to prevent the form being submitted multiple times!
$(options.modal).find('#modal-form-submit').prop('disabled', true);
// Form data to be uploaded to the server
// Only used if file / image upload is required
var form_data = new FormData();
@ -728,11 +732,31 @@ function handleFormSuccess(response, options) {
// Close the modal
if (!options.preventClose) {
// TODO: Actually just *delete* the modal,
// rather than hiding it!!
// Note: The modal will be deleted automatically after closing
$(options.modal).modal('hide');
}
// Display any required messages
// Should we show alerts immediately or cache them?
var cache = (options.follow && response.url) || options.redirect || options.reload;
// Display any messages
if (response && response.success) {
showAlertOrCache("alert-success", response.success, cache);
}
if (response && response.info) {
showAlertOrCache("alert-info", response.info, cache);
}
if (response && response.warning) {
showAlertOrCache("alert-warning", response.warning, cache);
}
if (response && response.danger) {
showAlertOrCache("alert-danger", response.danger, cache);
}
if (options.onSuccess) {
// Callback function
options.onSuccess(response, options);
@ -778,6 +802,9 @@ function clearFormErrors(options) {
*/
function handleFormErrors(errors, fields, options) {
// Reset the status of the "submit" button
$(options.modal).find('#modal-form-submit').prop('disabled', false);
// Remove any existing error messages from the form
clearFormErrors(options);
@ -1201,11 +1228,21 @@ function renderModelData(name, model, data, parameters, options) {
case 'partcategory':
renderer = renderPartCategory;
break;
case 'partparametertemplate':
renderer = renderPartParameterTemplate;
break;
case 'supplierpart':
renderer = renderSupplierPart;
break;
case 'build':
renderer = renderBuild;
break;
case 'owner':
renderer = renderOwner;
break;
case 'user':
renderer = renderUser;
break;
default:
break;
}

View File

@ -83,8 +83,6 @@ function createNewModal(options={}) {
// Capture "enter" key input
$(modal_name).on('keydown', 'input', function(event) {
if (event.keyCode == 13) {
event.preventDefault();
// Simulate a click on the 'Submit' button

View File

@ -70,6 +70,27 @@ function renderStockLocation(name, data, parameters, options) {
}
function renderBuild(name, data, parameters, options) {
var image = '';
if (data.part_detail && data.part_detail.thumbnail) {
image = data.part_detail.thumbnail;
} else {
image = `/static/img/blank_image.png`;
}
var html = `<img src='${image}' class='select2-thumbnail'>`;
html += `<span><b>${data.reference}</b></span> - ${data.quantity} x ${data.part_detail.full_name}`;
html += `<span class='float-right'>{% trans "Build ID" %}: ${data.pk}</span>`;
html += `<p><i>${data.title}</i></p>`;
return html;
}
// Renderer for "Part" model
function renderPart(name, data, parameters, options) {
@ -92,6 +113,18 @@ function renderPart(name, data, parameters, options) {
return html;
}
// Renderer for "User" model
function renderUser(name, data, parameters, options) {
var html = `<span>${data.username}</span>`;
if (data.first_name && data.last_name) {
html += ` - <i>${data.first_name} ${data.last_name}</i>`;
}
return html;
}
// Renderer for "Owner" model
function renderOwner(name, data, parameters, options) {
@ -133,6 +166,14 @@ function renderPartCategory(name, data, parameters, options) {
}
function renderPartParameterTemplate(name, data, parameters, options) {
var html = `<span>${data.name} - [${data.units}]</span>`;
return html;
}
// Rendered for "SupplierPart" model
function renderSupplierPart(name, data, parameters, options) {

View File

@ -220,6 +220,107 @@ function loadSimplePartTable(table, url, options={}) {
}
function loadPartParameterTable(table, url, options) {
var params = options.params || {};
// Load filters
var filters = loadTableFilters("part-parameters");
for (var key in params) {
filters[key] = params[key];
}
// setupFilterLsit("#part-parameters", $(table));
$(table).inventreeTable({
url: url,
original: params,
queryParams: filters,
name: 'partparameters',
groupBy: false,
formatNoMatches: function() { return '{% trans "No parameters found" %}'; },
columns: [
{
checkbox: true,
switchable: false,
visible: true,
},
{
field: 'name',
title: '{% trans "Name" %}',
switchable: false,
sortable: true,
formatter: function(value, row) {
return row.template_detail.name;
}
},
{
field: 'data',
title: '{% trans "Value" %}',
switchable: false,
sortable: true,
},
{
field: 'units',
title: '{% trans "Units" %}',
switchable: true,
sortable: true,
formatter: function(value, row) {
return row.template_detail.units;
}
},
{
field: 'actions',
title: '',
switchable: false,
sortable: false,
formatter: function(value, row) {
var pk = row.pk;
var html = `<div class='btn-group float-right' role='group'>`;
html += makeIconButton('fa-edit icon-blue', 'button-parameter-edit', pk, '{% trans "Edit parameter" %}');
html += makeIconButton('fa-trash-alt icon-red', 'button-parameter-delete', pk, '{% trans "Delete parameter" %}');
html += `</div>`;
return html;
}
}
],
onPostBody: function() {
// Setup button callbacks
$(table).find('.button-parameter-edit').click(function() {
var pk = $(this).attr('pk');
constructForm(`/api/part/parameter/${pk}/`, {
fields: {
data: {},
},
title: '{% trans "Edit Parameter" %}',
onSuccess: function() {
$(table).bootstrapTable('refresh');
}
});
});
$(table).find('.button-parameter-delete').click(function() {
var pk = $(this).attr('pk');
constructForm(`/api/part/parameter/${pk}/`, {
method: 'DELETE',
title: '{% trans "Delete Parameter" %}',
onSuccess: function() {
$(table).bootstrapTable('refresh');
}
});
});
}
});
}
function loadParametricPartTable(table, options={}) {
/* Load parametric table for part parameters
*