mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge pull request #1778 from SchrodingersGat/build-forms
Refactor BuildOrderEdit form
This commit is contained in:
commit
433098ce6e
@ -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
|
||||
|
@ -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)
|
||||
|
||||
|
20
InvenTree/build/migrations/0030_alter_build_reference.py
Normal file
20
InvenTree/build/migrations/0030_alter_build_reference.py
Normal 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'),
|
||||
),
|
||||
]
|
@ -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
|
||||
]
|
||||
|
@ -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',
|
||||
|
@ -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() {
|
||||
|
@ -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
|
||||
|
@ -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 """
|
||||
|
||||
|
@ -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'),
|
||||
|
@ -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 """
|
||||
|
||||
|
@ -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
|
||||
|
@ -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([
|
||||
|
@ -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)
|
||||
|
@ -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 """
|
||||
|
||||
|
@ -34,9 +34,7 @@
|
||||
{{ block.super }}
|
||||
$("#start-build").click(function() {
|
||||
newBuildOrder({
|
||||
data: {
|
||||
part: {{ part.id }},
|
||||
}
|
||||
part: {{ part.pk }},
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1,5 +0,0 @@
|
||||
{% extends "modal_delete_form.html" %}
|
||||
|
||||
{% block pre_form_content %}
|
||||
Are you sure you want to remove this parameter?
|
||||
{% endblock %}
|
@ -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 %}
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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 = [
|
||||
|
@ -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 """
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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',
|
||||
},
|
||||
|
@ -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,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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) {
|
||||
|
||||
|
@ -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
|
||||
*
|
||||
|
Loading…
Reference in New Issue
Block a user