mirror of
https://github.com/inventree/InvenTree.git
synced 2025-04-30 04:26:44 +00:00
Merge pull request #2631 from SchrodingersGat/build-serial-number-magic
Build serial number magic
This commit is contained in:
commit
bec6d2952d
@ -232,6 +232,29 @@ class BuildUnallocate(generics.CreateAPIView):
|
|||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
|
|
||||||
|
class BuildOutputCreate(generics.CreateAPIView):
|
||||||
|
"""
|
||||||
|
API endpoint for creating new build output(s)
|
||||||
|
"""
|
||||||
|
|
||||||
|
queryset = Build.objects.none()
|
||||||
|
|
||||||
|
serializer_class = build.serializers.BuildOutputCreateSerializer
|
||||||
|
|
||||||
|
def get_serializer_context(self):
|
||||||
|
ctx = super().get_serializer_context()
|
||||||
|
|
||||||
|
ctx['request'] = self.request
|
||||||
|
ctx['to_complete'] = True
|
||||||
|
|
||||||
|
try:
|
||||||
|
ctx['build'] = Build.objects.get(pk=self.kwargs.get('pk', None))
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return ctx
|
||||||
|
|
||||||
|
|
||||||
class BuildOutputComplete(generics.CreateAPIView):
|
class BuildOutputComplete(generics.CreateAPIView):
|
||||||
"""
|
"""
|
||||||
API endpoint for completing build outputs
|
API endpoint for completing build outputs
|
||||||
@ -455,6 +478,7 @@ build_api_urls = [
|
|||||||
url(r'^(?P<pk>\d+)/', include([
|
url(r'^(?P<pk>\d+)/', include([
|
||||||
url(r'^allocate/', BuildAllocate.as_view(), name='api-build-allocate'),
|
url(r'^allocate/', BuildAllocate.as_view(), name='api-build-allocate'),
|
||||||
url(r'^complete/', BuildOutputComplete.as_view(), name='api-build-output-complete'),
|
url(r'^complete/', BuildOutputComplete.as_view(), name='api-build-output-complete'),
|
||||||
|
url(r'^create-output/', BuildOutputCreate.as_view(), name='api-build-output-create'),
|
||||||
url(r'^delete-outputs/', BuildOutputDelete.as_view(), name='api-build-output-delete'),
|
url(r'^delete-outputs/', BuildOutputDelete.as_view(), name='api-build-output-delete'),
|
||||||
url(r'^finish/', BuildFinish.as_view(), name='api-build-finish'),
|
url(r'^finish/', BuildFinish.as_view(), name='api-build-finish'),
|
||||||
url(r'^unallocate/', BuildUnallocate.as_view(), name='api-build-unallocate'),
|
url(r'^unallocate/', BuildUnallocate.as_view(), name='api-build-unallocate'),
|
||||||
|
@ -14,51 +14,6 @@ from InvenTree.forms import HelperForm
|
|||||||
from .models import Build
|
from .models import Build
|
||||||
|
|
||||||
|
|
||||||
class BuildOutputCreateForm(HelperForm):
|
|
||||||
"""
|
|
||||||
Form for creating a new build output.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
|
|
||||||
build = kwargs.pop('build', None)
|
|
||||||
|
|
||||||
if build:
|
|
||||||
self.field_placeholder['serial_numbers'] = build.part.getSerialNumberString()
|
|
||||||
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
field_prefix = {
|
|
||||||
'serial_numbers': 'fa-hashtag',
|
|
||||||
}
|
|
||||||
|
|
||||||
output_quantity = forms.IntegerField(
|
|
||||||
label=_('Quantity'),
|
|
||||||
help_text=_('Enter quantity for build output'),
|
|
||||||
)
|
|
||||||
|
|
||||||
serial_numbers = forms.CharField(
|
|
||||||
label=_('Serial Numbers'),
|
|
||||||
required=False,
|
|
||||||
help_text=_('Enter serial numbers for build outputs'),
|
|
||||||
)
|
|
||||||
|
|
||||||
confirm = forms.BooleanField(
|
|
||||||
required=True,
|
|
||||||
label=_('Confirm'),
|
|
||||||
help_text=_('Confirm creation of build output'),
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Build
|
|
||||||
fields = [
|
|
||||||
'output_quantity',
|
|
||||||
'batch',
|
|
||||||
'serial_numbers',
|
|
||||||
'confirm',
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class CancelBuildForm(HelperForm):
|
class CancelBuildForm(HelperForm):
|
||||||
""" Form for cancelling a build """
|
""" Form for cancelling a build """
|
||||||
|
|
||||||
|
@ -646,11 +646,13 @@ class Build(MPTTModel, ReferenceIndexingMixin):
|
|||||||
batch: Override batch code
|
batch: Override batch code
|
||||||
serials: Serial numbers
|
serials: Serial numbers
|
||||||
location: Override location
|
location: Override location
|
||||||
|
auto_allocate: Automatically allocate stock with matching serial numbers
|
||||||
"""
|
"""
|
||||||
|
|
||||||
batch = kwargs.get('batch', self.batch)
|
batch = kwargs.get('batch', self.batch)
|
||||||
location = kwargs.get('location', self.destination)
|
location = kwargs.get('location', self.destination)
|
||||||
serials = kwargs.get('serials', None)
|
serials = kwargs.get('serials', None)
|
||||||
|
auto_allocate = kwargs.get('auto_allocate', False)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Determine if we can create a single output (with quantity > 0),
|
Determine if we can create a single output (with quantity > 0),
|
||||||
@ -672,6 +674,9 @@ class Build(MPTTModel, ReferenceIndexingMixin):
|
|||||||
Create multiple build outputs with a single quantity of 1
|
Create multiple build outputs with a single quantity of 1
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# Quantity *must* be an integer at this point!
|
||||||
|
quantity = int(quantity)
|
||||||
|
|
||||||
for ii in range(quantity):
|
for ii in range(quantity):
|
||||||
|
|
||||||
if serials:
|
if serials:
|
||||||
@ -679,7 +684,7 @@ class Build(MPTTModel, ReferenceIndexingMixin):
|
|||||||
else:
|
else:
|
||||||
serial = None
|
serial = None
|
||||||
|
|
||||||
StockModels.StockItem.objects.create(
|
output = StockModels.StockItem.objects.create(
|
||||||
quantity=1,
|
quantity=1,
|
||||||
location=location,
|
location=location,
|
||||||
part=self.part,
|
part=self.part,
|
||||||
@ -689,6 +694,37 @@ class Build(MPTTModel, ReferenceIndexingMixin):
|
|||||||
is_building=True,
|
is_building=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if auto_allocate and serial is not None:
|
||||||
|
|
||||||
|
# Get a list of BomItem objects which point to "trackable" parts
|
||||||
|
|
||||||
|
for bom_item in self.part.get_trackable_parts():
|
||||||
|
|
||||||
|
parts = bom_item.get_valid_parts_for_allocation()
|
||||||
|
|
||||||
|
for part in parts:
|
||||||
|
|
||||||
|
items = StockModels.StockItem.objects.filter(
|
||||||
|
part=part,
|
||||||
|
serial=str(serial),
|
||||||
|
quantity=1,
|
||||||
|
).filter(StockModels.StockItem.IN_STOCK_FILTER)
|
||||||
|
|
||||||
|
"""
|
||||||
|
Test if there is a matching serial number!
|
||||||
|
"""
|
||||||
|
if items.exists() and items.count() == 1:
|
||||||
|
stock_item = items[0]
|
||||||
|
|
||||||
|
# Allocate the stock item
|
||||||
|
BuildItem.objects.create(
|
||||||
|
build=self,
|
||||||
|
bom_item=bom_item,
|
||||||
|
stock_item=stock_item,
|
||||||
|
quantity=quantity,
|
||||||
|
install_into=output,
|
||||||
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
"""
|
"""
|
||||||
Create a single build output of the given quantity
|
Create a single build output of the given quantity
|
||||||
|
@ -19,6 +19,7 @@ from InvenTree.serializers import InvenTreeModelSerializer, InvenTreeAttachmentS
|
|||||||
from InvenTree.serializers import UserSerializerBrief, ReferenceIndexingSerializerMixin
|
from InvenTree.serializers import UserSerializerBrief, ReferenceIndexingSerializerMixin
|
||||||
|
|
||||||
import InvenTree.helpers
|
import InvenTree.helpers
|
||||||
|
from InvenTree.helpers import extract_serial_numbers
|
||||||
from InvenTree.serializers import InvenTreeDecimalField
|
from InvenTree.serializers import InvenTreeDecimalField
|
||||||
from InvenTree.status_codes import StockStatus
|
from InvenTree.status_codes import StockStatus
|
||||||
|
|
||||||
@ -170,6 +171,137 @@ class BuildOutputSerializer(serializers.Serializer):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class BuildOutputCreateSerializer(serializers.Serializer):
|
||||||
|
"""
|
||||||
|
Serializer for creating a new BuildOutput against a BuildOrder.
|
||||||
|
|
||||||
|
URL pattern is "/api/build/<pk>/create-output/", where <pk> is the PK of a Build.
|
||||||
|
|
||||||
|
The Build object is provided to the serializer context.
|
||||||
|
"""
|
||||||
|
|
||||||
|
quantity = serializers.DecimalField(
|
||||||
|
max_digits=15,
|
||||||
|
decimal_places=5,
|
||||||
|
min_value=0,
|
||||||
|
required=True,
|
||||||
|
label=_('Quantity'),
|
||||||
|
help_text=_('Enter quantity for build output'),
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_build(self):
|
||||||
|
return self.context["build"]
|
||||||
|
|
||||||
|
def get_part(self):
|
||||||
|
return self.get_build().part
|
||||||
|
|
||||||
|
def validate_quantity(self, quantity):
|
||||||
|
|
||||||
|
if quantity < 0:
|
||||||
|
raise ValidationError(_("Quantity must be greater than zero"))
|
||||||
|
|
||||||
|
part = self.get_part()
|
||||||
|
|
||||||
|
if int(quantity) != quantity:
|
||||||
|
# Quantity must be an integer value if the part being built is trackable
|
||||||
|
if part.trackable:
|
||||||
|
raise ValidationError(_("Integer quantity required for trackable parts"))
|
||||||
|
|
||||||
|
if part.has_trackable_parts():
|
||||||
|
raise ValidationError(_("Integer quantity required, as the bill of materials contains tracakble parts"))
|
||||||
|
|
||||||
|
return quantity
|
||||||
|
|
||||||
|
batch_code = serializers.CharField(
|
||||||
|
required=False,
|
||||||
|
allow_blank=True,
|
||||||
|
label=_('Batch Code'),
|
||||||
|
help_text=_('Batch code for this build output'),
|
||||||
|
)
|
||||||
|
|
||||||
|
serial_numbers = serializers.CharField(
|
||||||
|
allow_blank=True,
|
||||||
|
required=False,
|
||||||
|
label=_('Serial Numbers'),
|
||||||
|
help_text=_('Enter serial numbers for build outputs'),
|
||||||
|
)
|
||||||
|
|
||||||
|
def validate_serial_numbers(self, serial_numbers):
|
||||||
|
|
||||||
|
serial_numbers = serial_numbers.strip()
|
||||||
|
|
||||||
|
# TODO: Field level validation necessary here?
|
||||||
|
return serial_numbers
|
||||||
|
|
||||||
|
auto_allocate = serializers.BooleanField(
|
||||||
|
required=False,
|
||||||
|
default=False,
|
||||||
|
label=_('Auto Allocate Serial Numbers'),
|
||||||
|
help_text=_('Automatically allocate required items with matching serial numbers'),
|
||||||
|
)
|
||||||
|
|
||||||
|
def validate(self, data):
|
||||||
|
"""
|
||||||
|
Perform form validation
|
||||||
|
"""
|
||||||
|
|
||||||
|
part = self.get_part()
|
||||||
|
|
||||||
|
# Cache a list of serial numbers (to be used in the "save" method)
|
||||||
|
self.serials = None
|
||||||
|
|
||||||
|
quantity = data['quantity']
|
||||||
|
serial_numbers = data.get('serial_numbers', '')
|
||||||
|
|
||||||
|
if serial_numbers:
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.serials = extract_serial_numbers(serial_numbers, quantity, part.getLatestSerialNumberInt())
|
||||||
|
except DjangoValidationError as e:
|
||||||
|
raise ValidationError({
|
||||||
|
'serial_numbers': e.messages,
|
||||||
|
})
|
||||||
|
|
||||||
|
# Check for conflicting serial numbesr
|
||||||
|
existing = []
|
||||||
|
|
||||||
|
for serial in self.serials:
|
||||||
|
if part.checkIfSerialNumberExists(serial):
|
||||||
|
existing.append(serial)
|
||||||
|
|
||||||
|
if len(existing) > 0:
|
||||||
|
|
||||||
|
msg = _("The following serial numbers already exist")
|
||||||
|
msg += " : "
|
||||||
|
msg += ",".join([str(e) for e in existing])
|
||||||
|
|
||||||
|
raise ValidationError({
|
||||||
|
'serial_numbers': msg,
|
||||||
|
})
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
def save(self):
|
||||||
|
"""
|
||||||
|
Generate the new build output(s)
|
||||||
|
"""
|
||||||
|
|
||||||
|
data = self.validated_data
|
||||||
|
|
||||||
|
quantity = data['quantity']
|
||||||
|
batch_code = data.get('batch_code', '')
|
||||||
|
auto_allocate = data.get('auto_allocate', False)
|
||||||
|
|
||||||
|
build = self.get_build()
|
||||||
|
|
||||||
|
build.create_build_output(
|
||||||
|
quantity,
|
||||||
|
serials=self.serials,
|
||||||
|
batch=batch_code,
|
||||||
|
auto_allocate=auto_allocate,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class BuildOutputDeleteSerializer(serializers.Serializer):
|
class BuildOutputDeleteSerializer(serializers.Serializer):
|
||||||
"""
|
"""
|
||||||
DRF serializer for deleting (cancelling) one or more build outputs
|
DRF serializer for deleting (cancelling) one or more build outputs
|
||||||
|
@ -1,20 +0,0 @@
|
|||||||
{% extends "modal_form.html" %}
|
|
||||||
{% load i18n %}
|
|
||||||
{% block pre_form_content %}
|
|
||||||
|
|
||||||
{% if build.part.has_trackable_parts %}
|
|
||||||
<div class='alert alert-block alert-warning'>
|
|
||||||
{% trans "The Bill of Materials contains trackable parts" %}<br>
|
|
||||||
{% trans "Build outputs must be generated individually." %}<br>
|
|
||||||
{% trans "Multiple build outputs will be created based on the quantity specified." %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if build.part.trackable %}
|
|
||||||
<div class='alert alert-block alert-info'>
|
|
||||||
{% trans "Trackable parts can have serial numbers specified" %}<br>
|
|
||||||
{% trans "Enter serial numbers to generate multiple single build outputs" %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% endblock %}
|
|
@ -321,9 +321,11 @@
|
|||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
|
|
||||||
$('#btn-create-output').click(function() {
|
$('#btn-create-output').click(function() {
|
||||||
launchModalForm('{% url "build-output-create" build.id %}',
|
|
||||||
|
createBuildOutput(
|
||||||
|
{{ build.pk }},
|
||||||
{
|
{
|
||||||
reload: true,
|
trackable_parts: {% if build.part.has_trackable_parts %}true{% else %}false{% endif%},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -9,7 +9,6 @@ from . import views
|
|||||||
build_detail_urls = [
|
build_detail_urls = [
|
||||||
url(r'^cancel/', views.BuildCancel.as_view(), name='build-cancel'),
|
url(r'^cancel/', views.BuildCancel.as_view(), name='build-cancel'),
|
||||||
url(r'^delete/', views.BuildDelete.as_view(), name='build-delete'),
|
url(r'^delete/', views.BuildDelete.as_view(), name='build-delete'),
|
||||||
url(r'^create-output/', views.BuildOutputCreate.as_view(), name='build-output-create'),
|
|
||||||
|
|
||||||
url(r'^.*$', views.BuildDetail.as_view(), name='build-detail'),
|
url(r'^.*$', views.BuildDetail.as_view(), name='build-detail'),
|
||||||
]
|
]
|
||||||
|
@ -6,16 +6,14 @@ Django views for interacting with Build objects
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django.core.exceptions import ValidationError
|
|
||||||
from django.views.generic import DetailView, ListView
|
from django.views.generic import DetailView, ListView
|
||||||
from django.forms import HiddenInput
|
|
||||||
|
|
||||||
from .models import Build
|
from .models import Build
|
||||||
from . import forms
|
from . import forms
|
||||||
|
|
||||||
from InvenTree.views import AjaxUpdateView, AjaxDeleteView
|
from InvenTree.views import AjaxUpdateView, AjaxDeleteView
|
||||||
from InvenTree.views import InvenTreeRoleMixin
|
from InvenTree.views import InvenTreeRoleMixin
|
||||||
from InvenTree.helpers import str2bool, extract_serial_numbers
|
from InvenTree.helpers import str2bool
|
||||||
from InvenTree.status_codes import BuildStatus
|
from InvenTree.status_codes import BuildStatus
|
||||||
|
|
||||||
|
|
||||||
@ -76,121 +74,6 @@ class BuildCancel(AjaxUpdateView):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class BuildOutputCreate(AjaxUpdateView):
|
|
||||||
"""
|
|
||||||
Create a new build output (StockItem) for a given build.
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = Build
|
|
||||||
form_class = forms.BuildOutputCreateForm
|
|
||||||
ajax_template_name = 'build/build_output_create.html'
|
|
||||||
ajax_form_title = _('Create Build Output')
|
|
||||||
|
|
||||||
def validate(self, build, form, **kwargs):
|
|
||||||
"""
|
|
||||||
Validation for the form:
|
|
||||||
"""
|
|
||||||
|
|
||||||
quantity = form.cleaned_data.get('output_quantity', None)
|
|
||||||
serials = form.cleaned_data.get('serial_numbers', None)
|
|
||||||
|
|
||||||
if quantity is not None:
|
|
||||||
build = self.get_object()
|
|
||||||
|
|
||||||
# Check that requested output don't exceed build remaining quantity
|
|
||||||
maximum_output = int(build.remaining - build.incomplete_count)
|
|
||||||
|
|
||||||
if quantity > maximum_output:
|
|
||||||
form.add_error(
|
|
||||||
'output_quantity',
|
|
||||||
_('Maximum output quantity is ') + str(maximum_output),
|
|
||||||
)
|
|
||||||
|
|
||||||
elif quantity <= 0:
|
|
||||||
form.add_error(
|
|
||||||
'output_quantity',
|
|
||||||
_('Output quantity must be greater than zero'),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Check that the serial numbers are valid
|
|
||||||
if serials:
|
|
||||||
try:
|
|
||||||
extracted = extract_serial_numbers(serials, quantity, build.part.getLatestSerialNumberInt())
|
|
||||||
|
|
||||||
if extracted:
|
|
||||||
# Check for conflicting serial numbers
|
|
||||||
conflicts = build.part.find_conflicting_serial_numbers(extracted)
|
|
||||||
|
|
||||||
if len(conflicts) > 0:
|
|
||||||
msg = ",".join([str(c) for c in conflicts])
|
|
||||||
form.add_error(
|
|
||||||
'serial_numbers',
|
|
||||||
_('Serial numbers already exist') + ': ' + msg,
|
|
||||||
)
|
|
||||||
|
|
||||||
except ValidationError as e:
|
|
||||||
form.add_error('serial_numbers', e.messages)
|
|
||||||
|
|
||||||
else:
|
|
||||||
# If no serial numbers are provided, should they be?
|
|
||||||
if build.part.trackable:
|
|
||||||
form.add_error('serial_numbers', _('Serial numbers required for trackable build output'))
|
|
||||||
|
|
||||||
def save(self, build, form, **kwargs):
|
|
||||||
"""
|
|
||||||
Create a new build output
|
|
||||||
"""
|
|
||||||
|
|
||||||
data = form.cleaned_data
|
|
||||||
|
|
||||||
quantity = data.get('output_quantity', None)
|
|
||||||
batch = data.get('batch', None)
|
|
||||||
|
|
||||||
serials = data.get('serial_numbers', None)
|
|
||||||
|
|
||||||
if serials:
|
|
||||||
serial_numbers = extract_serial_numbers(serials, quantity, build.part.getLatestSerialNumberInt())
|
|
||||||
else:
|
|
||||||
serial_numbers = None
|
|
||||||
|
|
||||||
build.create_build_output(
|
|
||||||
quantity,
|
|
||||||
serials=serial_numbers,
|
|
||||||
batch=batch,
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_initial(self):
|
|
||||||
|
|
||||||
initials = super().get_initial()
|
|
||||||
|
|
||||||
build = self.get_object()
|
|
||||||
|
|
||||||
# Calculate the required quantity
|
|
||||||
quantity = max(0, build.remaining - build.incomplete_count)
|
|
||||||
initials['output_quantity'] = int(quantity)
|
|
||||||
|
|
||||||
return initials
|
|
||||||
|
|
||||||
def get_form(self):
|
|
||||||
|
|
||||||
build = self.get_object()
|
|
||||||
part = build.part
|
|
||||||
|
|
||||||
context = self.get_form_kwargs()
|
|
||||||
|
|
||||||
# Pass the 'part' through to the form,
|
|
||||||
# so we can add the next serial number as a placeholder
|
|
||||||
context['build'] = build
|
|
||||||
|
|
||||||
form = self.form_class(**context)
|
|
||||||
|
|
||||||
# If the part is not trackable, hide the serial number input
|
|
||||||
if not part.trackable:
|
|
||||||
form.fields['serial_numbers'].widget = HiddenInput()
|
|
||||||
|
|
||||||
return form
|
|
||||||
|
|
||||||
|
|
||||||
class BuildDetail(InvenTreeRoleMixin, DetailView):
|
class BuildDetail(InvenTreeRoleMixin, DetailView):
|
||||||
"""
|
"""
|
||||||
Detail view of a single Build object.
|
Detail view of a single Build object.
|
||||||
|
@ -1498,6 +1498,16 @@ class Part(MPTTModel):
|
|||||||
def has_bom(self):
|
def has_bom(self):
|
||||||
return self.get_bom_items().count() > 0
|
return self.get_bom_items().count() > 0
|
||||||
|
|
||||||
|
def get_trackable_parts(self):
|
||||||
|
"""
|
||||||
|
Return a queryset of all trackable parts in the BOM for this part
|
||||||
|
"""
|
||||||
|
|
||||||
|
queryset = self.get_bom_items()
|
||||||
|
queryset = queryset.filter(sub_part__trackable=True)
|
||||||
|
|
||||||
|
return queryset
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def has_trackable_parts(self):
|
def has_trackable_parts(self):
|
||||||
"""
|
"""
|
||||||
@ -1505,11 +1515,7 @@ class Part(MPTTModel):
|
|||||||
This is important when building the part.
|
This is important when building the part.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
for bom_item in self.get_bom_items().all():
|
return self.get_trackable_parts().count() > 0
|
||||||
if bom_item.sub_part.trackable:
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def bom_count(self):
|
def bom_count(self):
|
||||||
|
@ -21,6 +21,7 @@
|
|||||||
/* exported
|
/* exported
|
||||||
allocateStockToBuild,
|
allocateStockToBuild,
|
||||||
completeBuildOrder,
|
completeBuildOrder,
|
||||||
|
createBuildOutput,
|
||||||
editBuildOrder,
|
editBuildOrder,
|
||||||
loadAllocationTable,
|
loadAllocationTable,
|
||||||
loadBuildOrderAllocationTable,
|
loadBuildOrderAllocationTable,
|
||||||
@ -175,6 +176,85 @@ function completeBuildOrder(build_id, options={}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Construct a new build output against the provided build
|
||||||
|
*/
|
||||||
|
function createBuildOutput(build_id, options) {
|
||||||
|
|
||||||
|
// Request build order information from the server
|
||||||
|
inventreeGet(
|
||||||
|
`/api/build/${build_id}/`,
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
success: function(build) {
|
||||||
|
|
||||||
|
var html = '';
|
||||||
|
|
||||||
|
var trackable = build.part_detail.trackable;
|
||||||
|
var remaining = Math.max(0, build.quantity - build.completed);
|
||||||
|
|
||||||
|
var fields = {
|
||||||
|
quantity: {
|
||||||
|
value: remaining,
|
||||||
|
},
|
||||||
|
serial_numbers: {
|
||||||
|
hidden: !trackable,
|
||||||
|
required: options.trackable_parts || trackable,
|
||||||
|
},
|
||||||
|
batch_code: {},
|
||||||
|
auto_allocate: {
|
||||||
|
hidden: !trackable,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Work out the next available serial numbers
|
||||||
|
inventreeGet(`/api/part/${build.part}/serial-numbers/`, {}, {
|
||||||
|
success: function(data) {
|
||||||
|
if (data.next) {
|
||||||
|
fields.serial_numbers.placeholder = `{% trans "Next available serial number" %}: ${data.next}`;
|
||||||
|
} else {
|
||||||
|
fields.serial_numbers.placeholder = `{% trans "Latest serial number" %}: ${data.latest}`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (options.trackable_parts) {
|
||||||
|
html += `
|
||||||
|
<div class='alert alert-block alert-info'>
|
||||||
|
{% trans "The Bill of Materials contains trackable parts" %}.<br>
|
||||||
|
{% trans "Build outputs must be generated individually" %}.
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trackable) {
|
||||||
|
html += `
|
||||||
|
<div class='alert alert-block alert-info'>
|
||||||
|
{% trans "Trackable parts can have serial numbers specified" %}<br>
|
||||||
|
{% trans "Enter serial numbers to generate multiple single build outputs" %}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructForm(`/api/build/${build_id}/create-output/`, {
|
||||||
|
method: 'POST',
|
||||||
|
title: '{% trans "Create Build Output" %}',
|
||||||
|
confirm: true,
|
||||||
|
fields: fields,
|
||||||
|
preFormContent: html,
|
||||||
|
onSuccess: function(response) {
|
||||||
|
location.reload();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Construct a set of output buttons for a particular build output
|
* Construct a set of output buttons for a particular build output
|
||||||
*/
|
*/
|
||||||
|
@ -2014,7 +2014,7 @@ function constructField(name, parameters, options) {
|
|||||||
if (parameters.help_text && !options.hideLabels) {
|
if (parameters.help_text && !options.hideLabels) {
|
||||||
|
|
||||||
// Boolean values are handled differently!
|
// Boolean values are handled differently!
|
||||||
if (parameters.type != 'boolean') {
|
if (parameters.type != 'boolean' && !parameters.hidden) {
|
||||||
html += constructHelpText(name, parameters, options);
|
html += constructHelpText(name, parameters, options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2022,7 +2022,6 @@ function constructField(name, parameters, options) {
|
|||||||
// Div for error messages
|
// Div for error messages
|
||||||
html += `<div id='errors-${field_name}'></div>`;
|
html += `<div id='errors-${field_name}'></div>`;
|
||||||
|
|
||||||
|
|
||||||
html += `</div>`; // controls
|
html += `</div>`; // controls
|
||||||
html += `</div>`; // form-group
|
html += `</div>`; // form-group
|
||||||
|
|
||||||
@ -2212,6 +2211,10 @@ function constructInputOptions(name, classes, type, parameters, options={}) {
|
|||||||
return `<textarea ${opts.join(' ')}></textarea>`;
|
return `<textarea ${opts.join(' ')}></textarea>`;
|
||||||
} else if (parameters.type == 'boolean') {
|
} else if (parameters.type == 'boolean') {
|
||||||
|
|
||||||
|
if (parameters.hidden) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
var help_text = '';
|
var help_text = '';
|
||||||
|
|
||||||
if (!options.hideLabels && parameters.help_text) {
|
if (!options.hideLabels && parameters.help_text) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user