""" Django views for interacting with Build objects """ # -*- coding: utf-8 -*- from __future__ import unicode_literals from django.utils.translation import ugettext as _ from django.core.exceptions import ValidationError from django.views.generic import DetailView, ListView, UpdateView from django.forms import HiddenInput from django.urls import reverse from part.models import Part from .models import Build, BuildItem from . import forms from stock.models import StockLocation, StockItem from InvenTree.views import AjaxUpdateView, AjaxCreateView, AjaxDeleteView from InvenTree.helpers import str2bool, ExtractSerialNumbers from InvenTree.status_codes import BuildStatus class BuildIndex(ListView): """ View for displaying list of Builds """ model = Build template_name = 'build/index.html' context_object_name = 'builds' def get_queryset(self): """ Return all Build objects (order by date, newest first) """ return Build.objects.order_by('status', '-completion_date') def get_context_data(self, **kwargs): context = super(BuildIndex, self).get_context_data(**kwargs).copy() context['BuildStatus'] = BuildStatus 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) return context class BuildCancel(AjaxUpdateView): """ View to cancel a Build. Provides a cancellation information dialog """ model = Build ajax_template_name = 'build/cancel.html' ajax_form_title = _('Cancel Build') context_object_name = 'build' form_class = forms.CancelBuildForm def post(self, request, *args, **kwargs): """ Handle POST request. Mark the build status as CANCELLED """ build = self.get_object() form = self.get_form() valid = form.is_valid() confirm = str2bool(request.POST.get('confirm_cancel', False)) if confirm: build.cancelBuild(request.user) else: form.errors['confirm_cancel'] = [_('Confirm build cancellation')] valid = False data = { 'form_valid': valid, 'danger': _('Build was cancelled') } return self.renderJsonResponse(request, form, data=data) 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 form_class = forms.ConfirmBuildForm context_object_name = 'build' ajax_form_title = _('Allocate Stock') ajax_template_name = 'build/auto_allocate.html' 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: context['error'] = _('No matching build found') 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: form.errors['confirm'] = [_('Confirm stock allocation')] form.non_field_errors = [_('Check the confirmation box at the bottom of the list')] else: build.autoAllocate() valid = True data = { 'form_valid': valid, } return self.renderJsonResponse(request, form, data, context=self.get_context_data()) 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 ajax_form_title = _("Unallocate Stock") ajax_template_name = "build/unallocate.html" 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: form.errors['confirm'] = [_('Confirm unallocation of build stock')] form.non_field_errors = [_('Check the confirmation box')] else: build.unallocateStock() valid = True data = { 'form_valid': valid, } return self.renderJsonResponse(request, form, data) class BuildComplete(AjaxUpdateView): """ View to mark a build as Complete. - Notifies the user of which parts will be removed from stock. - Removes allocated items from stock - Deletes pending BuildItem objects """ model = Build form_class = forms.CompleteBuildForm context_object_name = "build" ajax_form_title = _("Complete Build") ajax_template_name = "build/complete.html" 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') else: if build.quantity == 1: text = _('Next available serial number is') else: text = _('Next available serial numbers are') form.field_placeholder['serial_numbers'] = text + " " + build.part.getSerialNumberString(build.quantity) form.rebuild_layout() return form 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() if build.part.default_location is not None: try: location = StockLocation.objects.get(pk=build.part.default_location.id) initials['location'] = location 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 """ build = Build.objects.get(id=self.kwargs['pk']) context = {} # Build object context['build'] = build # Items to be removed from stock taking = BuildItem.objects.filter(build=build.id) context['taking'] = taking return context def post(self, request, *args, **kwargs): """ 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 """ build = self.get_object() form = self.get_form() confirm = str2bool(request.POST.get('confirm', False)) loc_id = request.POST.get('location', None) valid = False if confirm is False: form.errors['confirm'] = [ _('Confirm completion of build'), ] else: try: location = StockLocation.objects.get(id=loc_id) valid = True except (ValueError, StockLocation.DoesNotExist): form.errors['location'] = [_('Invalid location selected')] serials = [] if build.part.trackable: # A build for a trackable part may optionally specify serial numbers. sn = request.POST.get('serial_numbers', '') sn = str(sn).strip() # 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) existing = [] for serial in serials: if build.part.checkIfSerialNumberExists(serial): 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 except ValidationError as e: form.errors['serial_numbers'] = e.messages valid = False if valid: if not build.completeBuild(location, serials, request.user): form.non_field_errors = [('Build could not be completed')] valid = False data = { 'form_valid': valid, } return self.renderJsonResponse(request, form, data, context=self.get_context_data()) def get_data(self): """ Provide feedback data back to the form """ return { 'info': _('Build marked as COMPLETE') } class BuildNotes(UpdateView): """ View for editing the 'notes' field of a Build object. """ context_object_name = 'build' template_name = 'build/notes.html' model = Build 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 class BuildDetail(DetailView): """ Detail view of a single Build object. """ model = Build template_name = 'build/detail.html' context_object_name = 'build' 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) ctx['BuildStatus'] = BuildStatus return ctx class BuildAllocate(DetailView): """ View for allocating parts to a Build """ model = Build context_object_name = 'build' template_name = 'build/allocate.html' 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 context['BuildStatus'] = BuildStatus context['bom_price'] = build.part.get_price_info(build.quantity, buy=False) if str2bool(self.request.GET.get('edit', None)): context['editing'] = True 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 = _('Start new Build') ajax_template_name = 'modal_form.html' 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() # User has provided a Part ID initials['part'] = self.request.GET.get('part', None) 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) return initials def get_data(self): return { 'success': _('Created new build'), } class BuildUpdate(AjaxUpdateView): """ View for editing a Build object """ model = Build form_class = forms.EditBuildForm context_object_name = 'build' ajax_form_title = _('Edit Build Details') ajax_template_name = 'modal_form.html' def get_data(self): return { 'info': _('Edited build'), } class BuildDelete(AjaxDeleteView): """ View to delete a build """ model = Build ajax_template_name = 'build/delete_build.html' ajax_form_title = _('Delete Build') 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' ajax_form_title = _('Unallocate Stock') context_object_name = 'item' def get_data(self): return { 'danger': _('Removed parts from build allocation') } class BuildItemCreate(AjaxCreateView): """ View for allocating a new part to a build """ model = BuildItem form_class = forms.EditBuildItemForm ajax_template_name = 'build/create_build_item.html' ajax_form_title = _('Allocate new Part') 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 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 build_id = form['build'].value() if build_id is not None: 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: self.part = Part.objects.get(pk=part_id) query = form.fields['stock_item'].queryset # Only allow StockItem objects which match the current part query = query.filter(part=part_id) if build_id is not None: 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 # 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)]) form.fields['stock_item'].queryset = query stocks = query.all() self.available_stock = stocks # If there is only one item selected, select it if len(stocks) == 1: form.fields['stock_item'].initial = stocks[0].id # There is no stock available elif len(stocks) == 0: # TODO - Add a message to the form describing the problem pass except Part.DoesNotExist: self.part = None pass return form 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') part_id = self.get_param('part') # Reference to a Part object part = None # Reference to a StockItem object item = None # Reference to a Build object build = None if part_id: try: part = Part.objects.get(pk=part_id) initials['part'] = part except Part.DoesNotExist: pass if build_id: try: build = Build.objects.get(pk=build_id) initials['build'] = build except Build.DoesNotExist: pass quantity = self.request.GET.get('quantity', None) 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: pass # 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 return initials class BuildItemEdit(AjaxUpdateView): """ View to edit a BuildItem object """ model = BuildItem ajax_template_name = 'modal_form.html' form_class = forms.EditBuildItemForm ajax_form_title = _('Edit Stock Allocation') def get_data(self): return { 'info': _('Updated Build Item'), } 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