From 989611cae268c9fdc5cf5834930e13d92d0da8c2 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 29 Apr 2019 22:19:13 +1000 Subject: [PATCH 01/37] Added BuildItemAllocation Model - Used to link stock items to a build --- InvenTree/build/forms.py | 2 +- .../migrations/0003_builditemallocation.py | 25 +++++++ InvenTree/build/migrations/0004_build_url.py | 18 +++++ InvenTree/build/models.py | 70 +++++++++++++++---- 4 files changed, 99 insertions(+), 16 deletions(-) create mode 100644 InvenTree/build/migrations/0003_builditemallocation.py create mode 100644 InvenTree/build/migrations/0004_build_url.py diff --git a/InvenTree/build/forms.py b/InvenTree/build/forms.py index 6254f0745c..05b1c99034 100644 --- a/InvenTree/build/forms.py +++ b/InvenTree/build/forms.py @@ -22,6 +22,6 @@ class EditBuildForm(HelperForm): 'quantity', 'batch', 'notes', - # 'status', + 'status', # 'completion_date', ] diff --git a/InvenTree/build/migrations/0003_builditemallocation.py b/InvenTree/build/migrations/0003_builditemallocation.py new file mode 100644 index 0000000000..add13c7ac1 --- /dev/null +++ b/InvenTree/build/migrations/0003_builditemallocation.py @@ -0,0 +1,25 @@ +# Generated by Django 2.2 on 2019-04-29 12:14 + +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('stock', '0009_auto_20190428_0841'), + ('build', '0002_auto_20190412_2030'), + ] + + operations = [ + migrations.CreateModel( + name='BuildItemAllocation', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('quantity', models.PositiveIntegerField(default=1, help_text='Stock quantity to allocate to build', validators=[django.core.validators.MinValueValidator(1)])), + ('build', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='allocated_stock', to='build.Build')), + ('stock', models.ForeignKey(help_text='Stock Item to allocate to build', on_delete=django.db.models.deletion.CASCADE, related_name='allocations', to='stock.StockItem')), + ], + ), + ] diff --git a/InvenTree/build/migrations/0004_build_url.py b/InvenTree/build/migrations/0004_build_url.py new file mode 100644 index 0000000000..187ac938d1 --- /dev/null +++ b/InvenTree/build/migrations/0004_build_url.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2 on 2019-04-29 12:18 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('build', '0003_builditemallocation'), + ] + + operations = [ + migrations.AddField( + model_name='build', + name='URL', + field=models.URLField(blank=True, help_text='Link to external URL'), + ), + ] diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index 880c1b08b1..6d83f74bf3 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -14,6 +14,17 @@ from django.core.validators import MinValueValidator class Build(models.Model): """ A Build object organises the creation of new parts from the component parts. + + Attributes: + part: The part to be built (from component BOM items) + title: Brief title describing the build (required) + quantity: Number of units to be built + status: Build status code + batch: Batch code transferred to build parts (optional) + creation_date: Date the build was created (auto) + completion_date: Date the build was completed + URL: External URL for extra information + notes: Text notes """ def get_absolute_url(self): @@ -23,16 +34,15 @@ class Build(models.Model): related_name='builds', limit_choices_to={'buildable': True}, ) - """ A reference to the part being built - only parts marked as 'buildable' may be selected """ - #: Brief title describing the build title = models.CharField(max_length=100, help_text='Brief description of the build') - #: Number of output parts to build - quantity = models.PositiveIntegerField(default=1, - validators=[MinValueValidator(1)], - help_text='Number of parts to build') - + quantity = models.PositiveIntegerField( + default=1, + validators=[MinValueValidator(1)], + help_text='Number of parts to build' + ) + # Build status codes PENDING = 10 # Build is pending / active HOLDING = 20 # Build is currently being held @@ -46,23 +56,21 @@ class Build(models.Model): COMPLETE: _("Complete"), } - #: Status of the build (ref BUILD_STATUS_CODES) status = models.PositiveIntegerField(default=PENDING, choices=BUILD_STATUS_CODES.items(), validators=[MinValueValidator(0)]) - - #: Batch number for the build (optional) + batch = models.CharField(max_length=100, blank=True, null=True, help_text='Batch code for this build output') - - #: Date the build model was 'created' + creation_date = models.DateField(auto_now=True, editable=False) - - #: Date the build was 'completed' (and parts removed from stock) + completion_date = models.DateField(null=True, blank=True) + + URL = models.URLField(blank=True, help_text='Link to external URL') - #: Notes attached to each build output notes = models.TextField(blank=True) + """ Notes attached to each build output """ @property def required_parts(self): @@ -106,3 +114,35 @@ class Build(models.Model): def is_complete(self): """ Returns True if the build status is COMPLETE """ return self.status == self.COMPLETE + + +class BuildItemAllocation(models.Model): + """ A BuildItemAllocation links multiple StockItem objects to a Build. + These are used to allocate part stock to a build. + Once the Build is completed, the parts are removed from stock and the + BuildItemAllocation objects are removed. + + Attributes: + build: Link to a Build object + stock: Link to a StockItem object + quantity: Number of units allocated + """ + + build = models.ForeignKey( + Build, + on_delete=models.CASCADE, + related_name='allocated_stock', + ) + + stock = models.ForeignKey( + 'stock.StockItem', + on_delete=models.CASCADE, + related_name='allocations', + help_text='Stock Item to allocate to build', + ) + + quantity = models.PositiveIntegerField( + default=1, + validators=[MinValueValidator(1)], + help_text='Stock quantity to allocate to build' + ) From 6326d6d0508a2679e26b04dbaec228eab3b2f2f0 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 29 Apr 2019 22:21:18 +1000 Subject: [PATCH 02/37] Fixed spelling mistake --- InvenTree/part/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 04d5471f92..8e503c88a0 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -125,7 +125,7 @@ class Part(models.Model): IPN = models.CharField(max_length=100, blank=True, help_text='Internal Part Number') # Provide a URL for an external link - URL = models.URLField(blank=True, help_text='Link to extenal URL') + URL = models.URLField(blank=True, help_text='Link to external URL') # Part category - all parts must be assigned to a category category = models.ForeignKey(PartCategory, related_name='parts', From e25579141e76a46bbfe9a42d57404928f6efa31c Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 29 Apr 2019 22:30:21 +1000 Subject: [PATCH 03/37] Rename BuildItemAllocation to BuildItem --- .../migrations/0005_auto_20190429_2229.py | 18 ++++++++++++++++++ InvenTree/build/models.py | 4 ++-- .../part/migrations/0013_auto_20190429_2229.py | 18 ++++++++++++++++++ 3 files changed, 38 insertions(+), 2 deletions(-) create mode 100644 InvenTree/build/migrations/0005_auto_20190429_2229.py create mode 100644 InvenTree/part/migrations/0013_auto_20190429_2229.py diff --git a/InvenTree/build/migrations/0005_auto_20190429_2229.py b/InvenTree/build/migrations/0005_auto_20190429_2229.py new file mode 100644 index 0000000000..73ebca7e1b --- /dev/null +++ b/InvenTree/build/migrations/0005_auto_20190429_2229.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2 on 2019-04-29 12:29 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('stock', '0009_auto_20190428_0841'), + ('build', '0004_build_url'), + ] + + operations = [ + migrations.RenameModel( + old_name='BuildItemAllocation', + new_name='BuildItem', + ), + ] diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index 6d83f74bf3..a164db50ee 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -116,8 +116,8 @@ class Build(models.Model): return self.status == self.COMPLETE -class BuildItemAllocation(models.Model): - """ A BuildItemAllocation links multiple StockItem objects to a Build. +class BuildItem(models.Model): + """ A BuildItem links multiple StockItem objects to a Build. These are used to allocate part stock to a build. Once the Build is completed, the parts are removed from stock and the BuildItemAllocation objects are removed. diff --git a/InvenTree/part/migrations/0013_auto_20190429_2229.py b/InvenTree/part/migrations/0013_auto_20190429_2229.py new file mode 100644 index 0000000000..9c339b18b1 --- /dev/null +++ b/InvenTree/part/migrations/0013_auto_20190429_2229.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2 on 2019-04-29 12:29 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('part', '0012_part_active'), + ] + + operations = [ + migrations.AlterField( + model_name='part', + name='URL', + field=models.URLField(blank=True, help_text='Link to external URL'), + ), + ] From f42116c0d9d107a5a48028ac411730f60dbc158a Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 29 Apr 2019 22:33:39 +1000 Subject: [PATCH 04/37] Renamed 'stock' to 'stock_item' --- .../migrations/0006_auto_20190429_2233.py | 18 ++++++++++++++++++ InvenTree/build/models.py | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 InvenTree/build/migrations/0006_auto_20190429_2233.py diff --git a/InvenTree/build/migrations/0006_auto_20190429_2233.py b/InvenTree/build/migrations/0006_auto_20190429_2233.py new file mode 100644 index 0000000000..5d03e13b75 --- /dev/null +++ b/InvenTree/build/migrations/0006_auto_20190429_2233.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2 on 2019-04-29 12:33 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('build', '0005_auto_20190429_2229'), + ] + + operations = [ + migrations.RenameField( + model_name='builditem', + old_name='stock', + new_name='stock_item', + ), + ] diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index a164db50ee..d0c3bbee03 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -134,7 +134,7 @@ class BuildItem(models.Model): related_name='allocated_stock', ) - stock = models.ForeignKey( + stock_item = models.ForeignKey( 'stock.StockItem', on_delete=models.CASCADE, related_name='allocations', From 17d9d25fb9906eb2966ac50d9811cc71e7491671 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 29 Apr 2019 22:56:40 +1000 Subject: [PATCH 05/37] Require Build and StockItem to be unique_together in BuildItem class --- .../migrations/0007_auto_20190429_2255.py | 18 ++++++++++++++++ InvenTree/build/models.py | 21 +++++++++++++++++++ InvenTree/build/serializers.py | 18 +++++++++++++--- 3 files changed, 54 insertions(+), 3 deletions(-) create mode 100644 InvenTree/build/migrations/0007_auto_20190429_2255.py diff --git a/InvenTree/build/migrations/0007_auto_20190429_2255.py b/InvenTree/build/migrations/0007_auto_20190429_2255.py new file mode 100644 index 0000000000..8a01606b84 --- /dev/null +++ b/InvenTree/build/migrations/0007_auto_20190429_2255.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2 on 2019-04-29 12:55 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('stock', '0009_auto_20190428_0841'), + ('build', '0006_auto_20190429_2233'), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='builditem', + unique_together={('build', 'stock_item')}, + ), + ] diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index d0c3bbee03..2d2ab0e099 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -6,6 +6,7 @@ Build database model definitions from __future__ import unicode_literals from django.utils.translation import ugettext as _ +from django.core.exceptions import ValidationError from django.urls import reverse from django.db import models @@ -128,6 +129,26 @@ class BuildItem(models.Model): quantity: Number of units allocated """ + class Meta: + unique_together = [ + ('build', 'stock_item'), + ] + + def clean(self): + """ Check validity of the BuildItem model. + The following checks are performed: + + - StockItem.part must be in the BOM of the Part object referenced by Build + """ + + if self.stock_item.part not in self.build.part.required_parts(): + print('stock_item:', self.stock_item.part) + for p in self.build.part.bom_items.all(): + print('bom_part:', p) + raise ValidationError( + {'stock_item': _("Selected stock item not found in BOM for part '{p}'".format(p=str(self.build.part)))} + ) + build = models.ForeignKey( Build, on_delete=models.CASCADE, diff --git a/InvenTree/build/serializers.py b/InvenTree/build/serializers.py index 955a8d59f5..efe4ca6b69 100644 --- a/InvenTree/build/serializers.py +++ b/InvenTree/build/serializers.py @@ -6,11 +6,11 @@ JSON serializers for Build API from __future__ import unicode_literals from rest_framework import serializers +from InvenTree.serializers import InvenTreeModelSerializer -from .models import Build +from .models import Build, BuildItem - -class BuildSerializer(serializers.ModelSerializer): +class BuildSerializer(InvenTreeModelSerializer): """ Serializes a Build object """ url = serializers.CharField(source='get_absolute_url', read_only=True) @@ -29,3 +29,15 @@ class BuildSerializer(serializers.ModelSerializer): 'status', 'status_text', 'notes'] + + +class BuildItemSerializer(InvenTreeModelSerializer): + """ Serializes a BuildItem object """ + + class Meta: + model = BuildItem + fields = [ + 'build', + 'stock', + 'quantity' + ] From c29827e45a555b6620bf9e9533a3149aeb463566 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 29 Apr 2019 22:59:42 +1000 Subject: [PATCH 06/37] Add API endpoint and serializer for BuildItem model --- InvenTree/build/api.py | 39 +++++++++++++++++---- InvenTree/build/models.py | 3 ++ InvenTree/build/serializers.py | 5 ++- InvenTree/build/templates/build/detail.html | 3 ++ InvenTree/part/models.py | 6 ++++ 5 files changed, 49 insertions(+), 7 deletions(-) diff --git a/InvenTree/build/api.py b/InvenTree/build/api.py index 393bd1f073..b987328702 100644 --- a/InvenTree/build/api.py +++ b/InvenTree/build/api.py @@ -9,10 +9,10 @@ from django_filters.rest_framework import DjangoFilterBackend from rest_framework import filters from rest_framework import generics, permissions -from django.conf.urls import url +from django.conf.urls import url, include -from .models import Build -from .serializers import BuildSerializer +from .models import Build, BuildItem +from .serializers import BuildSerializer, BuildItemSerializer class BuildList(generics.ListCreateAPIView): @@ -36,10 +36,37 @@ class BuildList(generics.ListCreateAPIView): ] filter_fields = [ - 'part', + 'build', ] -build_api_urls = [ - url(r'^.*$', BuildList.as_view(), name='api-build-list') +class BuildItemList(generics.ListCreateAPIView): + """ API endpoint for accessing a list of BuildItem objects + + - GET: Return list of objects + - POST: Create a new BuildItem object + """ + + queryset = BuildItem.objects.all() + serializer_class = BuildItemSerializer + + permission_classes = [ + permissions.IsAuthenticatedOrReadOnly, + ] + + filter_fields = [ + 'build', + 'part', + 'stock_item' + ] + + +build_item_api_urls = [ + url('^.*$', BuildItemList.as_view(), name='api-build-item-list'), +] + +build_api_urls = [ + url(r'^item/?', include(build_item_api_urls)), + + url(r'^.*$', BuildList.as_view(), name='api-build-list'), ] diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index 2d2ab0e099..cd4c71422f 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -28,6 +28,9 @@ class Build(models.Model): notes: Text notes """ + def __str__(self): + return "Build {q} x {part}".format(q=self.quantity, part=str(self.part)) + def get_absolute_url(self): return reverse('build-detail', kwargs={'pk': self.id}) diff --git a/InvenTree/build/serializers.py b/InvenTree/build/serializers.py index efe4ca6b69..65fafc12ab 100644 --- a/InvenTree/build/serializers.py +++ b/InvenTree/build/serializers.py @@ -34,10 +34,13 @@ class BuildSerializer(InvenTreeModelSerializer): class BuildItemSerializer(InvenTreeModelSerializer): """ Serializes a BuildItem object """ + part = serializers.CharField(source='stock_item.part', read_only=True) + class Meta: model = BuildItem fields = [ 'build', - 'stock', + 'part', + 'stock_item', 'quantity' ] diff --git a/InvenTree/build/templates/build/detail.html b/InvenTree/build/templates/build/detail.html index ad25b4307d..4f1c871a3f 100644 --- a/InvenTree/build/templates/build/detail.html +++ b/InvenTree/build/templates/build/detail.html @@ -73,6 +73,7 @@ {% endif %} +{% if build.is_active %}

Required Parts

@@ -93,6 +94,8 @@
+{% endif %} + {% include 'modals.html' %} {% endblock %} diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 8e503c88a0..635ad9fa97 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -307,6 +307,12 @@ class Part(models.Model): def used_in_count(self): return self.used_in.count() + def required_parts(self): + parts = [] + for bom in self.bom_items.all(): + parts.append(bom.sub_part) + return parts + @property def supplier_count(self): # Return the number of supplier parts available for this part From 192f604b76b142615d6034f843890da2a19fbe4b Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 29 Apr 2019 23:45:05 +1000 Subject: [PATCH 07/37] Override get_queryset for BuildItem API view - Special management to allow filtering by part --- InvenTree/build/api.py | 23 ++++++++++++++++++++--- InvenTree/build/serializers.py | 5 ++++- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/InvenTree/build/api.py b/InvenTree/build/api.py index b987328702..8e0a92917e 100644 --- a/InvenTree/build/api.py +++ b/InvenTree/build/api.py @@ -36,7 +36,7 @@ class BuildList(generics.ListCreateAPIView): ] filter_fields = [ - 'build', + 'part', ] @@ -47,16 +47,33 @@ class BuildItemList(generics.ListCreateAPIView): - POST: Create a new BuildItem object """ - queryset = BuildItem.objects.all() serializer_class = BuildItemSerializer + def get_queryset(self): + """ Override the queryset method, + to allow filtering by stock_item.part + """ + + # Does the user wish to filter by part? + part_pk = self.request.query_params.get('part', None) + + query = BuildItem.objects.all() + + if part_pk: + query = query.filter(stock_item__part=part_pk) + + return query + permission_classes = [ permissions.IsAuthenticatedOrReadOnly, ] + filter_backends = [ + DjangoFilterBackend, + ] + filter_fields = [ 'build', - 'part', 'stock_item' ] diff --git a/InvenTree/build/serializers.py b/InvenTree/build/serializers.py index 65fafc12ab..c5ef3e49eb 100644 --- a/InvenTree/build/serializers.py +++ b/InvenTree/build/serializers.py @@ -34,13 +34,16 @@ class BuildSerializer(InvenTreeModelSerializer): class BuildItemSerializer(InvenTreeModelSerializer): """ Serializes a BuildItem object """ - part = serializers.CharField(source='stock_item.part', read_only=True) + part = serializers.IntegerField(source='stock_item.part.pk', read_only=True) + part_name = serializers.CharField(source='stock_item.part', read_only=True) class Meta: model = BuildItem fields = [ + 'pk', 'build', 'part', + 'part_name', 'stock_item', 'quantity' ] From 5c5411132a8a1ed061e138b0652658caf1644876 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 29 Apr 2019 23:56:02 +1000 Subject: [PATCH 08/37] First pass at build allocation table - Uses bootstrap-table "detailView" function --- InvenTree/build/serializers.py | 5 ++- InvenTree/build/templates/build/allocate.html | 33 +++++++++++++++++-- InvenTree/static/script/inventree/part.js | 2 +- InvenTree/stock/serializers.py | 18 ++++++++++ 4 files changed, 54 insertions(+), 4 deletions(-) diff --git a/InvenTree/build/serializers.py b/InvenTree/build/serializers.py index c5ef3e49eb..8ad09f616b 100644 --- a/InvenTree/build/serializers.py +++ b/InvenTree/build/serializers.py @@ -7,6 +7,7 @@ from __future__ import unicode_literals from rest_framework import serializers from InvenTree.serializers import InvenTreeModelSerializer +from stock.serializers import StockItemSerializerBrief from .models import Build, BuildItem @@ -35,7 +36,8 @@ class BuildItemSerializer(InvenTreeModelSerializer): """ Serializes a BuildItem object """ part = serializers.IntegerField(source='stock_item.part.pk', read_only=True) - part_name = serializers.CharField(source='stock_item.part', read_only=True) + part_name = serializers.CharField(source='stock_item.part.name', read_only=True) + stock_item_detail = StockItemSerializerBrief(source='stock_item', read_only=True) class Meta: model = BuildItem @@ -45,5 +47,6 @@ class BuildItemSerializer(InvenTreeModelSerializer): 'part', 'part_name', 'stock_item', + 'stock_item_detail', 'quantity' ] diff --git a/InvenTree/build/templates/build/allocate.html b/InvenTree/build/templates/build/allocate.html index 0a746524b3..e4b527097a 100644 --- a/InvenTree/build/templates/build/allocate.html +++ b/InvenTree/build/templates/build/allocate.html @@ -18,16 +18,45 @@ {{ block.super }} + {% endblock %} {% block js_ready %} {{ block.super }} + function makeInnerTable(pk) { + var table = "
"; + return table; + } + $('#build-table').bootstrapTable({ sortable: true, + detailView: true, + detailFormatter: function(index, row, element) { + return makeInnerTable(row.pk); + }, + onExpandRow: function(index, row, $detail) { + $('#part-table-' + row.pk).bootstrapTable({ + columns: [ + { + field: 'stock_item_detail.location_name', + title: 'Location', + }, + { + field: 'stock_item_detail.quantity', + title: 'Available', + }, + { + field: 'quantity', + title: 'Allocated', + }, + ], + url: "/api/build/item/?build={{ build.pk }}&part=" + row.sub_part, + }); + }, columns: [ { - field: 'sub_part.name', + field: 'sub_part_detail.name', title: 'Part', }, { @@ -36,7 +65,7 @@ { field: 'quantity', title: 'Quantity', - } + }, ], }); diff --git a/InvenTree/static/script/inventree/part.js b/InvenTree/static/script/inventree/part.js index 9396e01772..053355113b 100644 --- a/InvenTree/static/script/inventree/part.js +++ b/InvenTree/static/script/inventree/part.js @@ -15,5 +15,5 @@ function getPartList(filters={}, options={}) { } function getBomList(filters={}, options={}) { - return inventreeGet('/api/part/bom/', filters, options); + return inventreeGet('/api/bom/', filters, options); } \ No newline at end of file diff --git a/InvenTree/stock/serializers.py b/InvenTree/stock/serializers.py index 0a863aa7f9..a6780faf15 100644 --- a/InvenTree/stock/serializers.py +++ b/InvenTree/stock/serializers.py @@ -47,6 +47,24 @@ class StockItemSerializerBrief(serializers.ModelSerializer): ] +class StockItemSerializerBrief(serializers.ModelSerializer): + """ Brief serializers for a StockItem """ + + location_name = serializers.CharField(source='location', read_only=True) + + class Meta: + model = StockItem + fields = [ + 'pk', + 'uuid', + 'part', + 'supplier_part', + 'location', + 'location_name', + 'quantity', + ] + + class StockItemSerializer(serializers.ModelSerializer): """ Serializer for a StockItem: From 8ec4101eddd1094fd0b58d852d130c258cfa2352 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 30 Apr 2019 00:16:20 +1000 Subject: [PATCH 09/37] Reduce items presented in BOM creation list - Don't allow selection of parts that are already in the BOM! --- InvenTree/part/views.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index af8559fb01..5428daa0ad 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -384,6 +384,35 @@ class BomItemCreate(AjaxCreateView): ajax_template_name = 'modal_form.html' ajax_form_title = 'Create BOM item' + def get_form(self): + """ Override get_form() method to reduce Part selection options. + + - Do not allow part to be added to its own BOM + - Remove any Part items that are already in the BOM + """ + + form = super(AjaxCreateView, self).get_form() + + part_id = form['part'].value() + + try: + part = Part.objects.get(id=part_id) + + # Don't allow selection of sub_part objects which are already added to the Bom! + query = form.fields['sub_part'].queryset + + # Don't allow a part to be added to its own BOM + query = query.exclude(id=part.id) + + # Eliminate any options that are already in the BOM! + query = query.exclude(id__in=[item.id for item in part.required_parts()]) + + form.fields['sub_part'].queryset = query + except Part.DoesNotExist: + pass + + return form + def get_initial(self): """ Provide initial data for the BomItem: From 395db4df383a3587404e053dd20a7d7d02bef976 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 30 Apr 2019 00:18:58 +1000 Subject: [PATCH 10/37] Pepe fixes --- InvenTree/build/serializers.py | 1 + InvenTree/part/views.py | 4 ++-- InvenTree/stock/serializers.py | 23 +++-------------------- 3 files changed, 6 insertions(+), 22 deletions(-) diff --git a/InvenTree/build/serializers.py b/InvenTree/build/serializers.py index 8ad09f616b..ed4d3b7b55 100644 --- a/InvenTree/build/serializers.py +++ b/InvenTree/build/serializers.py @@ -11,6 +11,7 @@ from stock.serializers import StockItemSerializerBrief from .models import Build, BuildItem + class BuildSerializer(InvenTreeModelSerializer): """ Serializes a Build object """ diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 5428daa0ad..a00e8f8e63 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -390,7 +390,7 @@ class BomItemCreate(AjaxCreateView): - Do not allow part to be added to its own BOM - Remove any Part items that are already in the BOM """ - + form = super(AjaxCreateView, self).get_form() part_id = form['part'].value() @@ -405,7 +405,7 @@ class BomItemCreate(AjaxCreateView): query = query.exclude(id=part.id) # Eliminate any options that are already in the BOM! - query = query.exclude(id__in=[item.id for item in part.required_parts()]) + query = query.exclude(id__in=[item.id for item in part.required_parts()]) form.fields['sub_part'].queryset = query except Part.DoesNotExist: diff --git a/InvenTree/stock/serializers.py b/InvenTree/stock/serializers.py index a6780faf15..269e3b4e78 100644 --- a/InvenTree/stock/serializers.py +++ b/InvenTree/stock/serializers.py @@ -29,35 +29,18 @@ class LocationBriefSerializer(serializers.ModelSerializer): class StockItemSerializerBrief(serializers.ModelSerializer): - """ - Provide a brief serializer for StockItem - """ - - url = serializers.CharField(source='get_absolute_url', read_only=True) + """ Brief serializers for a StockItem """ + location_name = serializers.CharField(source='location', read_only=True) part_name = serializers.CharField(source='part.name', read_only=True) - class Meta: - model = StockItem - fields = [ - 'pk', - 'uuid', - 'url', - 'part_name', - ] - - -class StockItemSerializerBrief(serializers.ModelSerializer): - """ Brief serializers for a StockItem """ - - location_name = serializers.CharField(source='location', read_only=True) - class Meta: model = StockItem fields = [ 'pk', 'uuid', 'part', + 'part_name', 'supplier_part', 'location', 'location_name', From a3d4c81939e3926784c0d854c660af88f550190a Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 30 Apr 2019 14:04:43 +1000 Subject: [PATCH 11/37] Display and edit build URL field --- InvenTree/build/forms.py | 1 + InvenTree/build/templates/build/detail.html | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/InvenTree/build/forms.py b/InvenTree/build/forms.py index 05b1c99034..8b99f627a6 100644 --- a/InvenTree/build/forms.py +++ b/InvenTree/build/forms.py @@ -21,6 +21,7 @@ class EditBuildForm(HelperForm): 'part', 'quantity', 'batch', + 'URL', 'notes', 'status', # 'completion_date', diff --git a/InvenTree/build/templates/build/detail.html b/InvenTree/build/templates/build/detail.html index 4f1c871a3f..9b239767c0 100644 --- a/InvenTree/build/templates/build/detail.html +++ b/InvenTree/build/templates/build/detail.html @@ -38,14 +38,19 @@ Quantity{{ build.quantity }} + + Status{% include "build_status.html" with build=build %} + {% if build.batch %} Batch{{ build.batch }} {% endif %} +{% if build.URL %} - Status{% include "build_status.html" with build=build %} + URL{{ build.URL }} +{% endif %} Created{{ build.creation_date }} From aa491e336d4e8e266d1adc55ea15160269e3f9c2 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 30 Apr 2019 14:42:48 +1000 Subject: [PATCH 12/37] Move javascript for Build allocation to build.js --- InvenTree/build/templates/build/allocate.html | 54 +-------- InvenTree/static/script/inventree/build.js | 106 ++++++++++++++++++ 2 files changed, 112 insertions(+), 48 deletions(-) create mode 100644 InvenTree/static/script/inventree/build.js diff --git a/InvenTree/build/templates/build/allocate.html b/InvenTree/build/templates/build/allocate.html index e4b527097a..365931a3ec 100644 --- a/InvenTree/build/templates/build/allocate.html +++ b/InvenTree/build/templates/build/allocate.html @@ -24,53 +24,11 @@ {% block js_ready %} {{ block.super }} - function makeInnerTable(pk) { - var table = "
"; - return table; - } - - $('#build-table').bootstrapTable({ - sortable: true, - detailView: true, - detailFormatter: function(index, row, element) { - return makeInnerTable(row.pk); - }, - onExpandRow: function(index, row, $detail) { - $('#part-table-' + row.pk).bootstrapTable({ - columns: [ - { - field: 'stock_item_detail.location_name', - title: 'Location', - }, - { - field: 'stock_item_detail.quantity', - title: 'Available', - }, - { - field: 'quantity', - title: 'Allocated', - }, - ], - url: "/api/build/item/?build={{ build.pk }}&part=" + row.sub_part, - }); - }, - columns: [ - { - field: 'sub_part_detail.name', - title: 'Part', - }, - { - title: 'Source', - }, - { - field: 'quantity', - title: 'Quantity', - }, - ], - }); - - getBomList({part: {{ build.part.id }}}).then(function(response) { - $("#build-table").bootstrapTable('load', response); - }); + makeBuildTable($('#build-table'), + { + build: {{ build.pk }}, + part: {{ build.part.pk }}, + } + ); {% endblock %} \ No newline at end of file diff --git a/InvenTree/static/script/inventree/build.js b/InvenTree/static/script/inventree/build.js new file mode 100644 index 0000000000..7e31fdae81 --- /dev/null +++ b/InvenTree/static/script/inventree/build.js @@ -0,0 +1,106 @@ +function makeBuildTable(table, options) { + /* Construct a table for allocation items to a build. + * Each row contains a sub_part for the BOM. + * Each row can be expended to allocate stock items against that part. + * + * options: + * build - ID of the build object + * part - ID of the part object for the build + * + */ + + table.bootstrapTable({ + sortable: false, + detailView: true, + detailFormatter: function(index, row, element) { + return makeAllocationTable({ + part: row.pk + }); + }, + onExpandRow: function(index, row, $detail) { + fillAllocationTable( + $("#part-table-" + row.pk), + row, + { + build: options.build + }, + ); + }, + columns: [ + { + field: 'sub_part_detail.name', + title: 'Part', + }, + { + field: 'quantity', + title: 'Required', + }, + { + field: 'allocated', + title: 'Allocated', + } + ], + }); + + getBomList( + { + part: options.part + }).then(function(response) { + table.bootstrapTable('load', response) + }); +} + + +function makeAllocationTable(options) { + /* Construct an allocation table for a single row + * in the Build table. + * Each allocation table is a 'detailView' of a parent Part row + * + * Options: + * part: Primary key of the part item + */ + + var table = ""; + table += "
"; + + return table; +} + +function fillAllocationTable(table, parent_row, options) { + /* Load data into an allocation table, + * and update the total stock allocation count in the parent row. + * + * table - the part allocation table + * row - parent row in the build allocation table + * + * options: + * build - pk of the Build object + */ + + table.bootstrapTable({ + columns: [ + { + field: 'stock_item_detail', + title: 'Stock Item', + formatter: function(value, row, index, field) { + return '' + value.quantity + ' x ' + value.part_name; + }, + }, + { + field: 'stock_item_detail.location_name', + title: 'Location', + }, + { + field: 'stock_item_detail.quantity', + title: 'Available', + }, + { + field: 'quantity', + title: 'Allocated' + }, + ], + url: "/api/build/item?build=" + options.build + "&part=" + parent_row.sub_part, + }); +} \ No newline at end of file From b192deb46593f5d8d531fe0943c9479e6b5a48de Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 30 Apr 2019 14:54:42 +1000 Subject: [PATCH 13/37] Calculate total allocation data per-row --- InvenTree/static/script/inventree/build.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/InvenTree/static/script/inventree/build.js b/InvenTree/static/script/inventree/build.js index 7e31fdae81..4a9ceae443 100644 --- a/InvenTree/static/script/inventree/build.js +++ b/InvenTree/static/script/inventree/build.js @@ -103,4 +103,15 @@ function fillAllocationTable(table, parent_row, options) { ], url: "/api/build/item?build=" + options.build + "&part=" + parent_row.sub_part, }); + + table.on('load-success.bs.table', function(data) { + var allocated = 0; + + var allocationData = table.bootstrapTable('getData'); + + // Calculate total allocation + for (var i = 0; i < allocationData.length; i++) { + allocated += allocationData[i].quantity; + } + }); } \ No newline at end of file From 1b32f9d6503ad95b7e00987e11ab99f39d1d0034 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 30 Apr 2019 15:14:59 +1000 Subject: [PATCH 14/37] Removed unused function --- InvenTree/static/script/inventree/tables.js | 47 +-------------------- 1 file changed, 2 insertions(+), 45 deletions(-) diff --git a/InvenTree/static/script/inventree/tables.js b/InvenTree/static/script/inventree/tables.js index 1ff1903326..979a7d5a21 100644 --- a/InvenTree/static/script/inventree/tables.js +++ b/InvenTree/static/script/inventree/tables.js @@ -2,10 +2,12 @@ function editButton(url, text='Edit') { return ""; } + function deleteButton(url, text='Delete') { return ""; } + function renderLink(text, url) { if (text === '' || url === '') { return text; @@ -14,51 +16,6 @@ function renderLink(text, url) { return '' + text + ''; } -function renderEditable(text, options) { - /* Wrap the text in an 'editable' link - * (using bootstrap-editable library) - * - * Can pass the following parameters in 'options': - * _type - parameter for data-type (default = 'text') - * _pk - parameter for data-pk (required) - * _title - title to show when editing - * _empty - placeholder text to show when field is empty - * _class - html class (default = 'editable-item') - * _id - id - * _value - Initial value of the editable (default = blank) - */ - - // Default values (if not supplied) - var _type = options._type || 'text'; - var _class = options._class || 'editable-item'; - - var html = "" + text + ""; - - return html; -} function enableButtons(elements, enabled) { for (let item of elements) { From 0208c6efe64bfa448ee07c48512d80bc7a7dadbc Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 30 Apr 2019 15:35:35 +1000 Subject: [PATCH 15/37] New Form and View to create a new BuildItem object - Allocates stock to a build --- InvenTree/build/forms.py | 14 ++++- InvenTree/build/templates/build/allocate.html | 1 + InvenTree/build/urls.py | 8 ++- InvenTree/build/views.py | 31 +++++++++- InvenTree/static/script/inventree/build.js | 61 ++++++++++++++++++- 5 files changed, 108 insertions(+), 7 deletions(-) diff --git a/InvenTree/build/forms.py b/InvenTree/build/forms.py index 8b99f627a6..cf30b3eaf9 100644 --- a/InvenTree/build/forms.py +++ b/InvenTree/build/forms.py @@ -7,7 +7,7 @@ from __future__ import unicode_literals from InvenTree.forms import HelperForm -from .models import Build +from .models import Build, BuildItem class EditBuildForm(HelperForm): @@ -26,3 +26,15 @@ class EditBuildForm(HelperForm): 'status', # 'completion_date', ] + + +class EditBuildItemForm(HelperForm): + """ Form for adding a new BuildItem to a Build """ + + class Meta: + model = BuildItem + fields = [ + 'build', + 'stock_item', + 'quantity', + ] diff --git a/InvenTree/build/templates/build/allocate.html b/InvenTree/build/templates/build/allocate.html index 365931a3ec..aca7a3ebba 100644 --- a/InvenTree/build/templates/build/allocate.html +++ b/InvenTree/build/templates/build/allocate.html @@ -28,6 +28,7 @@ { build: {{ build.pk }}, part: {{ build.part.pk }}, + new_item_url: "{% url 'build-item-create' %}", } ); diff --git a/InvenTree/build/urls.py b/InvenTree/build/urls.py index fea3b6596c..ef7a1369f8 100644 --- a/InvenTree/build/urls.py +++ b/InvenTree/build/urls.py @@ -6,6 +6,10 @@ from django.conf.urls import url, include from . import views +build_item_urls = [ + url('^new/', views.BuildItemCreate.as_view(), name='build-item-create'), +] + build_detail_urls = [ url(r'^edit/?', views.BuildUpdate.as_view(), name='build-edit'), url(r'^allocate/?', views.BuildAllocate.as_view(), name='build-allocate'), @@ -15,7 +19,9 @@ build_detail_urls = [ ] build_urls = [ - url(r'new/?', views.BuildCreate.as_view(), name='build-create'), + url(r'item/', include(build_item_urls)), + + url(r'new/', views.BuildCreate.as_view(), name='build-create'), url(r'^(?P\d+)/', include(build_detail_urls)), diff --git a/InvenTree/build/views.py b/InvenTree/build/views.py index dc7b3141c3..05044ec40d 100644 --- a/InvenTree/build/views.py +++ b/InvenTree/build/views.py @@ -10,8 +10,8 @@ from django.shortcuts import get_object_or_404 from django.views.generic import DetailView, ListView from part.models import Part -from .models import Build -from .forms import EditBuildForm +from .models import Build, BuildItem +from .forms import EditBuildForm, EditBuildItemForm from InvenTree.views import AjaxView, AjaxUpdateView, AjaxCreateView @@ -127,3 +127,30 @@ class BuildUpdate(AjaxUpdateView): return { 'info': 'Edited build', } + + +class BuildItemCreate(AjaxCreateView): + """ View for allocating a new part to a build """ + + model = BuildItem + form_class = EditBuildItemForm + ajax_template_name = 'modal_form.html' + ajax_form_title = 'Allocate new Part' + + 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') + + if build_id: + try: + initials['build'] = Build.objects.get(pk=build_id) + except Build.DoesNotExist: + pass + + return initials \ No newline at end of file diff --git a/InvenTree/static/script/inventree/build.js b/InvenTree/static/script/inventree/build.js index 4a9ceae443..c644aa4061 100644 --- a/InvenTree/static/script/inventree/build.js +++ b/InvenTree/static/script/inventree/build.js @@ -6,6 +6,7 @@ function makeBuildTable(table, options) { * options: * build - ID of the build object * part - ID of the part object for the build + * new_item_url - URL to create a new BuildItem * */ @@ -20,17 +21,28 @@ function makeBuildTable(table, options) { onExpandRow: function(index, row, $detail) { fillAllocationTable( $("#part-table-" + row.pk), + index, row, + table, { build: options.build }, ); }, columns: [ + { + field: 'pk', + title: 'ID', + visible: false, + }, { field: 'sub_part_detail.name', title: 'Part', }, + { + field: 'note', + title: 'Note', + }, { field: 'quantity', title: 'Required', @@ -38,7 +50,28 @@ function makeBuildTable(table, options) { { field: 'allocated', title: 'Allocated', - } + formatter: function(value, row, index, field) { + var html = ""; + + var url = options.new_item_url; + + url += "?build=" + options.build + "&part=" + row.sub_part; + + if (value) { + html = value; + } else { + html = "0"; + } + + html += "
"; + + html += ""; + + html += "
"; + + return html; + } + }, ], }); @@ -48,6 +81,16 @@ function makeBuildTable(table, options) { }).then(function(response) { table.bootstrapTable('load', response) }); + + // Button callbacks + table.on('click', '.new-item-button', function() { + var button = $(this); + + launchModalForm(button.attr('url'), { + success: function() { + } + }); + }); } @@ -68,12 +111,14 @@ function makeAllocationTable(options) { return table; } -function fillAllocationTable(table, parent_row, options) { +function fillAllocationTable(table, index, parent_row, parent_table, options) { /* Load data into an allocation table, * and update the total stock allocation count in the parent row. * * table - the part allocation table - * row - parent row in the build allocation table + * index - row index in the parent table + * parent_row - parent row data in the build allocation table + * parent_table - the parent build table * * options: * build - pk of the Build object @@ -113,5 +158,15 @@ function fillAllocationTable(table, parent_row, options) { for (var i = 0; i < allocationData.length; i++) { allocated += allocationData[i].quantity; } + + // Update the parent_row data + parent_row.quantity = allocated; + + /*parent_table.bootstrapTable('updateRow', + { + index: index, + row: parent_row + } + );*/ }); } \ No newline at end of file From db5521f02ec4d1014b582348c9b68a7a5089142b Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 30 Apr 2019 15:48:07 +1000 Subject: [PATCH 16/37] Limit available choicse in form - Only allow selection of StockItem which matches the correct part --- InvenTree/build/models.py | 5 +++++ InvenTree/build/views.py | 25 +++++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index cd4c71422f..e195130f03 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -132,6 +132,11 @@ class BuildItem(models.Model): quantity: Number of units allocated """ + def get_absolute_url(self): + # TODO - Fix! + return '/build/item/{pk}/'.format(pk=self.id) + #return reverse('build-detail', kwargs={'pk': self.id}) + class Meta: unique_together = [ ('build', 'stock_item'), diff --git a/InvenTree/build/views.py b/InvenTree/build/views.py index 05044ec40d..11d89c6d1a 100644 --- a/InvenTree/build/views.py +++ b/InvenTree/build/views.py @@ -8,6 +8,7 @@ from __future__ import unicode_literals from django.shortcuts import get_object_or_404 from django.views.generic import DetailView, ListView +from django.forms import HiddenInput from part.models import Part from .models import Build, BuildItem @@ -137,6 +138,30 @@ class BuildItemCreate(AjaxCreateView): ajax_template_name = 'modal_form.html' ajax_form_title = 'Allocate new Part' + 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 + if form['build'].value() 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: + part = Part.objects.get(pk=part_id) + query = form.fields['stock_item'].queryset + query = query.filter(part=part_id) + form.fields['stock_item'].queryset = query + except Part.DoesNotExist: + pass + + return form + def get_initial(self): """ Provide initial data for BomItem. Look for the folllowing in the GET data: From fbd5a2a2708d9586c7969e31443cefcf0c87b169 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 30 Apr 2019 16:35:40 +1000 Subject: [PATCH 17/37] Register BuildItem in admin interface --- InvenTree/build/admin.py | 34 +++++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/InvenTree/build/admin.py b/InvenTree/build/admin.py index 9662f6bf79..0ef5ccb75b 100644 --- a/InvenTree/build/admin.py +++ b/InvenTree/build/admin.py @@ -2,21 +2,33 @@ from __future__ import unicode_literals from django.contrib import admin +from import_export.admin import ImportExportModelAdmin -from .models import Build +from .models import Build, BuildItem -class BuildAdmin(admin.ModelAdmin): +class BuildAdmin(ImportExportModelAdmin): - list_display = ('part', - 'status', - 'batch', - 'quantity', - 'creation_date', - 'completion_date', - 'title', - 'notes', - ) + list_display = ( + 'part', + 'status', + 'batch', + 'quantity', + 'creation_date', + 'completion_date', + 'title', + 'notes', + ) + + +class BuildItemAdmin(admin.ModelAdmin): + + list_display = ( + 'build', + 'stock_item', + 'quantity' + ) admin.site.register(Build, BuildAdmin) +admin.site.register(BuildItem, BuildItemAdmin) From 6961d1ec685e93d071747aff79b9585cc667c39c Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 30 Apr 2019 16:35:55 +1000 Subject: [PATCH 18/37] Catch BuildItem errors in clean() --- InvenTree/build/models.py | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index e195130f03..56d6474af2 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -147,15 +147,25 @@ class BuildItem(models.Model): The following checks are performed: - StockItem.part must be in the BOM of the Part object referenced by Build + - Allocation quantity cannot exceed available quantity """ + + super(BuildItem, self).clean() - if self.stock_item.part not in self.build.part.required_parts(): - print('stock_item:', self.stock_item.part) - for p in self.build.part.bom_items.all(): - print('bom_part:', p) - raise ValidationError( - {'stock_item': _("Selected stock item not found in BOM for part '{p}'".format(p=str(self.build.part)))} - ) + errors = {} + + if self.stock_item is not None and self.stock_item.part is not None: + if self.stock_item.part not in self.build.part.required_parts(): + errors['stock_item'] = _("Selected stock item not found in BOM for part '{p}'".format(p=str(self.build.part))) + + if self.stock_item is not None and self.quantity > self.stock_item.quantity: + errors['quantity'] = _("Allocated quantity ({n}) must not exceed available quantity ({q})".format( + n=self.quantity, + q=self.stock_item.quantity + )) + + if len(errors) > 0: + raise ValidationError(errors) build = models.ForeignKey( Build, From 24ed6f393b618a7dffc0d903aa3c7a6cc342a282 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 30 Apr 2019 16:38:09 +1000 Subject: [PATCH 19/37] When adding new item allocations, filter the available stock items - Must match the appropriate part - Remove items that are already allocated --- InvenTree/build/views.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/InvenTree/build/views.py b/InvenTree/build/views.py index 11d89c6d1a..8e9fed0e90 100644 --- a/InvenTree/build/views.py +++ b/InvenTree/build/views.py @@ -12,6 +12,7 @@ from django.forms import HiddenInput from part.models import Part from .models import Build, BuildItem +from stock.models import StockItem from .forms import EditBuildForm, EditBuildItemForm from InvenTree.views import AjaxView, AjaxUpdateView, AjaxCreateView @@ -145,7 +146,9 @@ class BuildItemCreate(AjaxCreateView): # 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 - if form['build'].value() is not None: + 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 @@ -153,9 +156,17 @@ class BuildItemCreate(AjaxCreateView): if part_id: try: - part = Part.objects.get(pk=part_id) + 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: + # 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 except Part.DoesNotExist: pass From efbef251b63bc054e06b97dbe97a9c2012f44cb4 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 30 Apr 2019 18:51:05 +1000 Subject: [PATCH 20/37] Added views / forms to edit and delete BuildItem objects - Alter stock allocation - remove (unallocate) stock from a build --- .../templates/build/delete_build_item.html | 3 ++ InvenTree/build/urls.py | 6 +++ InvenTree/build/views.py | 34 +++++++++++- InvenTree/static/script/inventree/build.js | 52 ++++++++++++++----- 4 files changed, 81 insertions(+), 14 deletions(-) create mode 100644 InvenTree/build/templates/build/delete_build_item.html diff --git a/InvenTree/build/templates/build/delete_build_item.html b/InvenTree/build/templates/build/delete_build_item.html new file mode 100644 index 0000000000..23993feb95 --- /dev/null +++ b/InvenTree/build/templates/build/delete_build_item.html @@ -0,0 +1,3 @@ +Are you sure you want to unallocate these parts? +
+This will remove {{ item.quantity }} parts from build '{{ item.build.title }}'. \ No newline at end of file diff --git a/InvenTree/build/urls.py b/InvenTree/build/urls.py index ef7a1369f8..109616348d 100644 --- a/InvenTree/build/urls.py +++ b/InvenTree/build/urls.py @@ -6,7 +6,13 @@ from django.conf.urls import url, include from . import views +build_item_detail_urls = [ + url('^edit/?', views.BuildItemEdit.as_view(), name='build-item-edit'), + url('^delete/?', views.BuildItemDelete.as_view(), name='build-item-delete'), +] + build_item_urls = [ + url(r'^(?P\d+)/', include(build_item_detail_urls)), url('^new/', views.BuildItemCreate.as_view(), name='build-item-create'), ] diff --git a/InvenTree/build/views.py b/InvenTree/build/views.py index 8e9fed0e90..ab2c7d9484 100644 --- a/InvenTree/build/views.py +++ b/InvenTree/build/views.py @@ -15,7 +15,7 @@ from .models import Build, BuildItem from stock.models import StockItem from .forms import EditBuildForm, EditBuildItemForm -from InvenTree.views import AjaxView, AjaxUpdateView, AjaxCreateView +from InvenTree.views import AjaxView, AjaxUpdateView, AjaxCreateView, AjaxDeleteView class BuildIndex(ListView): @@ -131,6 +131,22 @@ class BuildUpdate(AjaxUpdateView): } +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 """ @@ -189,4 +205,18 @@ class BuildItemCreate(AjaxCreateView): except Build.DoesNotExist: pass - return initials \ No newline at end of file + return initials + + +class BuildItemEdit(AjaxUpdateView): + """ View to edit a BuildItem object """ + + model = BuildItem + ajax_template_name = 'modal_form.html' + form_class = EditBuildItemForm + ajax_form_title = 'Edit Stock Allocation' + + def get_data(self): + return { + 'info': 'Updated Build Item', + } \ No newline at end of file diff --git a/InvenTree/static/script/inventree/build.js b/InvenTree/static/script/inventree/build.js index c644aa4061..cc6c611f85 100644 --- a/InvenTree/static/script/inventree/build.js +++ b/InvenTree/static/script/inventree/build.js @@ -38,18 +38,13 @@ function makeBuildTable(table, options) { { field: 'sub_part_detail.name', title: 'Part', - }, - { - field: 'note', - title: 'Note', - }, - { - field: 'quantity', - title: 'Required', + formatter: function(value, row, index, field) { + return renderLink(value, row.sub_part_detail.url); + } }, { field: 'allocated', - title: 'Allocated', + title: 'Allocated to Build', formatter: function(value, row, index, field) { var html = ""; @@ -63,6 +58,9 @@ function makeBuildTable(table, options) { html = "0"; } + html += " of "; + html += row.quantity; + html += "
"; html += ""; @@ -140,15 +138,45 @@ function fillAllocationTable(table, index, parent_row, parent_table, options) { { field: 'stock_item_detail.quantity', title: 'Available', - }, + }, { field: 'quantity', - title: 'Allocated' - }, + title: 'Allocated', + formatter: function(value, row, index, field) { + + var html = value; + + var bEdit = ""; + var bDel = ""; + + html += "
" + bEdit + bDel + "
"; + + return html; + } + } ], url: "/api/build/item?build=" + options.build + "&part=" + parent_row.sub_part, }); + // Button callbacks for editing and deleting the allocations + table.on('click', '.item-edit-button', function() { + var button = $(this); + + launchModalForm(button.attr('url'), { + success: function() { + } + }); + }); + + table.on('click', '.item-del-button', function() { + var button = $(this); + + launchDeleteForm(button.attr('url'), { + success: function() { + } + }); + }); + table.on('load-success.bs.table', function(data) { var allocated = 0; From d061250a9aadd5c9d7f92d2aaf114d8c9fcb4786 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 30 Apr 2019 18:57:37 +1000 Subject: [PATCH 21/37] Changed delete form close button to "Cancel" --- InvenTree/templates/modals.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/templates/modals.html b/InvenTree/templates/modals.html index a84e4c7b44..9ba0335572 100644 --- a/InvenTree/templates/modals.html +++ b/InvenTree/templates/modals.html @@ -32,7 +32,7 @@
From 11d3975860d6accf0fa7a8b1d17cccdc7a84598d Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 30 Apr 2019 19:08:13 +1000 Subject: [PATCH 22/37] Build javascript improvements - Alter 'no records found' text - Reload allocation table on edit or delete --- InvenTree/static/script/inventree/build.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/InvenTree/static/script/inventree/build.js b/InvenTree/static/script/inventree/build.js index cc6c611f85..5235e6eec0 100644 --- a/InvenTree/static/script/inventree/build.js +++ b/InvenTree/static/script/inventree/build.js @@ -123,6 +123,7 @@ function fillAllocationTable(table, index, parent_row, parent_table, options) { */ table.bootstrapTable({ + formatNoMatches: function() { return 'No parts allocated for ' + parent_row.sub_part_detail.name; }, columns: [ { field: 'stock_item_detail', @@ -164,6 +165,7 @@ function fillAllocationTable(table, index, parent_row, parent_table, options) { launchModalForm(button.attr('url'), { success: function() { + table.bootstrapTable('refresh'); } }); }); @@ -173,6 +175,7 @@ function fillAllocationTable(table, index, parent_row, parent_table, options) { launchDeleteForm(button.attr('url'), { success: function() { + table.bootstrapTable('refresh'); } }); }); From d518739643b1230a0a9403960cd045513d380730 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 30 Apr 2019 19:17:54 +1000 Subject: [PATCH 23/37] PEP fixes --- InvenTree/build/models.py | 2 +- InvenTree/build/views.py | 3 +-- InvenTree/static/script/inventree/build.js | 11 +++++------ 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index 56d6474af2..487500e3d7 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -135,7 +135,7 @@ class BuildItem(models.Model): def get_absolute_url(self): # TODO - Fix! return '/build/item/{pk}/'.format(pk=self.id) - #return reverse('build-detail', kwargs={'pk': self.id}) + # return reverse('build-detail', kwargs={'pk': self.id}) class Meta: unique_together = [ diff --git a/InvenTree/build/views.py b/InvenTree/build/views.py index ab2c7d9484..07facc31f4 100644 --- a/InvenTree/build/views.py +++ b/InvenTree/build/views.py @@ -12,7 +12,6 @@ from django.forms import HiddenInput from part.models import Part from .models import Build, BuildItem -from stock.models import StockItem from .forms import EditBuildForm, EditBuildItemForm from InvenTree.views import AjaxView, AjaxUpdateView, AjaxCreateView, AjaxDeleteView @@ -219,4 +218,4 @@ class BuildItemEdit(AjaxUpdateView): def get_data(self): return { 'info': 'Updated Build Item', - } \ No newline at end of file + } diff --git a/InvenTree/static/script/inventree/build.js b/InvenTree/static/script/inventree/build.js index 5235e6eec0..a0b52d739a 100644 --- a/InvenTree/static/script/inventree/build.js +++ b/InvenTree/static/script/inventree/build.js @@ -1,4 +1,4 @@ -function makeBuildTable(table, options) { +function makeBuildTable(build_table, options) { /* Construct a table for allocation items to a build. * Each row contains a sub_part for the BOM. * Each row can be expended to allocate stock items against that part. @@ -23,7 +23,6 @@ function makeBuildTable(table, options) { $("#part-table-" + row.pk), index, row, - table, { build: options.build }, @@ -77,11 +76,11 @@ function makeBuildTable(table, options) { { part: options.part }).then(function(response) { - table.bootstrapTable('load', response) - }); + build_table.bootstrapTable('load', response); + }); // Button callbacks - table.on('click', '.new-item-button', function() { + build_table.on('click', '.new-item-button', function() { var button = $(this); launchModalForm(button.attr('url'), { @@ -109,7 +108,7 @@ function makeAllocationTable(options) { return table; } -function fillAllocationTable(table, index, parent_row, parent_table, options) { +function fillAllocationTable(table, index, parent_row, options) { /* Load data into an allocation table, * and update the total stock allocation count in the parent row. * From fb89574c42a37cf91002502e0b8295a2cd2faa59 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 30 Apr 2019 20:39:01 +1000 Subject: [PATCH 24/37] Skeleton code for Build cancel() and complete() functions - BuildComplete view --- InvenTree/build/models.py | 17 +++++++++ InvenTree/build/templates/build/allocate.html | 14 ++++++++ InvenTree/build/templates/build/complete.html | 1 + InvenTree/build/templates/build/detail.html | 2 +- InvenTree/build/urls.py | 1 + InvenTree/build/views.py | 35 +++++++++++++++++-- InvenTree/static/script/inventree/build.js | 11 +++--- 7 files changed, 72 insertions(+), 9 deletions(-) create mode 100644 InvenTree/build/templates/build/complete.html diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index 487500e3d7..3891cc512a 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -76,6 +76,23 @@ class Build(models.Model): notes = models.TextField(blank=True) """ Notes attached to each build output """ + def cancelBuild(self): + """ Mark the Build as CANCELLED + + - Delete any pending BuildItem objects (but do not remove items from stock) + """ + print("cancelled!") + + + def completeBuild(self): + """ Mark the Build as COMPLETE + + - Takes allocated items from stock + - Delete pending BuildItem objects + """ + + print("complete!!!!") + @property def required_parts(self): """ Returns a dict of parts required to build this part (BOM) """ diff --git a/InvenTree/build/templates/build/allocate.html b/InvenTree/build/templates/build/allocate.html index aca7a3ebba..ea33932d87 100644 --- a/InvenTree/build/templates/build/allocate.html +++ b/InvenTree/build/templates/build/allocate.html @@ -12,6 +12,10 @@
+
+ +
+ {% endblock %} {% block js_load %} @@ -32,4 +36,14 @@ } ); + $("#complete-build").on('click', function() { + launchModalForm( + "{% url 'build-complete' build.id %}", + { + reload: true, + submit_text: "Complete Build", + } + ); + }); + {% endblock %} \ No newline at end of file diff --git a/InvenTree/build/templates/build/complete.html b/InvenTree/build/templates/build/complete.html new file mode 100644 index 0000000000..6cf26c2a6c --- /dev/null +++ b/InvenTree/build/templates/build/complete.html @@ -0,0 +1 @@ +Mark as COMPLETE \ No newline at end of file diff --git a/InvenTree/build/templates/build/detail.html b/InvenTree/build/templates/build/detail.html index 9b239767c0..1960ed4ef5 100644 --- a/InvenTree/build/templates/build/detail.html +++ b/InvenTree/build/templates/build/detail.html @@ -118,7 +118,7 @@ launchModalForm("{% url 'build-cancel' build.id %}", { reload: true, - submit_text: "Cancel", + submit_text: "Cancel Build", }); }); {% endblock %} diff --git a/InvenTree/build/urls.py b/InvenTree/build/urls.py index 109616348d..162b9a613f 100644 --- a/InvenTree/build/urls.py +++ b/InvenTree/build/urls.py @@ -20,6 +20,7 @@ build_detail_urls = [ url(r'^edit/?', views.BuildUpdate.as_view(), name='build-edit'), url(r'^allocate/?', views.BuildAllocate.as_view(), name='build-allocate'), url(r'^cancel/?', views.BuildCancel.as_view(), name='build-cancel'), + url(r'^complete/?', views.BuildComplete.as_view(), name='build-complete'), url(r'^.*$', views.BuildDetail.as_view(), name='build-detail'), ] diff --git a/InvenTree/build/views.py b/InvenTree/build/views.py index 07facc31f4..31e25c017f 100644 --- a/InvenTree/build/views.py +++ b/InvenTree/build/views.py @@ -47,7 +47,7 @@ class BuildCancel(AjaxView): Provides a cancellation information dialog """ model = Build - template_name = 'build/cancel.html' + ajax_template_name = 'build/cancel.html' ajax_form_title = 'Cancel Build' context_object_name = 'build' fields = [] @@ -57,8 +57,7 @@ class BuildCancel(AjaxView): build = get_object_or_404(Build, pk=self.kwargs['pk']) - build.status = Build.CANCELLED - build.save() + build.cancelBuild() return self.renderJsonResponse(request, None) @@ -69,6 +68,36 @@ class BuildCancel(AjaxView): } +class BuildComplete(AjaxView): + """ 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 + ajax_template_name = "build/complete.html" + ajax_form_title = "Complete Build" + context_object_name = "build" + fields = [] + + def post(self, request, *args, **kwargs): + """ Handle POST request. Mark the build as COMPLETE """ + + build = get_object_or_404(Build, pk=self.kwargs['pk']) + + build.complete() + + return self.renderJsonResponse(request, None) + + 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 diff --git a/InvenTree/static/script/inventree/build.js b/InvenTree/static/script/inventree/build.js index a0b52d739a..f5c41e7662 100644 --- a/InvenTree/static/script/inventree/build.js +++ b/InvenTree/static/script/inventree/build.js @@ -10,9 +10,10 @@ function makeBuildTable(build_table, options) { * */ - table.bootstrapTable({ + build_table.bootstrapTable({ sortable: false, detailView: true, + showHeader: false, detailFormatter: function(index, row, element) { return makeAllocationTable({ part: row.pk @@ -45,16 +46,16 @@ function makeBuildTable(build_table, options) { field: 'allocated', title: 'Allocated to Build', formatter: function(value, row, index, field) { - var html = ""; + var html = "Allocated "; var url = options.new_item_url; url += "?build=" + options.build + "&part=" + row.sub_part; if (value) { - html = value; + html += value; } else { - html = "0"; + html += "0"; } html += " of "; @@ -62,7 +63,7 @@ function makeBuildTable(build_table, options) { html += "
"; - html += ""; + html += ""; html += "
"; From 7e7ac60a1a4c6e0222cadb115997bce18a8e0399 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 30 Apr 2019 20:55:28 +1000 Subject: [PATCH 25/37] Tweaks - Can't edit build status directly - Refresh BOM table on reload - Hide Company tabs that are not yet functional --- InvenTree/build/forms.py | 2 - InvenTree/company/templates/company/tabs.html | 4 +- InvenTree/part/templates/part/bom.html | 9 +++- InvenTree/static/script/inventree/bom.js | 46 +++++++++---------- 4 files changed, 34 insertions(+), 27 deletions(-) diff --git a/InvenTree/build/forms.py b/InvenTree/build/forms.py index cf30b3eaf9..87116c8646 100644 --- a/InvenTree/build/forms.py +++ b/InvenTree/build/forms.py @@ -23,8 +23,6 @@ class EditBuildForm(HelperForm): 'batch', 'URL', 'notes', - 'status', - # 'completion_date', ] diff --git a/InvenTree/company/templates/company/tabs.html b/InvenTree/company/templates/company/tabs.html index 4eaab521ba..4ad14f6b98 100644 --- a/InvenTree/company/templates/company/tabs.html +++ b/InvenTree/company/templates/company/tabs.html @@ -6,11 +6,13 @@ Supplier Parts {{ company.part_count }} + {% if 0 %} Purchase Orders {% endif %} - {% if company.is_customer %} + {% endif %} + {% if company.is_customer and 0 %} Sales Orders diff --git a/InvenTree/part/templates/part/bom.html b/InvenTree/part/templates/part/bom.html index 805f501dcd..c178ef455c 100644 --- a/InvenTree/part/templates/part/bom.html +++ b/InvenTree/part/templates/part/bom.html @@ -60,7 +60,14 @@ }); $("#bom-item-new").click(function () { - launchModalForm("{% url 'bom-item-create' %}?parent={{ part.id }}", {}); + launchModalForm( + "{% url 'bom-item-create' %}?parent={{ part.id }}", + { + success: function() { + $("#bom-table").bootstrapTable('refresh'); + } + } + ); }); {% else %} diff --git a/InvenTree/static/script/inventree/bom.js b/InvenTree/static/script/inventree/bom.js index cc533f5201..1107c5986a 100644 --- a/InvenTree/static/script/inventree/bom.js +++ b/InvenTree/static/script/inventree/bom.js @@ -114,22 +114,12 @@ function loadBomTable(table, options) { } ); - // Part notes - cols.push( - { - field: 'note', - title: 'Notes', - searchable: true, - sortable: false, - } - ); - if (options.editable) { cols.push({ formatter: function(value, row, index, field) { var bEdit = ""; var bDelt = ""; - + return "
" + bEdit + bDelt + "
"; } }); @@ -143,7 +133,7 @@ function loadBomTable(table, options) { sortable: true, formatter: function(value, row, index, field) { var text = ""; - + if (row.quantity < row.sub_part_detail.available_stock) { text = "" + value + ""; @@ -152,22 +142,32 @@ function loadBomTable(table, options) { { text = "" + value + ""; } - + return renderLink(text, row.sub_part.url + "stock/"); } } + ); + } + + // Part notes + cols.push( + { + field: 'note', + title: 'Notes', + searchable: true, + sortable: false, + } ); - } - // Configure the table (bootstrap-table) - - table.bootstrapTable({ - sortable: true, - search: true, - clickToSelect: true, - queryParams: function(p) { - return { - part: options.parent_id, + // Configure the table (bootstrap-table) + + table.bootstrapTable({ + sortable: true, + search: true, + clickToSelect: true, + queryParams: function(p) { + return { + part: options.parent_id, } }, columns: cols, From b6becbc5700c8f2ddadf3c6e69fb5eabe15b464c Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 1 May 2019 07:48:46 +1000 Subject: [PATCH 26/37] Rework build allocation - Each item renders as a collapsible panel with overview data at the top --- InvenTree/build/templates/build/allocate.html | 21 ++- .../templates/build/allocation_item.html | 24 +++ InvenTree/build/views.py | 14 ++ InvenTree/part/views.py | 1 - InvenTree/static/script/inventree/bom.js | 90 ++++----- InvenTree/static/script/inventree/build.js | 173 ++---------------- 6 files changed, 114 insertions(+), 209 deletions(-) create mode 100644 InvenTree/build/templates/build/allocation_item.html diff --git a/InvenTree/build/templates/build/allocate.html b/InvenTree/build/templates/build/allocate.html index ea33932d87..1b52a5a144 100644 --- a/InvenTree/build/templates/build/allocate.html +++ b/InvenTree/build/templates/build/allocate.html @@ -9,6 +9,10 @@
+{% for bom_item in bom_items.all %} +{% include "build/allocation_item.html" with item=bom_item build=build %} +{% endfor %} +
@@ -28,14 +32,18 @@ {% block js_ready %} {{ block.super }} - makeBuildTable($('#build-table'), - { - build: {{ build.pk }}, - part: {{ build.part.pk }}, - new_item_url: "{% url 'build-item-create' %}", - } + {% for bom_item in bom_items.all %} + + loadAllocationTable( + $("#allocate-table-id-{{ bom_item.sub_part.id }}"), + "{% url 'api-build-item-list' %}?build={{ build.id }}&part={{ bom_item.sub_part.id }}", + $("#new-item-{{ bom_item.sub_part.id }}") ); + {% endfor %} + + /* + $("#complete-build").on('click', function() { launchModalForm( "{% url 'build-complete' build.id %}", @@ -45,5 +53,6 @@ } ); }); + */ {% endblock %} \ No newline at end of file diff --git a/InvenTree/build/templates/build/allocation_item.html b/InvenTree/build/templates/build/allocation_item.html new file mode 100644 index 0000000000..271ae94e3a --- /dev/null +++ b/InvenTree/build/templates/build/allocation_item.html @@ -0,0 +1,24 @@ +
+
+
+
+ +
+
+ +
+
+
+
+
+
+ +
+
+
+
+
\ No newline at end of file diff --git a/InvenTree/build/views.py b/InvenTree/build/views.py index 31e25c017f..e23166322d 100644 --- a/InvenTree/build/views.py +++ b/InvenTree/build/views.py @@ -111,6 +111,20 @@ class BuildAllocate(DetailView): 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 """ diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index a00e8f8e63..5adeafe500 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -72,7 +72,6 @@ class PartCreate(AjaxCreateView): def get_category_id(self): return self.request.GET.get('category', None) - # If a category is provided in the URL, pass that to the page context def get_context_data(self, **kwargs): """ Provide extra context information for the form to display: diff --git a/InvenTree/static/script/inventree/bom.js b/InvenTree/static/script/inventree/bom.js index 1107c5986a..1b58b81a93 100644 --- a/InvenTree/static/script/inventree/bom.js +++ b/InvenTree/static/script/inventree/bom.js @@ -126,53 +126,53 @@ function loadBomTable(table, options) { } else { cols.push( - { - field: 'sub_part_detail.available_stock', - title: 'Available', - searchable: false, - sortable: true, - formatter: function(value, row, index, field) { - var text = ""; - - if (row.quantity < row.sub_part_detail.available_stock) - { - text = "" + value + ""; - } - else - { - text = "" + value + ""; - } - - return renderLink(text, row.sub_part.url + "stock/"); - } - } - ); - } - - // Part notes - cols.push( - { - field: 'note', - title: 'Notes', - searchable: true, - sortable: false, - } - ); - - // Configure the table (bootstrap-table) - - table.bootstrapTable({ + { + field: 'sub_part_detail.available_stock', + title: 'Available', + searchable: false, sortable: true, - search: true, - clickToSelect: true, - queryParams: function(p) { - return { - part: options.parent_id, + formatter: function(value, row, index, field) { + var text = ""; + + if (row.quantity < row.sub_part_detail.available_stock) + { + text = "" + value + ""; + } + else + { + text = "" + value + ""; + } + + return renderLink(text, row.sub_part.url + "stock/"); } - }, - columns: cols, - url: options.bom_url - }); + } + ); + } + + // Part notes + cols.push( + { + field: 'note', + title: 'Notes', + searchable: true, + sortable: false, + } + ); + + // Configure the table (bootstrap-table) + + table.bootstrapTable({ + sortable: true, + search: true, + clickToSelect: true, + queryParams: function(p) { + return { + part: options.parent_id, + } + }, + columns: cols, + url: options.bom_url +}); // In editing mode, attached editables to the appropriate table elements if (options.editable) { diff --git a/InvenTree/static/script/inventree/build.js b/InvenTree/static/script/inventree/build.js index f5c41e7662..1ffc9fe1e5 100644 --- a/InvenTree/static/script/inventree/build.js +++ b/InvenTree/static/script/inventree/build.js @@ -1,150 +1,21 @@ -function makeBuildTable(build_table, options) { - /* Construct a table for allocation items to a build. - * Each row contains a sub_part for the BOM. - * Each row can be expended to allocate stock items against that part. - * - * options: - * build - ID of the build object - * part - ID of the part object for the build - * new_item_url - URL to create a new BuildItem - * - */ - - build_table.bootstrapTable({ - sortable: false, - detailView: true, - showHeader: false, - detailFormatter: function(index, row, element) { - return makeAllocationTable({ - part: row.pk - }); - }, - onExpandRow: function(index, row, $detail) { - fillAllocationTable( - $("#part-table-" + row.pk), - index, - row, - { - build: options.build - }, - ); - }, - columns: [ - { - field: 'pk', - title: 'ID', - visible: false, - }, - { - field: 'sub_part_detail.name', - title: 'Part', - formatter: function(value, row, index, field) { - return renderLink(value, row.sub_part_detail.url); - } - }, - { - field: 'allocated', - title: 'Allocated to Build', - formatter: function(value, row, index, field) { - var html = "Allocated "; - - var url = options.new_item_url; - - url += "?build=" + options.build + "&part=" + row.sub_part; - - if (value) { - html += value; - } else { - html += "0"; - } - - html += " of "; - html += row.quantity; - - html += "
"; - - html += ""; - - html += "
"; - - return html; - } - }, - ], - }); - - getBomList( - { - part: options.part - }).then(function(response) { - build_table.bootstrapTable('load', response); - }); - - // Button callbacks - build_table.on('click', '.new-item-button', function() { - var button = $(this); - - launchModalForm(button.attr('url'), { - success: function() { - } - }); - }); -} - - -function makeAllocationTable(options) { - /* Construct an allocation table for a single row - * in the Build table. - * Each allocation table is a 'detailView' of a parent Part row - * - * Options: - * part: Primary key of the part item - */ - - var table = ""; - table += "
"; - - return table; -} - -function fillAllocationTable(table, index, parent_row, options) { - /* Load data into an allocation table, - * and update the total stock allocation count in the parent row. - * - * table - the part allocation table - * index - row index in the parent table - * parent_row - parent row data in the build allocation table - * parent_table - the parent build table - * - * options: - * build - pk of the Build object - */ - +function loadAllocationTable(table, url, button) { + + // Load the allocation table table.bootstrapTable({ - formatNoMatches: function() { return 'No parts allocated for ' + parent_row.sub_part_detail.name; }, + url: url, + sortable: false, columns: [ { field: 'stock_item_detail', title: 'Stock Item', formatter: function(value, row, index, field) { - return '' + value.quantity + ' x ' + value.part_name; - }, - }, - { - field: 'stock_item_detail.location_name', - title: 'Location', - }, - { - field: 'stock_item_detail.quantity', - title: 'Available', + return '' + value.quantity + ' x ' + value.part_name + ' @ ' + value.location_name; + } }, { field: 'quantity', title: 'Allocated', formatter: function(value, row, index, field) { - var html = value; var bEdit = ""; @@ -156,7 +27,15 @@ function fillAllocationTable(table, index, parent_row, options) { } } ], - url: "/api/build/item?build=" + options.build + "&part=" + parent_row.sub_part, + }); + + // Callback for 'new-item' button + button.click(function() { + launchModalForm(button.attr('url'), { + success: function() { + table.bootstrapTable('refresh'); + } + }); }); // Button callbacks for editing and deleting the allocations @@ -180,24 +59,4 @@ function fillAllocationTable(table, index, parent_row, options) { }); }); - table.on('load-success.bs.table', function(data) { - var allocated = 0; - - var allocationData = table.bootstrapTable('getData'); - - // Calculate total allocation - for (var i = 0; i < allocationData.length; i++) { - allocated += allocationData[i].quantity; - } - - // Update the parent_row data - parent_row.quantity = allocated; - - /*parent_table.bootstrapTable('updateRow', - { - index: index, - row: parent_row - } - );*/ - }); } \ No newline at end of file From fc42cca10e8606bd7c85ccc0687f1104af28cb31 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 1 May 2019 08:08:50 +1000 Subject: [PATCH 27/37] Automatically allocate parts when a Build is created - If there is only one StockItem to choose from, allocate parts from that StockItem --- InvenTree/build/models.py | 46 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index 3891cc512a..d4e01795a4 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -12,6 +12,8 @@ from django.urls import reverse from django.db import models from django.core.validators import MinValueValidator +from stock.models import StockItem + class Build(models.Model): """ A Build object organises the creation of new parts from the component parts. @@ -28,6 +30,50 @@ class Build(models.Model): notes: Text notes """ + def save(self, *args, **kwargs): + """ Called when the Build model is saved to the database. + + If this is a new Build, try to allocate StockItem objects automatically. + + - If there is only one StockItem for a Part, use that one. + - If there are multiple StockItem objects, leave blank and let the user decide + """ + + allocate_parts = False + + # If there is no PK yet, then this is the first time the Build has been saved + if not self.pk: + allocate_parts = True + + # Save this Build first + super(Build, self).save(*args, **kwargs) + + if allocate_parts: + for item in self.part.bom_items.all(): + part = item.sub_part + # Number of parts required for this build + q_req = item.quantity * self.quantity + + stock = StockItem.objects.filter(part=part) + + if len(stock) == 1: + stock_item = stock[0] + + # Are there any parts available? + if stock_item.quantity > 0: + # If there are not enough parts, reduce the amount we will take + if stock_item.quantity < q_req: + q_req = stock_item.quantity + + # Allocate parts to this build + build_item = BuildItem( + build=self, + stock_item=stock_item, + quantity=q_req) + + build_item.save() + + def __str__(self): return "Build {q} x {part}".format(q=self.quantity, part=str(self.part)) From 7dd960a29930f2180a585952518e2b5a73334de8 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 1 May 2019 08:19:57 +1000 Subject: [PATCH 28/37] PEP fixes --- InvenTree/build/models.py | 6 ++---- InvenTree/build/views.py | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index d4e01795a4..711ba3c6a8 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -31,7 +31,7 @@ class Build(models.Model): """ def save(self, *args, **kwargs): - """ Called when the Build model is saved to the database. + """ Called when the Build model is saved to the database. If this is a new Build, try to allocate StockItem objects automatically. @@ -67,13 +67,12 @@ class Build(models.Model): # Allocate parts to this build build_item = BuildItem( - build=self, + build=self, stock_item=stock_item, quantity=q_req) build_item.save() - def __str__(self): return "Build {q} x {part}".format(q=self.quantity, part=str(self.part)) @@ -129,7 +128,6 @@ class Build(models.Model): """ print("cancelled!") - def completeBuild(self): """ Mark the Build as COMPLETE diff --git a/InvenTree/build/views.py b/InvenTree/build/views.py index e23166322d..403f0aa4c5 100644 --- a/InvenTree/build/views.py +++ b/InvenTree/build/views.py @@ -69,7 +69,7 @@ class BuildCancel(AjaxView): class BuildComplete(AjaxView): - """ View to mark a build as Complete. + """ View to mark a build as Complete. - Notifies the user of which parts will be removed from stock. - Removes allocated items from stock From 7c11d917de3249601c6475365a94311cf7a00739 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 1 May 2019 09:40:49 +1000 Subject: [PATCH 29/37] Bug fixes - Part creation form was setting a field as HiddenInput() rather than its widget - Added 'comment' file to FileAttachment model --- InvenTree/part/admin.py | 2 +- .../migrations/0014_partattachment_comment.py | 18 ++++++++++++++++++ InvenTree/part/models.py | 7 ++++++- InvenTree/part/views.py | 2 +- 4 files changed, 26 insertions(+), 3 deletions(-) create mode 100644 InvenTree/part/migrations/0014_partattachment_comment.py diff --git a/InvenTree/part/admin.py b/InvenTree/part/admin.py index 4e63bf65e0..56bf290739 100644 --- a/InvenTree/part/admin.py +++ b/InvenTree/part/admin.py @@ -19,7 +19,7 @@ class PartCategoryAdmin(ImportExportModelAdmin): class PartAttachmentAdmin(admin.ModelAdmin): - list_display = ('part', 'attachment') + list_display = ('part', 'attachment', 'comment') class BomItemAdmin(ImportExportModelAdmin): diff --git a/InvenTree/part/migrations/0014_partattachment_comment.py b/InvenTree/part/migrations/0014_partattachment_comment.py new file mode 100644 index 0000000000..a51f588a59 --- /dev/null +++ b/InvenTree/part/migrations/0014_partattachment_comment.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2 on 2019-04-30 23:34 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('part', '0013_auto_20190429_2229'), + ] + + operations = [ + migrations.AddField( + model_name='partattachment', + name='comment', + field=models.CharField(blank=True, help_text='Attachment description', max_length=100), + ), + ] diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 635ad9fa97..19682929cf 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -366,6 +366,8 @@ class PartAttachment(models.Model): attachment = models.FileField(upload_to=attach_file, null=True, blank=True) + comment = models.CharField(max_length=100, blank=True, help_text="Attachment description") + @property def basename(self): return os.path.basename(self.attachment.name) @@ -405,8 +407,11 @@ class BomItem(models.Model): - A part cannot refer to a part which refers to it """ + if self.part is None or self.sub_part is None: + # Field validation will catch these None values + pass # A part cannot refer to itself in its BOM - if self.part == self.sub_part: + elif self.part == self.sub_part: raise ValidationError({'sub_part': _('Part cannot be added to its own Bill of Materials')}) # Test for simple recursion diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 6e3eed583a..7ea8e5efbf 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -98,7 +98,7 @@ class PartCreate(AjaxCreateView): form = super(AjaxCreateView, self).get_form() # Hide the default_supplier field (there are no matching supplier parts yet!) - form.fields['default_supplier'] = HiddenInput() + form.fields['default_supplier'].widget = HiddenInput() return form From ad1d75c2595e9feddb9ba7c63ae2d0511b1b5780 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 1 May 2019 18:54:54 +1000 Subject: [PATCH 30/37] Improve table formatting when no allocation items found --- InvenTree/build/templates/build/allocate.html | 1 + InvenTree/static/css/inventree.css | 4 ++++ InvenTree/static/script/inventree/build.js | 3 ++- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/InvenTree/build/templates/build/allocate.html b/InvenTree/build/templates/build/allocate.html index 1b52a5a144..92d0e9bf7b 100644 --- a/InvenTree/build/templates/build/allocate.html +++ b/InvenTree/build/templates/build/allocate.html @@ -36,6 +36,7 @@ loadAllocationTable( $("#allocate-table-id-{{ bom_item.sub_part.id }}"), + "{{ bom_item.sub_part.name }}", "{% url 'api-build-item-list' %}?build={{ build.id }}&part={{ bom_item.sub_part.id }}", $("#new-item-{{ bom_item.sub_part.id }}") ); diff --git a/InvenTree/static/css/inventree.css b/InvenTree/static/css/inventree.css index 14b021adc7..f16ce3f038 100644 --- a/InvenTree/static/css/inventree.css +++ b/InvenTree/static/css/inventree.css @@ -122,6 +122,10 @@ margin-right: 2px; } +.panel-group { + margin-bottom: 5px; +} + .float-right { float: right; } diff --git a/InvenTree/static/script/inventree/build.js b/InvenTree/static/script/inventree/build.js index 1ffc9fe1e5..92fbf430f7 100644 --- a/InvenTree/static/script/inventree/build.js +++ b/InvenTree/static/script/inventree/build.js @@ -1,9 +1,10 @@ -function loadAllocationTable(table, url, button) { +function loadAllocationTable(table, part, url, button) { // Load the allocation table table.bootstrapTable({ url: url, sortable: false, + formatNoMatches: function() { return 'No parts allocated for ' + part; }, columns: [ { field: 'stock_item_detail', From 98109bb1a102dfff3aba560b7b99e3de1b454189 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 1 May 2019 22:12:09 +1000 Subject: [PATCH 31/37] Make build allocation much more intuiitive - Display current allocation + total allocation requirement - Color code results - Required custom 'multiply' template tag --- InvenTree/build/templates/build/allocate.html | 3 ++ .../templates/build/allocation_item.html | 16 +++++++++- InvenTree/build/templatetags/__init__.py | 0 .../build/templatetags/inventree_extras.py | 12 +++++++ InvenTree/static/css/inventree.css | 14 +++++++- InvenTree/static/script/inventree/build.js | 32 ++++++++++++++++++- 6 files changed, 74 insertions(+), 3 deletions(-) create mode 100644 InvenTree/build/templatetags/__init__.py create mode 100644 InvenTree/build/templatetags/inventree_extras.py diff --git a/InvenTree/build/templates/build/allocate.html b/InvenTree/build/templates/build/allocate.html index 92d0e9bf7b..e9438d898f 100644 --- a/InvenTree/build/templates/build/allocate.html +++ b/InvenTree/build/templates/build/allocate.html @@ -1,5 +1,6 @@ {% extends "base.html" %} {% load static %} +{% load inventree_extras %} {% block content %} @@ -36,8 +37,10 @@ loadAllocationTable( $("#allocate-table-id-{{ bom_item.sub_part.id }}"), + {{ bom_item.sub_part.id }}, "{{ bom_item.sub_part.name }}", "{% url 'api-build-item-list' %}?build={{ build.id }}&part={{ bom_item.sub_part.id }}", + {% multiply build.quantity bom_item.quantity %}, $("#new-item-{{ bom_item.sub_part.id }}") ); diff --git a/InvenTree/build/templates/build/allocation_item.html b/InvenTree/build/templates/build/allocation_item.html index 271ae94e3a..9982457aca 100644 --- a/InvenTree/build/templates/build/allocation_item.html +++ b/InvenTree/build/templates/build/allocation_item.html @@ -1,3 +1,5 @@ +{% load inventree_extras %} +
@@ -7,7 +9,19 @@ {{ item.sub_part.name }}
-
+
+ Required: +
+
+ {% multiply build.quantity item.quantity %} +
+
+ Allocated: +
+
+ 0 +
+
diff --git a/InvenTree/build/templatetags/__init__.py b/InvenTree/build/templatetags/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/InvenTree/build/templatetags/inventree_extras.py b/InvenTree/build/templatetags/inventree_extras.py new file mode 100644 index 0000000000..cb8cd0d235 --- /dev/null +++ b/InvenTree/build/templatetags/inventree_extras.py @@ -0,0 +1,12 @@ +""" This module provides template tags for extra functionality +over and above the built-in Django tags. +""" + +from django import template + +register = template.Library() + + +@register.simple_tag() +def multiply(x, y, *args, **kwargs): + return x * y diff --git a/InvenTree/static/css/inventree.css b/InvenTree/static/css/inventree.css index f16ce3f038..0a1c34036b 100644 --- a/InvenTree/static/css/inventree.css +++ b/InvenTree/static/css/inventree.css @@ -139,4 +139,16 @@ top: 50%; left: 50%; transform: translate(-50%, -50%); -} \ No newline at end of file +} + +.part-allocation-pass { + background: #dbf0db; +} + +.part-allocation-underallocated { + background: #f0dbdb; +} + +.part-allocation-overallocated { + background: #ccf5ff; +} diff --git a/InvenTree/static/script/inventree/build.js b/InvenTree/static/script/inventree/build.js index 92fbf430f7..fd20af0743 100644 --- a/InvenTree/static/script/inventree/build.js +++ b/InvenTree/static/script/inventree/build.js @@ -1,4 +1,21 @@ -function loadAllocationTable(table, part, url, button) { +function updateAllocationTotal(id, count, required) { + + + $('#allocation-total-'+id).html(count); + + var el = $("#allocation-panel-" + id); + el.removeClass('part-allocation-pass part-allocation-underallocated part-allocation-overallocated'); + + if (count < required) { + el.addClass('part-allocation-underallocated'); + } else if (count > required) { + el.addClass('part-allocation-overallocated'); + } else { + el.addClass('part-allocation-pass'); + } +} + +function loadAllocationTable(table, part_id, part, url, required, button) { // Load the allocation table table.bootstrapTable({ @@ -39,6 +56,19 @@ function loadAllocationTable(table, part, url, button) { }); }); + table.on('load-success.bs.table', function(data) { + // Extract table data + var results = table.bootstrapTable('getData'); + + var count = 0; + + for (var i = 0; i < results.length; i++) { + count += results[i].quantity; + } + + updateAllocationTotal(part_id, count, required); + }); + // Button callbacks for editing and deleting the allocations table.on('click', '.item-edit-button', function() { var button = $(this); From 7f63a94f15fe32fb3dea829be6dab585c4ccba18 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 1 May 2019 22:21:13 +1000 Subject: [PATCH 32/37] Cancel build function now works --- InvenTree/build/models.py | 16 ++++++++++++---- InvenTree/build/serializers.py | 7 +++++++ InvenTree/build/views.py | 2 +- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index 711ba3c6a8..d13919c24e 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -9,7 +9,7 @@ from django.utils.translation import ugettext as _ from django.core.exceptions import ValidationError from django.urls import reverse -from django.db import models +from django.db import models, transaction from django.core.validators import MinValueValidator from stock.models import StockItem @@ -94,13 +94,11 @@ class Build(models.Model): # Build status codes PENDING = 10 # Build is pending / active - HOLDING = 20 # Build is currently being held CANCELLED = 30 # Build was cancelled COMPLETE = 40 # Build is complete #: Build status codes BUILD_STATUS_CODES = {PENDING: _("Pending"), - HOLDING: _("Holding"), CANCELLED: _("Cancelled"), COMPLETE: _("Complete"), } @@ -121,13 +119,23 @@ class Build(models.Model): notes = models.TextField(blank=True) """ Notes attached to each build output """ + @transaction.atomic def cancelBuild(self): """ Mark the Build as CANCELLED - Delete any pending BuildItem objects (but do not remove items from stock) + - Set build status to CANCELLED + - Save the Build object """ + + for item in BuildItem.objects.filter(build=self.id): + item.delete() + + self.status = self.CANCELLED + self.save() print("cancelled!") + @transaction.atomic def completeBuild(self): """ Mark the Build as COMPLETE @@ -172,7 +180,6 @@ class Build(models.Model): return self.status in [ self.PENDING, - self.HOLDING ] @property @@ -232,6 +239,7 @@ class BuildItem(models.Model): Build, on_delete=models.CASCADE, related_name='allocated_stock', + help_text='Build to allocate parts' ) stock_item = models.ForeignKey( diff --git a/InvenTree/build/serializers.py b/InvenTree/build/serializers.py index ed4d3b7b55..947f3f6dc1 100644 --- a/InvenTree/build/serializers.py +++ b/InvenTree/build/serializers.py @@ -32,6 +32,13 @@ class BuildSerializer(InvenTreeModelSerializer): 'status_text', 'notes'] + read_only_fields = [ + 'status', + 'creation_date', + 'completion_data', + 'status_text', + ] + class BuildItemSerializer(InvenTreeModelSerializer): """ Serializes a BuildItem object """ diff --git a/InvenTree/build/views.py b/InvenTree/build/views.py index 403f0aa4c5..ebc5372596 100644 --- a/InvenTree/build/views.py +++ b/InvenTree/build/views.py @@ -64,7 +64,7 @@ class BuildCancel(AjaxView): def get_data(self): """ Provide JSON context data. """ return { - 'info': 'Build was cancelled' + 'danger': 'Build was cancelled' } From fbb9a708f560f841fab50b811ea1b5a32cf20521 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 1 May 2019 22:45:41 +1000 Subject: [PATCH 33/37] More intelligent passing of context data in AjaxView - If context is not explicitly supplied, use default get_context_data() function --- InvenTree/InvenTree/views.py | 5 ++++- InvenTree/build/serializers.py | 2 ++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py index 26303a6549..ca291632b8 100644 --- a/InvenTree/InvenTree/views.py +++ b/InvenTree/InvenTree/views.py @@ -93,7 +93,7 @@ class AjaxMixin(object): """ return {} - def renderJsonResponse(self, request, form=None, data={}, context={}): + def renderJsonResponse(self, request, form=None, data={}, context=None): """ Render a JSON response based on specific class context. Args: @@ -106,6 +106,9 @@ class AjaxMixin(object): JSON response object """ + if not context: + context = self.get_context_data() + if form: context['form'] = form diff --git a/InvenTree/build/serializers.py b/InvenTree/build/serializers.py index 947f3f6dc1..0f5696e816 100644 --- a/InvenTree/build/serializers.py +++ b/InvenTree/build/serializers.py @@ -28,6 +28,7 @@ class BuildSerializer(InvenTreeModelSerializer): 'completion_date', 'part', 'quantity', + 'is_active', 'status', 'status_text', 'notes'] @@ -37,6 +38,7 @@ class BuildSerializer(InvenTreeModelSerializer): 'creation_date', 'completion_data', 'status_text', + 'is_active', ] From 29f7b1a32bd3ce8e9c38f283f7a80a9be3e6ef92 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 1 May 2019 22:52:33 +1000 Subject: [PATCH 34/37] Revert "More intelligent passing of context data in AjaxView" This reverts commit fbb9a708f560f841fab50b811ea1b5a32cf20521. --- InvenTree/InvenTree/views.py | 5 +---- InvenTree/build/serializers.py | 2 -- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py index ca291632b8..26303a6549 100644 --- a/InvenTree/InvenTree/views.py +++ b/InvenTree/InvenTree/views.py @@ -93,7 +93,7 @@ class AjaxMixin(object): """ return {} - def renderJsonResponse(self, request, form=None, data={}, context=None): + def renderJsonResponse(self, request, form=None, data={}, context={}): """ Render a JSON response based on specific class context. Args: @@ -106,9 +106,6 @@ class AjaxMixin(object): JSON response object """ - if not context: - context = self.get_context_data() - if form: context['form'] = form diff --git a/InvenTree/build/serializers.py b/InvenTree/build/serializers.py index 0f5696e816..947f3f6dc1 100644 --- a/InvenTree/build/serializers.py +++ b/InvenTree/build/serializers.py @@ -28,7 +28,6 @@ class BuildSerializer(InvenTreeModelSerializer): 'completion_date', 'part', 'quantity', - 'is_active', 'status', 'status_text', 'notes'] @@ -38,7 +37,6 @@ class BuildSerializer(InvenTreeModelSerializer): 'creation_date', 'completion_data', 'status_text', - 'is_active', ] From 905d78e25ce95bef9670f3a2fd65d74a23bd6a05 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 2 May 2019 00:04:39 +1000 Subject: [PATCH 35/37] 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: +
    +
  • Remove allocated parts from stock
  • +
  • Create {{ build.quantity }} new items in the selected location
  • +
+{% 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 %}