mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Add an API serializer to complete build outputs
This commit is contained in:
parent
4b4bf38ae5
commit
54dd05a24d
@ -6,7 +6,7 @@ JSON API for the Build app
|
|||||||
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.shortcuts import get_object_or_404
|
||||||
from django.conf.urls import url, include
|
from django.conf.urls import url, include
|
||||||
|
|
||||||
from rest_framework import filters, generics
|
from rest_framework import filters, generics
|
||||||
@ -20,7 +20,7 @@ from InvenTree.helpers import str2bool, isNull
|
|||||||
from InvenTree.status_codes import BuildStatus
|
from InvenTree.status_codes import BuildStatus
|
||||||
|
|
||||||
from .models import Build, BuildItem, BuildOrderAttachment
|
from .models import Build, BuildItem, BuildOrderAttachment
|
||||||
from .serializers import BuildAttachmentSerializer, BuildSerializer, BuildItemSerializer
|
from .serializers import BuildAttachmentSerializer, BuildCompleteSerializer, BuildSerializer, BuildItemSerializer
|
||||||
from .serializers import BuildAllocationSerializer, BuildUnallocationSerializer
|
from .serializers import BuildAllocationSerializer, BuildUnallocationSerializer
|
||||||
|
|
||||||
|
|
||||||
@ -196,30 +196,34 @@ class BuildUnallocate(generics.CreateAPIView):
|
|||||||
queryset = Build.objects.none()
|
queryset = Build.objects.none()
|
||||||
|
|
||||||
serializer_class = BuildUnallocationSerializer
|
serializer_class = BuildUnallocationSerializer
|
||||||
|
|
||||||
def get_build(self):
|
|
||||||
"""
|
|
||||||
Returns the BuildOrder associated with this API endpoint
|
|
||||||
"""
|
|
||||||
|
|
||||||
pk = self.kwargs.get('pk', None)
|
|
||||||
|
|
||||||
try:
|
|
||||||
build = Build.objects.get(pk=pk)
|
|
||||||
except (ValueError, Build.DoesNotExist):
|
|
||||||
raise ValidationError(_("Matching build order does not exist"))
|
|
||||||
|
|
||||||
return build
|
|
||||||
|
|
||||||
def get_serializer_context(self):
|
def get_serializer_context(self):
|
||||||
|
|
||||||
ctx = super().get_serializer_context()
|
ctx = super().get_serializer_context()
|
||||||
ctx['build'] = self.get_build()
|
ctx['build'] = get_object_or_404(Build, pk=self.kwargs.get('pk', None))
|
||||||
ctx['request'] = self.request
|
ctx['request'] = self.request
|
||||||
|
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
|
|
||||||
|
class BuildComplete(generics.CreateAPIView):
|
||||||
|
"""
|
||||||
|
API endpoint for completing build outputs
|
||||||
|
"""
|
||||||
|
|
||||||
|
queryset = Build.objects.none()
|
||||||
|
|
||||||
|
serializer_class = BuildCompleteSerializer
|
||||||
|
|
||||||
|
def get_serializer_context(self):
|
||||||
|
ctx = super().get_serializer_context()
|
||||||
|
|
||||||
|
ctx['request'] = self.request
|
||||||
|
ctx['build'] = get_object_or_404(Build, pk=self.kwargs.get('pk', None))
|
||||||
|
|
||||||
|
return ctx
|
||||||
|
|
||||||
|
|
||||||
class BuildAllocate(generics.CreateAPIView):
|
class BuildAllocate(generics.CreateAPIView):
|
||||||
"""
|
"""
|
||||||
API endpoint to allocate stock items to a build order
|
API endpoint to allocate stock items to a build order
|
||||||
@ -236,20 +240,6 @@ class BuildAllocate(generics.CreateAPIView):
|
|||||||
|
|
||||||
serializer_class = BuildAllocationSerializer
|
serializer_class = BuildAllocationSerializer
|
||||||
|
|
||||||
def get_build(self):
|
|
||||||
"""
|
|
||||||
Returns the BuildOrder associated with this API endpoint
|
|
||||||
"""
|
|
||||||
|
|
||||||
pk = self.kwargs.get('pk', None)
|
|
||||||
|
|
||||||
try:
|
|
||||||
build = Build.objects.get(pk=pk)
|
|
||||||
except (Build.DoesNotExist, ValueError):
|
|
||||||
raise ValidationError(_("Matching build order does not exist"))
|
|
||||||
|
|
||||||
return build
|
|
||||||
|
|
||||||
def get_serializer_context(self):
|
def get_serializer_context(self):
|
||||||
"""
|
"""
|
||||||
Provide the Build object to the serializer context
|
Provide the Build object to the serializer context
|
||||||
@ -257,7 +247,7 @@ class BuildAllocate(generics.CreateAPIView):
|
|||||||
|
|
||||||
context = super().get_serializer_context()
|
context = super().get_serializer_context()
|
||||||
|
|
||||||
context['build'] = self.get_build()
|
context['build'] = get_object_or_404(Build, pk=self.kwargs.get('pk', None))
|
||||||
context['request'] = self.request
|
context['request'] = self.request
|
||||||
|
|
||||||
return context
|
return context
|
||||||
@ -385,6 +375,7 @@ build_api_urls = [
|
|||||||
# Build Detail
|
# Build Detail
|
||||||
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/', BuildComplete.as_view(), name='api-build-complete'),
|
||||||
url(r'^unallocate/', BuildUnallocate.as_view(), name='api-build-unallocate'),
|
url(r'^unallocate/', BuildUnallocate.as_view(), name='api-build-unallocate'),
|
||||||
url(r'^.*$', BuildDetail.as_view(), name='api-build-detail'),
|
url(r'^.*$', BuildDetail.as_view(), name='api-build-detail'),
|
||||||
])),
|
])),
|
||||||
|
@ -722,7 +722,7 @@ class Build(MPTTModel):
|
|||||||
items.all().delete()
|
items.all().delete()
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def completeBuildOutput(self, output, user, **kwargs):
|
def complete_build_output(self, output, user, **kwargs):
|
||||||
"""
|
"""
|
||||||
Complete a particular build output
|
Complete a particular build output
|
||||||
|
|
||||||
@ -739,10 +739,6 @@ class Build(MPTTModel):
|
|||||||
allocated_items = output.items_to_install.all()
|
allocated_items = output.items_to_install.all()
|
||||||
|
|
||||||
for build_item in allocated_items:
|
for build_item in allocated_items:
|
||||||
|
|
||||||
# TODO: This is VERY SLOW as each deletion from the database takes ~1 second to complete
|
|
||||||
# TODO: Use the background worker process to handle this task!
|
|
||||||
|
|
||||||
# Complete the allocation of stock for that item
|
# Complete the allocation of stock for that item
|
||||||
build_item.complete_allocation(user)
|
build_item.complete_allocation(user)
|
||||||
|
|
||||||
@ -768,6 +764,7 @@ class Build(MPTTModel):
|
|||||||
|
|
||||||
# Increase the completed quantity for this build
|
# Increase the completed quantity for this build
|
||||||
self.completed += output.quantity
|
self.completed += output.quantity
|
||||||
|
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
def requiredQuantity(self, part, output):
|
def requiredQuantity(self, part, output):
|
||||||
|
@ -18,9 +18,10 @@ from rest_framework.serializers import ValidationError
|
|||||||
from InvenTree.serializers import InvenTreeModelSerializer, InvenTreeAttachmentSerializer
|
from InvenTree.serializers import InvenTreeModelSerializer, InvenTreeAttachmentSerializer
|
||||||
from InvenTree.serializers import InvenTreeAttachmentSerializerField, UserSerializerBrief
|
from InvenTree.serializers import InvenTreeAttachmentSerializerField, UserSerializerBrief
|
||||||
|
|
||||||
|
from InvenTree.status_codes import StockStatus
|
||||||
import InvenTree.helpers
|
import InvenTree.helpers
|
||||||
|
|
||||||
from stock.models import StockItem
|
from stock.models import StockItem, StockLocation
|
||||||
from stock.serializers import StockItemSerializerBrief, LocationSerializer
|
from stock.serializers import StockItemSerializerBrief, LocationSerializer
|
||||||
|
|
||||||
from part.models import BomItem
|
from part.models import BomItem
|
||||||
@ -120,6 +121,120 @@ class BuildSerializer(InvenTreeModelSerializer):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class BuildOutputSerializer(serializers.Serializer):
|
||||||
|
"""
|
||||||
|
Serializer for a "BuildOutput"
|
||||||
|
|
||||||
|
Note that a "BuildOutput" is really just a StockItem which is "in production"!
|
||||||
|
"""
|
||||||
|
|
||||||
|
output = serializers.PrimaryKeyRelatedField(
|
||||||
|
queryset=StockItem.objects.all(),
|
||||||
|
many=False,
|
||||||
|
allow_null=False,
|
||||||
|
required=True,
|
||||||
|
label=_('Build Output'),
|
||||||
|
)
|
||||||
|
|
||||||
|
def validate_output(self, output):
|
||||||
|
|
||||||
|
build = self.context['build']
|
||||||
|
|
||||||
|
# The stock item must point to the build
|
||||||
|
if output.build != build:
|
||||||
|
raise ValidationError(_("Build output does not match the parent build"))
|
||||||
|
|
||||||
|
# The part must match!
|
||||||
|
if output.part != build.part:
|
||||||
|
raise ValidationError(_("Output part does not match BuildOrder part"))
|
||||||
|
|
||||||
|
# The build output must be "in production"
|
||||||
|
if not output.is_building:
|
||||||
|
raise ValidationError(_("This build output has already been completed"))
|
||||||
|
|
||||||
|
return output
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
fields = [
|
||||||
|
'output',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class BuildCompleteSerializer(serializers.Serializer):
|
||||||
|
"""
|
||||||
|
DRF serializer for completing one or more build outputs
|
||||||
|
"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
fields = [
|
||||||
|
'outputs',
|
||||||
|
'location',
|
||||||
|
'status',
|
||||||
|
'notes',
|
||||||
|
]
|
||||||
|
|
||||||
|
outputs = BuildOutputSerializer(
|
||||||
|
many=True,
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
location = serializers.PrimaryKeyRelatedField(
|
||||||
|
queryset=StockLocation.objects.all(),
|
||||||
|
required=True,
|
||||||
|
many=False,
|
||||||
|
label=_("Location"),
|
||||||
|
help_text=_("Location for completed build outputs"),
|
||||||
|
)
|
||||||
|
|
||||||
|
status = serializers.ChoiceField(
|
||||||
|
choices=list(StockStatus.items()),
|
||||||
|
default=StockStatus.OK,
|
||||||
|
label=_("Status"),
|
||||||
|
)
|
||||||
|
|
||||||
|
notes = serializers.CharField(
|
||||||
|
label=_("Notes"),
|
||||||
|
required=False,
|
||||||
|
allow_blank=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def validate(self, data):
|
||||||
|
|
||||||
|
super().validate(data)
|
||||||
|
|
||||||
|
outputs = data.get('outputs', [])
|
||||||
|
|
||||||
|
if len(outputs) == 0:
|
||||||
|
raise ValidationError(_("A list of build outputs must be provided"))
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
def save(self):
|
||||||
|
"""
|
||||||
|
"save" the serializer to complete the build outputs
|
||||||
|
"""
|
||||||
|
|
||||||
|
build = self.context['build']
|
||||||
|
request = self.context['request']
|
||||||
|
|
||||||
|
data = self.validated_data
|
||||||
|
|
||||||
|
outputs = data.get('outputs', [])
|
||||||
|
|
||||||
|
# Mark the specified build outputs as "complete"
|
||||||
|
with transaction.atomic():
|
||||||
|
for item in outputs:
|
||||||
|
|
||||||
|
output = item['output']
|
||||||
|
|
||||||
|
build.complete_build_output(
|
||||||
|
output,
|
||||||
|
request.user,
|
||||||
|
status=data['status'],
|
||||||
|
notes=data.get('notes', '')
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class BuildUnallocationSerializer(serializers.Serializer):
|
class BuildUnallocationSerializer(serializers.Serializer):
|
||||||
"""
|
"""
|
||||||
DRF serializer for unallocating stock from a BuildOrder
|
DRF serializer for unallocating stock from a BuildOrder
|
||||||
|
@ -96,11 +96,6 @@ src="{% static 'img/blank_image.png' %}"
|
|||||||
</div>
|
</div>
|
||||||
<!-- Build actions -->
|
<!-- Build actions -->
|
||||||
{% if roles.build.change %}
|
{% if roles.build.change %}
|
||||||
{% if build.active %}
|
|
||||||
<button id='build-complete' title='{% trans "Complete Build" %}' class='btn btn-success'>
|
|
||||||
<span class='fas fa-paper-plane'></span>
|
|
||||||
</button>
|
|
||||||
{% endif %}
|
|
||||||
<div class='btn-group'>
|
<div class='btn-group'>
|
||||||
<button id='build-options' title='{% trans "Build actions" %}' class='btn btn-default dropdown-toggle' type='button' data-toggle='dropdown'>
|
<button id='build-options' title='{% trans "Build actions" %}' class='btn btn-default dropdown-toggle' type='button' data-toggle='dropdown'>
|
||||||
<span class='fas fa-tools'></span> <span class='caret'></span>
|
<span class='fas fa-tools'></span> <span class='caret'></span>
|
||||||
@ -115,6 +110,11 @@ src="{% static 'img/blank_image.png' %}"
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
{% if build.active %}
|
||||||
|
<button id='build-complete' title='{% trans "Complete Build" %}' class='btn btn-success'>
|
||||||
|
<span class='fas fa-check-circle'></span>
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -319,11 +319,11 @@ class BuildTest(TestCase):
|
|||||||
self.assertTrue(self.build.isFullyAllocated(self.output_1))
|
self.assertTrue(self.build.isFullyAllocated(self.output_1))
|
||||||
self.assertTrue(self.build.isFullyAllocated(self.output_2))
|
self.assertTrue(self.build.isFullyAllocated(self.output_2))
|
||||||
|
|
||||||
self.build.completeBuildOutput(self.output_1, None)
|
self.build.complete_build_output(self.output_1, None)
|
||||||
|
|
||||||
self.assertFalse(self.build.can_complete)
|
self.assertFalse(self.build.can_complete)
|
||||||
|
|
||||||
self.build.completeBuildOutput(self.output_2, None)
|
self.build.complete_build_output(self.output_2, None)
|
||||||
|
|
||||||
self.assertTrue(self.build.can_complete)
|
self.assertTrue(self.build.can_complete)
|
||||||
|
|
||||||
|
@ -434,7 +434,7 @@ class BuildOutputComplete(AjaxUpdateView):
|
|||||||
stock_status = StockStatus.OK
|
stock_status = StockStatus.OK
|
||||||
|
|
||||||
# Complete the build output
|
# Complete the build output
|
||||||
build.completeBuildOutput(
|
build.complete_build_output(
|
||||||
output,
|
output,
|
||||||
self.request.user,
|
self.request.user,
|
||||||
location=location,
|
location=location,
|
||||||
|
@ -8,6 +8,7 @@ from __future__ import unicode_literals
|
|||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django.conf.urls import url, include
|
from django.conf.urls import url, include
|
||||||
from django.db.models import Q, F
|
from django.db.models import Q, F
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
|
|
||||||
from django_filters import rest_framework as rest_filters
|
from django_filters import rest_framework as rest_filters
|
||||||
from rest_framework import generics
|
from rest_framework import generics
|
||||||
@ -232,25 +233,11 @@ class POReceive(generics.CreateAPIView):
|
|||||||
context = super().get_serializer_context()
|
context = super().get_serializer_context()
|
||||||
|
|
||||||
# Pass the purchase order through to the serializer for validation
|
# Pass the purchase order through to the serializer for validation
|
||||||
context['order'] = self.get_order()
|
context['order'] = get_object_or_404(PurchaseOrder, pk=self.kwargs.get('pk', None))
|
||||||
context['request'] = self.request
|
context['request'] = self.request
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
def get_order(self):
|
|
||||||
"""
|
|
||||||
Returns the PurchaseOrder associated with this API endpoint
|
|
||||||
"""
|
|
||||||
|
|
||||||
pk = self.kwargs.get('pk', None)
|
|
||||||
|
|
||||||
try:
|
|
||||||
order = PurchaseOrder.objects.get(pk=pk)
|
|
||||||
except (PurchaseOrder.DoesNotExist, ValueError):
|
|
||||||
raise ValidationError(_("Matching purchase order does not exist"))
|
|
||||||
|
|
||||||
return order
|
|
||||||
|
|
||||||
|
|
||||||
class POLineItemFilter(rest_filters.FilterSet):
|
class POLineItemFilter(rest_filters.FilterSet):
|
||||||
"""
|
"""
|
||||||
|
@ -151,7 +151,7 @@ function makeBuildOutputActionButtons(output, buildInfo, lines) {
|
|||||||
|
|
||||||
// Add a button to "complete" the particular build output
|
// Add a button to "complete" the particular build output
|
||||||
html += makeIconButton(
|
html += makeIconButton(
|
||||||
'fa-check icon-green', 'button-output-complete', outputId,
|
'fa-check-circle icon-green', 'button-output-complete', outputId,
|
||||||
'{% trans "Complete build output" %}',
|
'{% trans "Complete build output" %}',
|
||||||
{
|
{
|
||||||
// disabled: true
|
// disabled: true
|
||||||
|
Loading…
Reference in New Issue
Block a user