From 905d78e25ce95bef9670f3a2fd65d74a23bd6a05 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 2 May 2019 00:04:39 +1000 Subject: [PATCH] Complete build now works - Marks build as complete - Deletes temporary BuildItem objects - Preselects the part's default_location if there is one - Creates a new stockitem in the selected location --- InvenTree/InvenTree/views.py | 2 +- InvenTree/build/forms.py | 21 ++++- .../migrations/0008_auto_20190501_2344.py | 25 ++++++ InvenTree/build/models.py | 44 ++++++++++- InvenTree/build/templates/build/allocate.html | 7 +- InvenTree/build/templates/build/complete.html | 14 +++- InvenTree/build/views.py | 76 ++++++++++++++++--- InvenTree/static/script/inventree/build.js | 4 + .../migrations/0010_auto_20190501_2344.py | 18 +++++ InvenTree/stock/models.py | 2 +- InvenTree/templates/modal_form.html | 8 +- 11 files changed, 198 insertions(+), 23 deletions(-) create mode 100644 InvenTree/build/migrations/0008_auto_20190501_2344.py create mode 100644 InvenTree/stock/migrations/0010_auto_20190501_2344.py diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py index 26303a6549..04d1978745 100644 --- a/InvenTree/InvenTree/views.py +++ b/InvenTree/InvenTree/views.py @@ -199,7 +199,7 @@ class AjaxUpdateView(AjaxMixin, UpdateView): form = self.get_form() - return self.renderJsonResponse(request, form) + return self.renderJsonResponse(request, form, context=self.get_context_data()) def post(self, request, *args, **kwargs): """ Respond to POST request. diff --git a/InvenTree/build/forms.py b/InvenTree/build/forms.py index 87116c8646..de56ca34f7 100644 --- a/InvenTree/build/forms.py +++ b/InvenTree/build/forms.py @@ -6,8 +6,9 @@ Django Forms for interacting with Build objects from __future__ import unicode_literals from InvenTree.forms import HelperForm - +from django import forms from .models import Build, BuildItem +from stock.models import StockLocation class EditBuildForm(HelperForm): @@ -26,6 +27,24 @@ class EditBuildForm(HelperForm): ] +class CompleteBuildForm(HelperForm): + """ Form for marking a Build as complete """ + + location = forms.ModelChoiceField( + queryset=StockLocation.objects.all(), + help_text='Location of completed parts', + ) + + confirm = forms.BooleanField(required=False, help_text='Confirm build submission') + + class Meta: + model = Build + fields = [ + 'location', + 'confirm' + ] + + class EditBuildItemForm(HelperForm): """ Form for adding a new BuildItem to a Build """ diff --git a/InvenTree/build/migrations/0008_auto_20190501_2344.py b/InvenTree/build/migrations/0008_auto_20190501_2344.py new file mode 100644 index 0000000000..febdd2d1b1 --- /dev/null +++ b/InvenTree/build/migrations/0008_auto_20190501_2344.py @@ -0,0 +1,25 @@ +# Generated by Django 2.2 on 2019-05-01 13:44 + +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('build', '0007_auto_20190429_2255'), + ] + + operations = [ + migrations.AlterField( + model_name='build', + name='status', + field=models.PositiveIntegerField(choices=[(10, 'Pending'), (30, 'Cancelled'), (40, 'Complete')], default=10, validators=[django.core.validators.MinValueValidator(0)]), + ), + migrations.AlterField( + model_name='builditem', + name='build', + field=models.ForeignKey(help_text='Build to allocate parts', on_delete=django.db.models.deletion.CASCADE, related_name='allocated_stock', to='build.Build'), + ), + ] diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index d13919c24e..56ff7c5804 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -5,6 +5,8 @@ Build database model definitions # -*- coding: utf-8 -*- from __future__ import unicode_literals +from datetime import datetime + from django.utils.translation import ugettext as _ from django.core.exceptions import ValidationError @@ -128,22 +130,56 @@ class Build(models.Model): - Save the Build object """ - for item in BuildItem.objects.filter(build=self.id): + for item in self.allocated_stock.all(): item.delete() self.status = self.CANCELLED self.save() - print("cancelled!") @transaction.atomic - def completeBuild(self): + def completeBuild(self, location, user): """ Mark the Build as COMPLETE - Takes allocated items from stock - Delete pending BuildItem objects """ - print("complete!!!!") + for item in self.allocated_stock.all(): + + # Subtract stock from the item + item.stock_item.take_stock( + item.quantity, + user, + 'Removed {n} items to build {m} x {part}'.format( + n=item.quantity, + m=self.quantity, + part=self.part.name + ) + ) + + # Delete the item + item.delete() + + # Mark the date of completion + self.completion_date = datetime.now().date() + + # Add stock of the newly created item + item = StockItem.objects.create( + part=self.part, + location=location, + quantity=self.quantity, + batch=str(self.batch) if self.batch else '', + notes='Built {q} on {now}'.format( + q=self.quantity, + now=str(datetime.now().date()) + ) + ) + + item.save() + + # Finally, mark the build as complete + self.status = self.COMPLETE + self.save() @property def required_parts(self): diff --git a/InvenTree/build/templates/build/allocate.html b/InvenTree/build/templates/build/allocate.html index e9438d898f..c3cda38429 100644 --- a/InvenTree/build/templates/build/allocate.html +++ b/InvenTree/build/templates/build/allocate.html @@ -7,7 +7,7 @@

Allocate Parts for Build

{{ build.title }}

- +{{ build.quantity }} x {{ build.part.name }}
{% for bom_item in bom_items.all %} @@ -46,8 +46,6 @@ {% endfor %} - /* - $("#complete-build").on('click', function() { launchModalForm( "{% url 'build-complete' build.id %}", @@ -57,6 +55,5 @@ } ); }); - */ -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/InvenTree/build/templates/build/complete.html b/InvenTree/build/templates/build/complete.html index 6cf26c2a6c..ce1577259d 100644 --- a/InvenTree/build/templates/build/complete.html +++ b/InvenTree/build/templates/build/complete.html @@ -1 +1,13 @@ -Mark as COMPLETE \ No newline at end of file +{% extends "modal_form.html" %} + +{% block pre_form_content %} +Build: {{ build.title }} - {{ build.quantity }} x {{ build.part.name }} +
+Are you sure you want to mark this build as complete? +
+Completing the build will perform the following actions: + +{% endblock %} \ No newline at end of file diff --git a/InvenTree/build/views.py b/InvenTree/build/views.py index ebc5372596..76558d87ee 100644 --- a/InvenTree/build/views.py +++ b/InvenTree/build/views.py @@ -12,7 +12,8 @@ from django.forms import HiddenInput from part.models import Part from .models import Build, BuildItem -from .forms import EditBuildForm, EditBuildItemForm +from .forms import EditBuildForm, EditBuildItemForm, CompleteBuildForm +from stock.models import StockLocation from InvenTree.views import AjaxView, AjaxUpdateView, AjaxCreateView, AjaxDeleteView @@ -68,7 +69,7 @@ class BuildCancel(AjaxView): } -class BuildComplete(AjaxView): +class BuildComplete(AjaxUpdateView): """ View to mark a build as Complete. - Notifies the user of which parts will be removed from stock. @@ -77,19 +78,76 @@ class BuildComplete(AjaxView): """ model = Build - ajax_template_name = "build/complete.html" - ajax_form_title = "Complete Build" + form_class = CompleteBuildForm context_object_name = "build" - fields = [] + 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) + 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 + """ + + context = super(BuildComplete, self).get_context_data(**kwargs).copy() + context['build'] = self.get_object() + + return context def post(self, request, *args, **kwargs): - """ Handle POST request. Mark the build as COMPLETE """ + """ 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 = get_object_or_404(Build, pk=self.kwargs['pk']) + build = self.get_object() - build.complete() + form = self.get_form() - return self.renderJsonResponse(request, None) + confirm = 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: + print('id:', loc_id) + 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 """ diff --git a/InvenTree/static/script/inventree/build.js b/InvenTree/static/script/inventree/build.js index fd20af0743..826e2b1661 100644 --- a/InvenTree/static/script/inventree/build.js +++ b/InvenTree/static/script/inventree/build.js @@ -30,6 +30,10 @@ function loadAllocationTable(table, part_id, part, url, required, button) { return '' + value.quantity + ' x ' + value.part_name + ' @ ' + value.location_name; } }, + { + field: 'stock_item_detail.quantity', + title: 'Available', + }, { field: 'quantity', title: 'Allocated', diff --git a/InvenTree/stock/migrations/0010_auto_20190501_2344.py b/InvenTree/stock/migrations/0010_auto_20190501_2344.py new file mode 100644 index 0000000000..61ea730b03 --- /dev/null +++ b/InvenTree/stock/migrations/0010_auto_20190501_2344.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2 on 2019-05-01 13:44 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('stock', '0009_auto_20190428_0841'), + ] + + operations = [ + migrations.AlterField( + model_name='stockitem', + name='batch', + field=models.CharField(blank=True, help_text='Batch code for this stock item', max_length=100, null=True), + ), + ] diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 2f61c068cd..cb0ad8aea4 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -158,7 +158,7 @@ class StockItem(models.Model): URL = models.URLField(max_length=125, blank=True) # Optional batch information - batch = models.CharField(max_length=100, blank=True, + batch = models.CharField(max_length=100, blank=True, null=True, help_text='Batch code for this stock item') # If this part was produced by a build, point to that build here diff --git a/InvenTree/templates/modal_form.html b/InvenTree/templates/modal_form.html index 1318e4f238..566671c657 100644 --- a/InvenTree/templates/modal_form.html +++ b/InvenTree/templates/modal_form.html @@ -1,3 +1,6 @@ +{% block pre_form_content %} +{% endblock %} + {% if form.non_field_errors %}