2019-04-27 10:35:14 +00:00
|
|
|
"""
|
2019-04-27 12:18:07 +00:00
|
|
|
Django views for interacting with Build objects
|
2019-04-27 10:35:14 +00:00
|
|
|
"""
|
|
|
|
|
2018-04-16 14:32:02 +00:00
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
from __future__ import unicode_literals
|
|
|
|
|
2019-07-22 03:45:09 +00:00
|
|
|
from django.utils.translation import ugettext as _
|
|
|
|
from django.core.exceptions import ValidationError
|
2020-02-01 13:00:19 +00:00
|
|
|
from django.views.generic import DetailView, ListView, UpdateView
|
2019-04-30 05:48:07 +00:00
|
|
|
from django.forms import HiddenInput
|
2020-02-01 13:00:19 +00:00
|
|
|
from django.urls import reverse
|
2018-04-16 14:32:02 +00:00
|
|
|
|
2018-04-17 13:13:41 +00:00
|
|
|
from part.models import Part
|
2019-04-30 05:35:35 +00:00
|
|
|
from .models import Build, BuildItem
|
2019-05-09 22:33:54 +00:00
|
|
|
from . import forms
|
2019-05-01 14:25:19 +00:00
|
|
|
from stock.models import StockLocation, StockItem
|
2018-04-16 14:32:02 +00:00
|
|
|
|
2019-05-14 08:32:20 +00:00
|
|
|
from InvenTree.views import AjaxUpdateView, AjaxCreateView, AjaxDeleteView
|
2020-10-06 09:29:16 +00:00
|
|
|
from InvenTree.views import InvenTreeRoleMixin
|
2019-07-22 03:45:09 +00:00
|
|
|
from InvenTree.helpers import str2bool, ExtractSerialNumbers
|
2019-06-04 13:42:48 +00:00
|
|
|
from InvenTree.status_codes import BuildStatus
|
2018-04-17 14:03:42 +00:00
|
|
|
|
2018-04-27 15:16:47 +00:00
|
|
|
|
2020-10-06 09:29:16 +00:00
|
|
|
class BuildIndex(InvenTreeRoleMixin, ListView):
|
2019-04-27 10:35:14 +00:00
|
|
|
""" View for displaying list of Builds
|
|
|
|
"""
|
2018-04-16 14:32:02 +00:00
|
|
|
model = Build
|
|
|
|
template_name = 'build/index.html'
|
|
|
|
context_object_name = 'builds'
|
2020-10-06 09:29:16 +00:00
|
|
|
role_required = 'build.view'
|
2018-04-16 14:32:02 +00:00
|
|
|
|
2018-04-17 13:13:41 +00:00
|
|
|
def get_queryset(self):
|
2019-04-27 10:35:14 +00:00
|
|
|
""" Return all Build objects (order by date, newest first) """
|
2018-04-17 13:13:41 +00:00
|
|
|
return Build.objects.order_by('status', '-completion_date')
|
|
|
|
|
|
|
|
def get_context_data(self, **kwargs):
|
|
|
|
|
|
|
|
context = super(BuildIndex, self).get_context_data(**kwargs).copy()
|
|
|
|
|
2019-06-04 13:42:48 +00:00
|
|
|
context['BuildStatus'] = BuildStatus
|
2018-04-17 13:13:41 +00:00
|
|
|
|
2019-06-04 13:42:48 +00:00
|
|
|
context['active'] = self.get_queryset().filter(status__in=BuildStatus.ACTIVE_CODES)
|
|
|
|
|
|
|
|
context['completed'] = self.get_queryset().filter(status=BuildStatus.COMPLETE)
|
|
|
|
context['cancelled'] = self.get_queryset().filter(status=BuildStatus.CANCELLED)
|
2018-04-17 13:13:41 +00:00
|
|
|
|
|
|
|
return context
|
|
|
|
|
2018-04-16 14:32:02 +00:00
|
|
|
|
2019-05-14 08:20:54 +00:00
|
|
|
class BuildCancel(AjaxUpdateView):
|
2019-04-27 10:35:14 +00:00
|
|
|
""" View to cancel a Build.
|
|
|
|
Provides a cancellation information dialog
|
|
|
|
"""
|
2019-05-14 08:20:54 +00:00
|
|
|
|
2018-04-29 14:23:44 +00:00
|
|
|
model = Build
|
2019-04-30 10:39:01 +00:00
|
|
|
ajax_template_name = 'build/cancel.html'
|
2020-02-11 21:11:59 +00:00
|
|
|
ajax_form_title = _('Cancel Build')
|
2018-04-29 14:23:44 +00:00
|
|
|
context_object_name = 'build'
|
2019-05-14 08:20:54 +00:00
|
|
|
form_class = forms.CancelBuildForm
|
2020-10-06 09:29:16 +00:00
|
|
|
role_required = 'build.change'
|
2018-04-29 14:23:44 +00:00
|
|
|
|
|
|
|
def post(self, request, *args, **kwargs):
|
2019-04-27 10:35:14 +00:00
|
|
|
""" Handle POST request. Mark the build status as CANCELLED """
|
2018-04-29 14:23:44 +00:00
|
|
|
|
2019-05-14 08:20:54 +00:00
|
|
|
build = self.get_object()
|
|
|
|
|
|
|
|
form = self.get_form()
|
|
|
|
|
|
|
|
valid = form.is_valid()
|
2018-04-29 14:23:44 +00:00
|
|
|
|
2019-05-14 08:20:54 +00:00
|
|
|
confirm = str2bool(request.POST.get('confirm_cancel', False))
|
2018-04-29 14:23:44 +00:00
|
|
|
|
2019-05-14 08:20:54 +00:00
|
|
|
if confirm:
|
|
|
|
build.cancelBuild(request.user)
|
|
|
|
else:
|
2020-02-11 21:11:59 +00:00
|
|
|
form.errors['confirm_cancel'] = [_('Confirm build cancellation')]
|
2019-05-14 08:20:54 +00:00
|
|
|
valid = False
|
2018-04-29 14:23:44 +00:00
|
|
|
|
2019-05-14 08:20:54 +00:00
|
|
|
data = {
|
|
|
|
'form_valid': valid,
|
2020-02-11 21:11:59 +00:00
|
|
|
'danger': _('Build was cancelled')
|
2018-04-29 14:23:44 +00:00
|
|
|
}
|
|
|
|
|
2019-05-14 08:20:54 +00:00
|
|
|
return self.renderJsonResponse(request, form, data=data)
|
|
|
|
|
2018-04-29 14:23:44 +00:00
|
|
|
|
2019-05-09 13:55:30 +00:00
|
|
|
class BuildAutoAllocate(AjaxUpdateView):
|
|
|
|
""" View to auto-allocate parts for a build.
|
|
|
|
Follows a simple set of rules to automatically allocate StockItem objects.
|
|
|
|
|
|
|
|
Ref: build.models.Build.getAutoAllocations()
|
|
|
|
"""
|
|
|
|
|
|
|
|
model = Build
|
2019-05-09 22:33:54 +00:00
|
|
|
form_class = forms.ConfirmBuildForm
|
2019-05-09 13:55:30 +00:00
|
|
|
context_object_name = 'build'
|
2020-02-11 21:11:59 +00:00
|
|
|
ajax_form_title = _('Allocate Stock')
|
2019-05-09 13:55:30 +00:00
|
|
|
ajax_template_name = 'build/auto_allocate.html'
|
2020-10-06 09:29:16 +00:00
|
|
|
role_required = 'build.change'
|
2019-05-09 13:55:30 +00:00
|
|
|
|
|
|
|
def get_context_data(self, *args, **kwargs):
|
|
|
|
""" Get the context data for form rendering. """
|
|
|
|
|
|
|
|
context = {}
|
|
|
|
|
|
|
|
try:
|
|
|
|
build = Build.objects.get(id=self.kwargs['pk'])
|
|
|
|
context['build'] = build
|
|
|
|
context['allocations'] = build.getAutoAllocations()
|
|
|
|
except Build.DoesNotExist:
|
2020-02-11 21:11:59 +00:00
|
|
|
context['error'] = _('No matching build found')
|
2019-05-09 13:55:30 +00:00
|
|
|
|
|
|
|
return context
|
|
|
|
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
|
|
""" Handle POST request. Perform auto allocations.
|
|
|
|
|
|
|
|
- If the form validation passes, perform allocations
|
|
|
|
- Otherwise, the form is passed back to the client
|
|
|
|
"""
|
|
|
|
|
|
|
|
build = self.get_object()
|
|
|
|
form = self.get_form()
|
|
|
|
|
|
|
|
confirm = request.POST.get('confirm', False)
|
|
|
|
|
|
|
|
valid = False
|
|
|
|
|
|
|
|
if confirm is False:
|
2020-02-11 21:11:59 +00:00
|
|
|
form.errors['confirm'] = [_('Confirm stock allocation')]
|
2020-04-24 00:20:56 +00:00
|
|
|
form.non_field_errors = [_('Check the confirmation box at the bottom of the list')]
|
2019-05-09 13:55:30 +00:00
|
|
|
else:
|
|
|
|
build.autoAllocate()
|
|
|
|
valid = True
|
|
|
|
|
|
|
|
data = {
|
|
|
|
'form_valid': valid,
|
|
|
|
}
|
|
|
|
|
|
|
|
return self.renderJsonResponse(request, form, data, context=self.get_context_data())
|
|
|
|
|
|
|
|
|
2019-05-09 22:33:54 +00:00
|
|
|
class BuildUnallocate(AjaxUpdateView):
|
|
|
|
""" View to un-allocate all parts from a build.
|
|
|
|
|
|
|
|
Provides a simple confirmation dialog with a BooleanField checkbox.
|
|
|
|
"""
|
|
|
|
|
|
|
|
model = Build
|
|
|
|
form_class = forms.ConfirmBuildForm
|
2020-02-11 21:11:59 +00:00
|
|
|
ajax_form_title = _("Unallocate Stock")
|
2019-05-09 22:33:54 +00:00
|
|
|
ajax_template_name = "build/unallocate.html"
|
2020-10-06 09:29:16 +00:00
|
|
|
form_required = 'build.change'
|
2019-05-09 22:33:54 +00:00
|
|
|
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
|
|
|
|
|
|
build = self.get_object()
|
|
|
|
form = self.get_form()
|
|
|
|
|
|
|
|
confirm = request.POST.get('confirm', False)
|
|
|
|
|
|
|
|
valid = False
|
|
|
|
|
|
|
|
if confirm is False:
|
2020-02-11 21:11:59 +00:00
|
|
|
form.errors['confirm'] = [_('Confirm unallocation of build stock')]
|
2020-04-24 00:20:56 +00:00
|
|
|
form.non_field_errors = [_('Check the confirmation box')]
|
2019-05-09 22:33:54 +00:00
|
|
|
else:
|
|
|
|
build.unallocateStock()
|
|
|
|
valid = True
|
|
|
|
|
|
|
|
data = {
|
|
|
|
'form_valid': valid,
|
|
|
|
}
|
|
|
|
|
|
|
|
return self.renderJsonResponse(request, form, data)
|
|
|
|
|
|
|
|
|
2019-05-01 14:04:39 +00:00
|
|
|
class BuildComplete(AjaxUpdateView):
|
2019-04-30 22:19:57 +00:00
|
|
|
""" View to mark a build as Complete.
|
2019-04-30 10:39:01 +00:00
|
|
|
|
|
|
|
- Notifies the user of which parts will be removed from stock.
|
|
|
|
- Removes allocated items from stock
|
|
|
|
- Deletes pending BuildItem objects
|
|
|
|
"""
|
|
|
|
|
|
|
|
model = Build
|
2019-05-09 22:33:54 +00:00
|
|
|
form_class = forms.CompleteBuildForm
|
2019-04-30 10:39:01 +00:00
|
|
|
context_object_name = "build"
|
2020-02-11 21:11:59 +00:00
|
|
|
ajax_form_title = _("Complete Build")
|
2019-05-01 14:04:39 +00:00
|
|
|
ajax_template_name = "build/complete.html"
|
2020-10-06 09:29:16 +00:00
|
|
|
role_required = 'build.change'
|
2019-05-01 14:04:39 +00:00
|
|
|
|
2019-07-22 03:45:09 +00:00
|
|
|
def get_form(self):
|
|
|
|
""" Get the form object.
|
|
|
|
|
|
|
|
If the part is trackable, include a field for serial numbers.
|
|
|
|
"""
|
|
|
|
build = self.get_object()
|
|
|
|
|
|
|
|
form = super().get_form()
|
|
|
|
|
|
|
|
if not build.part.trackable:
|
|
|
|
form.fields.pop('serial_numbers')
|
2020-05-16 07:29:41 +00:00
|
|
|
else:
|
2020-05-16 07:52:25 +00:00
|
|
|
|
2020-09-01 10:16:46 +00:00
|
|
|
form.field_placeholder['serial_numbers'] = build.part.getSerialNumberString(build.quantity)
|
2020-05-16 07:29:41 +00:00
|
|
|
|
|
|
|
form.rebuild_layout()
|
2019-07-22 03:45:09 +00:00
|
|
|
|
|
|
|
return form
|
|
|
|
|
2019-05-01 14:04:39 +00:00
|
|
|
def get_initial(self):
|
|
|
|
""" Get initial form data for the CompleteBuild form
|
|
|
|
|
|
|
|
- If the part being built has a default location, pre-select that location
|
|
|
|
"""
|
|
|
|
|
|
|
|
initials = super(BuildComplete, self).get_initial().copy()
|
|
|
|
|
|
|
|
build = self.get_object()
|
2020-05-16 07:29:41 +00:00
|
|
|
|
2019-05-01 14:04:39 +00:00
|
|
|
if build.part.default_location is not None:
|
|
|
|
try:
|
|
|
|
location = StockLocation.objects.get(pk=build.part.default_location.id)
|
2019-05-01 14:12:28 +00:00
|
|
|
initials['location'] = location
|
2019-05-01 14:04:39 +00:00
|
|
|
except StockLocation.DoesNotExist:
|
|
|
|
pass
|
|
|
|
|
|
|
|
return initials
|
|
|
|
|
|
|
|
def get_context_data(self, **kwargs):
|
|
|
|
""" Get context data for passing to the rendered form
|
|
|
|
|
|
|
|
- Build information is required
|
|
|
|
"""
|
|
|
|
|
2019-07-22 03:45:09 +00:00
|
|
|
build = Build.objects.get(id=self.kwargs['pk'])
|
2019-05-01 14:12:28 +00:00
|
|
|
|
2019-07-22 03:45:09 +00:00
|
|
|
context = {}
|
2019-07-22 05:55:36 +00:00
|
|
|
|
2019-05-01 14:12:28 +00:00
|
|
|
# Build object
|
|
|
|
context['build'] = build
|
|
|
|
|
|
|
|
# Items to be removed from stock
|
|
|
|
taking = BuildItem.objects.filter(build=build.id)
|
|
|
|
context['taking'] = taking
|
2019-05-01 14:04:39 +00:00
|
|
|
|
|
|
|
return context
|
2019-04-30 10:39:01 +00:00
|
|
|
|
|
|
|
def post(self, request, *args, **kwargs):
|
2019-05-01 14:04:39 +00:00
|
|
|
""" Handle POST request. Mark the build as COMPLETE
|
|
|
|
|
|
|
|
- If the form validation passes, the Build objects completeBuild() method is called
|
|
|
|
- Otherwise, the form is passed back to the client
|
|
|
|
"""
|
2019-04-30 10:39:01 +00:00
|
|
|
|
2019-05-01 14:04:39 +00:00
|
|
|
build = self.get_object()
|
2019-04-30 10:39:01 +00:00
|
|
|
|
2019-05-01 14:04:39 +00:00
|
|
|
form = self.get_form()
|
2019-04-30 10:39:01 +00:00
|
|
|
|
2019-05-14 08:20:54 +00:00
|
|
|
confirm = str2bool(request.POST.get('confirm', False))
|
2019-05-01 14:04:39 +00:00
|
|
|
|
|
|
|
loc_id = request.POST.get('location', None)
|
|
|
|
|
|
|
|
valid = False
|
|
|
|
|
|
|
|
if confirm is False:
|
|
|
|
form.errors['confirm'] = [
|
2020-02-11 21:11:59 +00:00
|
|
|
_('Confirm completion of build'),
|
2019-05-01 14:04:39 +00:00
|
|
|
]
|
|
|
|
else:
|
|
|
|
try:
|
|
|
|
location = StockLocation.objects.get(id=loc_id)
|
|
|
|
valid = True
|
2020-04-25 13:59:28 +00:00
|
|
|
except (ValueError, StockLocation.DoesNotExist):
|
2020-02-11 21:11:59 +00:00
|
|
|
form.errors['location'] = [_('Invalid location selected')]
|
2019-05-01 14:04:39 +00:00
|
|
|
|
2019-07-22 05:55:36 +00:00
|
|
|
serials = []
|
2019-07-22 03:45:09 +00:00
|
|
|
|
2019-07-22 05:55:36 +00:00
|
|
|
if build.part.trackable:
|
2020-04-25 13:59:28 +00:00
|
|
|
# A build for a trackable part may optionally specify serial numbers.
|
2019-07-22 03:45:09 +00:00
|
|
|
|
2019-07-22 05:55:36 +00:00
|
|
|
sn = request.POST.get('serial_numbers', '')
|
|
|
|
|
2019-08-28 22:05:45 +00:00
|
|
|
sn = str(sn).strip()
|
2019-07-22 05:55:36 +00:00
|
|
|
|
2019-08-28 22:05:45 +00:00
|
|
|
# If the user has specified serial numbers, check they are valid
|
|
|
|
if len(sn) > 0:
|
|
|
|
try:
|
|
|
|
# Exctract a list of provided serial numbers
|
|
|
|
serials = ExtractSerialNumbers(sn, build.quantity)
|
2019-07-22 05:55:36 +00:00
|
|
|
|
2019-08-28 22:05:45 +00:00
|
|
|
existing = []
|
2019-07-22 05:55:36 +00:00
|
|
|
|
2019-08-28 22:05:45 +00:00
|
|
|
for serial in serials:
|
2020-05-16 02:03:18 +00:00
|
|
|
if build.part.checkIfSerialNumberExists(serial):
|
2019-08-28 22:05:45 +00:00
|
|
|
existing.append(serial)
|
|
|
|
|
|
|
|
if len(existing) > 0:
|
|
|
|
exists = ",".join([str(x) for x in existing])
|
|
|
|
form.errors['serial_numbers'] = [_('The following serial numbers already exist: ({sn})'.format(sn=exists))]
|
|
|
|
valid = False
|
2019-07-22 03:45:09 +00:00
|
|
|
|
2019-08-28 22:05:45 +00:00
|
|
|
except ValidationError as e:
|
|
|
|
form.errors['serial_numbers'] = e.messages
|
|
|
|
valid = False
|
2019-07-22 03:45:09 +00:00
|
|
|
|
2019-05-01 14:04:39 +00:00
|
|
|
if valid:
|
2020-04-25 13:59:28 +00:00
|
|
|
if not build.completeBuild(location, serials, request.user):
|
|
|
|
form.non_field_errors = [('Build could not be completed')]
|
|
|
|
valid = False
|
2019-05-01 14:04:39 +00:00
|
|
|
|
|
|
|
data = {
|
|
|
|
'form_valid': valid,
|
|
|
|
}
|
|
|
|
|
2019-07-22 03:45:09 +00:00
|
|
|
return self.renderJsonResponse(request, form, data, context=self.get_context_data())
|
2019-04-30 10:39:01 +00:00
|
|
|
|
|
|
|
def get_data(self):
|
|
|
|
""" Provide feedback data back to the form """
|
|
|
|
return {
|
2020-02-11 21:11:59 +00:00
|
|
|
'info': _('Build marked as COMPLETE')
|
2019-04-30 10:39:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2020-02-01 13:00:19 +00:00
|
|
|
class BuildNotes(UpdateView):
|
|
|
|
""" View for editing the 'notes' field of a Build object.
|
|
|
|
"""
|
|
|
|
|
|
|
|
context_object_name = 'build'
|
|
|
|
template_name = 'build/notes.html'
|
|
|
|
model = Build
|
2020-10-06 09:29:16 +00:00
|
|
|
role_required = 'build.view'
|
2020-02-01 13:00:19 +00:00
|
|
|
|
|
|
|
fields = ['notes']
|
|
|
|
|
|
|
|
def get_success_url(self):
|
|
|
|
return reverse('build-notes', kwargs={'pk': self.get_object().id})
|
|
|
|
|
|
|
|
def get_context_data(self, **kwargs):
|
|
|
|
|
|
|
|
ctx = super().get_context_data(**kwargs)
|
|
|
|
|
|
|
|
ctx['editing'] = str2bool(self.request.GET.get('edit', ''))
|
|
|
|
|
|
|
|
return ctx
|
|
|
|
|
|
|
|
|
2018-04-16 14:32:02 +00:00
|
|
|
class BuildDetail(DetailView):
|
2019-04-27 10:35:14 +00:00
|
|
|
""" Detail view of a single Build object. """
|
2020-10-06 09:29:16 +00:00
|
|
|
|
2018-04-16 14:32:02 +00:00
|
|
|
model = Build
|
|
|
|
template_name = 'build/detail.html'
|
|
|
|
context_object_name = 'build'
|
2020-10-06 09:29:16 +00:00
|
|
|
role_required = 'build.view'
|
2018-04-17 13:13:41 +00:00
|
|
|
|
2019-05-18 11:56:00 +00:00
|
|
|
def get_context_data(self, **kwargs):
|
|
|
|
|
|
|
|
ctx = super(DetailView, self).get_context_data(**kwargs)
|
|
|
|
|
|
|
|
build = self.get_object()
|
|
|
|
|
|
|
|
ctx['bom_price'] = build.part.get_price_info(build.quantity, buy=False)
|
2019-06-13 11:32:37 +00:00
|
|
|
ctx['BuildStatus'] = BuildStatus
|
2019-05-18 11:56:00 +00:00
|
|
|
|
|
|
|
return ctx
|
|
|
|
|
2018-04-17 13:13:41 +00:00
|
|
|
|
2018-04-29 14:23:44 +00:00
|
|
|
class BuildAllocate(DetailView):
|
2019-04-27 10:35:14 +00:00
|
|
|
""" View for allocating parts to a Build """
|
2018-04-29 14:23:44 +00:00
|
|
|
model = Build
|
|
|
|
context_object_name = 'build'
|
|
|
|
template_name = 'build/allocate.html'
|
2020-10-06 09:29:16 +00:00
|
|
|
role_required = ['build.change']
|
2018-04-29 14:23:44 +00:00
|
|
|
|
2019-04-30 21:48:46 +00:00
|
|
|
def get_context_data(self, **kwargs):
|
|
|
|
""" Provide extra context information for the Build allocation page """
|
|
|
|
|
|
|
|
context = super(DetailView, self).get_context_data(**kwargs)
|
|
|
|
|
|
|
|
build = self.get_object()
|
|
|
|
part = build.part
|
|
|
|
bom_items = part.bom_items
|
|
|
|
|
|
|
|
context['part'] = part
|
|
|
|
context['bom_items'] = bom_items
|
2019-06-13 11:32:37 +00:00
|
|
|
context['BuildStatus'] = BuildStatus
|
2019-04-30 21:48:46 +00:00
|
|
|
|
2019-06-03 11:59:15 +00:00
|
|
|
context['bom_price'] = build.part.get_price_info(build.quantity, buy=False)
|
|
|
|
|
2019-05-26 22:07:38 +00:00
|
|
|
if str2bool(self.request.GET.get('edit', None)):
|
|
|
|
context['editing'] = True
|
|
|
|
|
2019-04-30 21:48:46 +00:00
|
|
|
return context
|
|
|
|
|
2018-04-29 14:23:44 +00:00
|
|
|
|
2018-04-27 15:06:42 +00:00
|
|
|
class BuildCreate(AjaxCreateView):
|
2019-04-27 10:35:14 +00:00
|
|
|
""" View to create a new Build object """
|
2018-04-17 13:13:41 +00:00
|
|
|
model = Build
|
|
|
|
context_object_name = 'build'
|
2019-05-09 22:33:54 +00:00
|
|
|
form_class = forms.EditBuildForm
|
2020-10-19 11:49:28 +00:00
|
|
|
ajax_form_title = _('New Build Order')
|
2018-04-27 15:06:42 +00:00
|
|
|
ajax_template_name = 'modal_form.html'
|
2020-10-06 09:29:16 +00:00
|
|
|
role_required = 'build.add'
|
2018-04-17 13:13:41 +00:00
|
|
|
|
|
|
|
def get_initial(self):
|
2019-04-27 10:35:14 +00:00
|
|
|
""" Get initial parameters for Build creation.
|
|
|
|
|
|
|
|
If 'part' is specified in the GET query, initialize the Build with the specified Part
|
|
|
|
"""
|
|
|
|
|
2018-04-17 13:13:41 +00:00
|
|
|
initials = super(BuildCreate, self).get_initial().copy()
|
|
|
|
|
2020-10-20 10:01:51 +00:00
|
|
|
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()
|
|
|
|
except (ValueError, Part.DoesNotExist):
|
|
|
|
pass
|
2018-04-17 13:13:41 +00:00
|
|
|
|
2020-10-19 12:22:09 +00:00
|
|
|
initials['reference'] = Build.getNextBuildNumber()
|
|
|
|
|
2020-04-25 13:17:07 +00:00
|
|
|
initials['parent'] = self.request.GET.get('parent', None)
|
|
|
|
|
2020-04-25 03:15:45 +00:00
|
|
|
# User has provided a SalesOrder ID
|
|
|
|
initials['sales_order'] = self.request.GET.get('sales_order', None)
|
|
|
|
|
|
|
|
initials['quantity'] = self.request.GET.get('quantity', 1)
|
2018-04-17 13:13:41 +00:00
|
|
|
|
|
|
|
return initials
|
|
|
|
|
2018-04-29 14:23:44 +00:00
|
|
|
def get_data(self):
|
|
|
|
return {
|
2020-02-11 21:11:59 +00:00
|
|
|
'success': _('Created new build'),
|
2018-04-29 14:23:44 +00:00
|
|
|
}
|
|
|
|
|
2018-04-17 13:13:41 +00:00
|
|
|
|
2018-04-27 15:06:42 +00:00
|
|
|
class BuildUpdate(AjaxUpdateView):
|
2019-04-27 10:35:14 +00:00
|
|
|
""" View for editing a Build object """
|
|
|
|
|
2018-04-17 13:13:41 +00:00
|
|
|
model = Build
|
2019-05-09 22:33:54 +00:00
|
|
|
form_class = forms.EditBuildForm
|
2018-04-17 13:13:41 +00:00
|
|
|
context_object_name = 'build'
|
2020-02-11 21:11:59 +00:00
|
|
|
ajax_form_title = _('Edit Build Details')
|
2018-04-27 15:06:42 +00:00
|
|
|
ajax_template_name = 'modal_form.html'
|
2020-10-06 09:29:16 +00:00
|
|
|
role_required = 'build.change'
|
2018-04-29 14:23:44 +00:00
|
|
|
|
|
|
|
def get_data(self):
|
|
|
|
return {
|
2020-02-11 21:11:59 +00:00
|
|
|
'info': _('Edited build'),
|
2018-04-29 14:23:44 +00:00
|
|
|
}
|
2019-04-30 05:35:35 +00:00
|
|
|
|
|
|
|
|
2019-08-15 09:34:55 +00:00
|
|
|
class BuildDelete(AjaxDeleteView):
|
|
|
|
""" View to delete a build """
|
|
|
|
|
|
|
|
model = Build
|
|
|
|
ajax_template_name = 'build/delete_build.html'
|
2020-02-11 21:11:59 +00:00
|
|
|
ajax_form_title = _('Delete Build')
|
2020-10-06 09:29:16 +00:00
|
|
|
role_required = 'build.delete'
|
2019-08-15 09:34:55 +00:00
|
|
|
|
|
|
|
|
2019-04-30 08:51:05 +00:00
|
|
|
class BuildItemDelete(AjaxDeleteView):
|
|
|
|
""" View to 'unallocate' a BuildItem.
|
|
|
|
Really we are deleting the BuildItem object from the database.
|
|
|
|
"""
|
|
|
|
|
|
|
|
model = BuildItem
|
|
|
|
ajax_template_name = 'build/delete_build_item.html'
|
2020-02-11 21:11:59 +00:00
|
|
|
ajax_form_title = _('Unallocate Stock')
|
2019-04-30 08:51:05 +00:00
|
|
|
context_object_name = 'item'
|
2020-10-06 09:29:16 +00:00
|
|
|
role_required = 'build.delete'
|
2019-04-30 08:51:05 +00:00
|
|
|
|
|
|
|
def get_data(self):
|
|
|
|
return {
|
2020-02-11 21:11:59 +00:00
|
|
|
'danger': _('Removed parts from build allocation')
|
2019-04-30 08:51:05 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2019-04-30 05:35:35 +00:00
|
|
|
class BuildItemCreate(AjaxCreateView):
|
|
|
|
""" View for allocating a new part to a build """
|
|
|
|
|
|
|
|
model = BuildItem
|
2019-05-09 22:33:54 +00:00
|
|
|
form_class = forms.EditBuildItemForm
|
2019-05-18 03:24:15 +00:00
|
|
|
ajax_template_name = 'build/create_build_item.html'
|
2020-02-11 21:11:59 +00:00
|
|
|
ajax_form_title = _('Allocate new Part')
|
2020-10-06 09:29:16 +00:00
|
|
|
role_required = 'build.add'
|
2019-04-30 05:35:35 +00:00
|
|
|
|
2019-05-18 03:24:15 +00:00
|
|
|
part = None
|
|
|
|
available_stock = None
|
|
|
|
|
|
|
|
def get_context_data(self):
|
|
|
|
ctx = super(AjaxCreateView, self).get_context_data()
|
|
|
|
|
|
|
|
if self.part:
|
|
|
|
ctx['part'] = self.part
|
|
|
|
|
|
|
|
if self.available_stock:
|
|
|
|
ctx['stock'] = self.available_stock
|
|
|
|
else:
|
|
|
|
ctx['no_stock'] = True
|
|
|
|
|
|
|
|
return ctx
|
|
|
|
|
2019-04-30 05:48:07 +00:00
|
|
|
def get_form(self):
|
|
|
|
""" Create Form for making / editing new Part object """
|
|
|
|
|
|
|
|
form = super(AjaxCreateView, self).get_form()
|
|
|
|
|
|
|
|
# If the Build object is specified, hide the input field.
|
|
|
|
# We do not want the users to be able to move a BuildItem to a different build
|
2019-04-30 06:38:09 +00:00
|
|
|
build_id = form['build'].value()
|
|
|
|
|
|
|
|
if build_id is not None:
|
2019-04-30 05:48:07 +00:00
|
|
|
form.fields['build'].widget = HiddenInput()
|
|
|
|
|
|
|
|
# If the sub_part is supplied, limit to matching stock items
|
|
|
|
part_id = self.get_param('part')
|
|
|
|
|
|
|
|
if part_id:
|
|
|
|
try:
|
2019-05-18 03:24:15 +00:00
|
|
|
self.part = Part.objects.get(pk=part_id)
|
2019-04-30 06:38:09 +00:00
|
|
|
|
2019-04-30 05:48:07 +00:00
|
|
|
query = form.fields['stock_item'].queryset
|
2019-04-30 06:38:09 +00:00
|
|
|
|
|
|
|
# Only allow StockItem objects which match the current part
|
2019-04-30 05:48:07 +00:00
|
|
|
query = query.filter(part=part_id)
|
2019-04-30 06:38:09 +00:00
|
|
|
|
|
|
|
if build_id is not None:
|
2019-05-10 09:12:56 +00:00
|
|
|
try:
|
|
|
|
build = Build.objects.get(id=build_id)
|
|
|
|
|
|
|
|
if build.take_from is not None:
|
|
|
|
# Limit query to stock items that are downstream of the 'take_from' location
|
|
|
|
query = query.filter(location__in=[loc for loc in build.take_from.getUniqueChildren()])
|
|
|
|
|
|
|
|
except Build.DoesNotExist:
|
|
|
|
pass
|
|
|
|
|
2019-04-30 06:38:09 +00:00
|
|
|
# Exclude StockItem objects which are already allocated to this build and part
|
|
|
|
query = query.exclude(id__in=[item.stock_item.id for item in BuildItem.objects.filter(build=build_id, stock_item__part=part_id)])
|
|
|
|
|
2019-04-30 05:48:07 +00:00
|
|
|
form.fields['stock_item'].queryset = query
|
2019-05-07 12:46:37 +00:00
|
|
|
|
|
|
|
stocks = query.all()
|
2019-05-18 03:24:15 +00:00
|
|
|
self.available_stock = stocks
|
|
|
|
|
2019-05-07 12:46:37 +00:00
|
|
|
# If there is only one item selected, select it
|
|
|
|
if len(stocks) == 1:
|
|
|
|
form.fields['stock_item'].initial = stocks[0].id
|
2019-05-07 13:03:05 +00:00
|
|
|
# There is no stock available
|
|
|
|
elif len(stocks) == 0:
|
|
|
|
# TODO - Add a message to the form describing the problem
|
|
|
|
pass
|
2019-05-07 12:46:37 +00:00
|
|
|
|
2019-04-30 05:48:07 +00:00
|
|
|
except Part.DoesNotExist:
|
2019-05-18 03:24:15 +00:00
|
|
|
self.part = None
|
2019-04-30 05:48:07 +00:00
|
|
|
pass
|
|
|
|
|
|
|
|
return form
|
|
|
|
|
2019-04-30 05:35:35 +00:00
|
|
|
def get_initial(self):
|
|
|
|
""" Provide initial data for BomItem. Look for the folllowing in the GET data:
|
|
|
|
|
|
|
|
- build: pk of the Build object
|
|
|
|
"""
|
|
|
|
|
|
|
|
initials = super(AjaxCreateView, self).get_initial().copy()
|
|
|
|
|
|
|
|
build_id = self.get_param('build')
|
2019-05-07 12:46:37 +00:00
|
|
|
part_id = self.get_param('part')
|
|
|
|
|
2020-04-25 13:17:07 +00:00
|
|
|
# Reference to a Part object
|
|
|
|
part = None
|
|
|
|
|
|
|
|
# Reference to a StockItem object
|
|
|
|
item = None
|
|
|
|
|
|
|
|
# Reference to a Build object
|
|
|
|
build = None
|
|
|
|
|
2019-05-07 12:46:37 +00:00
|
|
|
if part_id:
|
|
|
|
try:
|
|
|
|
part = Part.objects.get(pk=part_id)
|
2020-04-25 13:17:07 +00:00
|
|
|
initials['part'] = part
|
2019-05-07 12:46:37 +00:00
|
|
|
except Part.DoesNotExist:
|
2020-04-25 13:17:07 +00:00
|
|
|
pass
|
2019-08-15 11:19:59 +00:00
|
|
|
|
2019-04-30 05:35:35 +00:00
|
|
|
if build_id:
|
|
|
|
try:
|
2019-05-07 12:46:37 +00:00
|
|
|
build = Build.objects.get(pk=build_id)
|
|
|
|
initials['build'] = build
|
2020-04-25 13:17:07 +00:00
|
|
|
except Build.DoesNotExist:
|
|
|
|
pass
|
2019-05-07 12:46:37 +00:00
|
|
|
|
2020-04-25 13:17:07 +00:00
|
|
|
quantity = self.request.GET.get('quantity', None)
|
2019-05-07 12:46:37 +00:00
|
|
|
|
2020-04-25 13:17:07 +00:00
|
|
|
if quantity is not None:
|
|
|
|
quantity = float(quantity)
|
|
|
|
|
|
|
|
if quantity is None:
|
|
|
|
# Work out how many parts remain to be alloacted for the build
|
|
|
|
if part:
|
|
|
|
quantity = build.getUnallocatedQuantity(part)
|
|
|
|
|
|
|
|
item_id = self.get_param('item')
|
|
|
|
|
|
|
|
# If the request specifies a particular StockItem
|
|
|
|
if item_id:
|
|
|
|
try:
|
|
|
|
item = StockItem.objects.get(pk=item_id)
|
|
|
|
except:
|
2019-04-30 05:35:35 +00:00
|
|
|
pass
|
|
|
|
|
2020-04-25 13:17:07 +00:00
|
|
|
# If a StockItem is not selected, try to auto-select one
|
|
|
|
if item is None and part is not None:
|
|
|
|
items = StockItem.objects.filter(part=part)
|
|
|
|
if items.count() == 1:
|
|
|
|
item = items.first()
|
|
|
|
|
|
|
|
# Finally, if a StockItem is selected, ensure the quantity is not too much
|
|
|
|
if item is not None:
|
|
|
|
if quantity is None:
|
|
|
|
quantity = item.unallocated_quantity()
|
|
|
|
else:
|
|
|
|
quantity = min(quantity, item.unallocated_quantity())
|
|
|
|
|
|
|
|
if quantity is not None:
|
|
|
|
initials['quantity'] = quantity
|
|
|
|
|
2019-04-30 08:51:05 +00:00
|
|
|
return initials
|
|
|
|
|
|
|
|
|
|
|
|
class BuildItemEdit(AjaxUpdateView):
|
|
|
|
""" View to edit a BuildItem object """
|
|
|
|
|
|
|
|
model = BuildItem
|
|
|
|
ajax_template_name = 'modal_form.html'
|
2019-05-09 22:33:54 +00:00
|
|
|
form_class = forms.EditBuildItemForm
|
2020-02-11 21:11:59 +00:00
|
|
|
ajax_form_title = _('Edit Stock Allocation')
|
2020-10-06 09:29:16 +00:00
|
|
|
role_required = 'build.change'
|
2019-04-30 08:51:05 +00:00
|
|
|
|
|
|
|
def get_data(self):
|
|
|
|
return {
|
2020-02-11 21:11:59 +00:00
|
|
|
'info': _('Updated Build Item'),
|
2019-04-30 09:17:54 +00:00
|
|
|
}
|
2019-05-01 14:25:19 +00:00
|
|
|
|
|
|
|
def get_form(self):
|
|
|
|
""" Create form for editing a BuildItem.
|
|
|
|
|
|
|
|
- Limit the StockItem options to items that match the part
|
|
|
|
"""
|
|
|
|
|
|
|
|
build_item = self.get_object()
|
|
|
|
|
|
|
|
form = super(BuildItemEdit, self).get_form()
|
|
|
|
|
|
|
|
query = StockItem.objects.all()
|
|
|
|
|
|
|
|
if build_item.stock_item:
|
|
|
|
part_id = build_item.stock_item.part.id
|
|
|
|
query = query.filter(part=part_id)
|
|
|
|
|
|
|
|
form.fields['stock_item'].queryset = query
|
|
|
|
|
|
|
|
form.fields['build'].widget = HiddenInput()
|
|
|
|
|
|
|
|
return form
|