""" Django views for interacting with Build objects """ # -*- coding: utf-8 -*- from __future__ import unicode_literals from django.views.generic import DetailView, ListView from django.forms import HiddenInput 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 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['active'] = self.get_queryset().filter(status__in=[Build.PENDING, ]) context['completed'] = self.get_queryset().filter(status=Build.COMPLETE) context['cancelled'] = self.get_queryset().filter(status=Build.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_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 = self.get_object() # Build object context = super(BuildComplete, self).get_context_data(**kwargs).copy() 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 StockLocation.DoesNotExist: form.errors['location'] = ['Invalid location selected'] if valid: build.completeBuild(location, request.user) data = { 'form_valid': valid, } return self.renderJsonResponse(request, form, data) def get_data(self): """ Provide feedback data back to the form """ return { 'info': 'Build marked as COMPLETE' } 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) 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 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() part_id = self.request.GET.get('part', None) if part_id: try: initials['part'] = Part.objects.get(pk=part_id) except Part.DoesNotExist: pass 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 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') if part_id: try: part = Part.objects.get(pk=part_id) except Part.DoesNotExist: part = None if build_id: try: build = Build.objects.get(pk=build_id) initials['build'] = build # Try to work out how many parts to allocate if part: unallocated = build.getUnallocatedQuantity(part) initials['quantity'] = unallocated except Build.DoesNotExist: pass 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