From fdcef7b69980d6ff8fa8e207703473900821515a Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 20 Oct 2020 20:37:57 +1100 Subject: [PATCH 01/75] Add "install_into" field for BuildItem - Points to which output stock item it will be going into --- .../migrations/0021_auto_20201020_0908.py | 25 +++++++++++++++++++ InvenTree/build/models.py | 13 +++++++++- InvenTree/stock/api.py | 6 +++++ 3 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 InvenTree/build/migrations/0021_auto_20201020_0908.py diff --git a/InvenTree/build/migrations/0021_auto_20201020_0908.py b/InvenTree/build/migrations/0021_auto_20201020_0908.py new file mode 100644 index 0000000000..5fa450f5c7 --- /dev/null +++ b/InvenTree/build/migrations/0021_auto_20201020_0908.py @@ -0,0 +1,25 @@ +# Generated by Django 3.0.7 on 2020-10-20 09:08 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('stock', '0052_stockitem_is_building'), + ('build', '0020_auto_20201019_1325'), + ] + + operations = [ + migrations.AddField( + model_name='builditem', + name='install_into', + field=models.ForeignKey(blank=True, help_text='Destination stock item', limit_choices_to={'is_building': True}, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='items_to_install', to='stock.StockItem'), + ), + migrations.AlterField( + model_name='builditem', + name='stock_item', + field=models.ForeignKey(help_text='Source stock item', limit_choices_to={'belongs_to': None, 'build_order': None, 'sales_order': None}, on_delete=django.db.models.deletion.CASCADE, related_name='allocations', to='stock.StockItem'), + ), + ] diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index 877affd144..3afc7d5d10 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -568,7 +568,7 @@ class BuildItem(models.Model): 'stock.StockItem', on_delete=models.CASCADE, related_name='allocations', - help_text=_('Stock Item to allocate to build'), + help_text=_('Source stock item'), limit_choices_to={ 'build_order': None, 'sales_order': None, @@ -583,3 +583,14 @@ class BuildItem(models.Model): validators=[MinValueValidator(0)], help_text=_('Stock quantity to allocate to build') ) + + install_into = models.ForeignKey( + 'stock.StockItem', + on_delete=models.SET_NULL, + blank=True, null=True, + related_name='items_to_install', + help_text=_('Destination stock item'), + limit_choices_to={ + 'is_building': True, + } + ) diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index dabeaa20ea..5d71d00a0c 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -493,6 +493,12 @@ class StockList(generics.ListCreateAPIView): if build_order: queryset = queryset.filter(build_order=build_order) + is_building = params.get('is_building', None) + + if is_building: + is_building = str2bool(is_building) + queryset = queryset.filter(is_building=is_building) + sales_order = params.get('sales_order', None) if sales_order: From 28460b30238512eafd9bdc6b4fb6c4b0601b1f73 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 20 Oct 2020 20:42:29 +1100 Subject: [PATCH 02/75] Validate that the BuildItem quantity is an integer --- InvenTree/build/models.py | 17 ++++++++--------- InvenTree/stock/serializers.py | 1 + 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index 3afc7d5d10..f4a66e6b6f 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -32,7 +32,7 @@ from part import models as PartModels class Build(MPTTModel): - """ A Build object organises the creation of new parts from the component parts. + """ A Build object organises the creation of new StockItem objects from other existing StockItem objects. Attributes: part: The part to be built (from component BOM items) @@ -70,15 +70,14 @@ class Build(MPTTModel): super().clean() - try: - if self.part.trackable: - if not self.quantity == int(self.quantity): - raise ValidationError({ - 'quantity': _("Build quantity must be integer value for trackable parts") - }) - except PartModels.Part.DoesNotExist: - pass + # Build quantity must be an integer + # Maybe in the future this will be adjusted? + if not self.quantity == int(self.quantity): + raise ValidationError({ + 'quantity': _("Build quantity must be integer value for trackable parts") + }) + reference = models.CharField( unique=True, max_length=64, diff --git a/InvenTree/stock/serializers.py b/InvenTree/stock/serializers.py index 4a9b5a886b..d257f12f97 100644 --- a/InvenTree/stock/serializers.py +++ b/InvenTree/stock/serializers.py @@ -157,6 +157,7 @@ class StockItemSerializer(InvenTreeModelSerializer): 'customer', 'build_order', 'in_stock', + 'is_building', 'link', 'location', 'location_detail', From ac79e131bc7685643818e98b9346e29364b5bb75 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 20 Oct 2020 21:01:51 +1100 Subject: [PATCH 03/75] Add "destination" field to BuildOrder --- InvenTree/InvenTree/views.py | 4 +-- InvenTree/build/forms.py | 5 ++-- .../migrations/0022_auto_20201020_0953.py | 26 +++++++++++++++++++ InvenTree/build/models.py | 11 +++++++- InvenTree/build/templates/build/index.html | 2 +- InvenTree/build/views.py | 13 ++++++++-- InvenTree/part/templates/part/detail.html | 2 +- 7 files changed, 54 insertions(+), 9 deletions(-) create mode 100644 InvenTree/build/migrations/0022_auto_20201020_0953.py diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py index bb7c1e6f5d..eb168ad547 100644 --- a/InvenTree/InvenTree/views.py +++ b/InvenTree/InvenTree/views.py @@ -322,7 +322,7 @@ class AjaxCreateView(AjaxMixin, CreateView): """ pass - def post_save(self, **kwargs): + def post_save(self, new_object, **kwargs): """ Hook for doing something with the created object after it is saved """ @@ -356,7 +356,7 @@ class AjaxCreateView(AjaxMixin, CreateView): self.pre_save() self.object = self.form.save() - self.post_save() + self.post_save(self.object) # Return the PK of the newly-created object data['pk'] = self.object.pk diff --git a/InvenTree/build/forms.py b/InvenTree/build/forms.py index 74227adb9c..e39d0ba9cf 100644 --- a/InvenTree/build/forms.py +++ b/InvenTree/build/forms.py @@ -35,10 +35,11 @@ class EditBuildForm(HelperForm): 'title', 'part', 'quantity', + 'batch', + 'take_from', + 'destination', 'parent', 'sales_order', - 'take_from', - 'batch', 'link', ] diff --git a/InvenTree/build/migrations/0022_auto_20201020_0953.py b/InvenTree/build/migrations/0022_auto_20201020_0953.py new file mode 100644 index 0000000000..62a82ce7fd --- /dev/null +++ b/InvenTree/build/migrations/0022_auto_20201020_0953.py @@ -0,0 +1,26 @@ +# Generated by Django 3.0.7 on 2020-10-20 09:53 + +from django.db import migrations, models +import django.db.models.deletion +import mptt.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('stock', '0052_stockitem_is_building'), + ('build', '0021_auto_20201020_0908'), + ] + + operations = [ + migrations.AddField( + model_name='build', + name='destination', + field=models.ForeignKey(blank=True, help_text='Select location where the completed items will be stored', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='incoming_builds', to='stock.StockLocation', verbose_name='Destination Location'), + ), + migrations.AlterField( + model_name='build', + name='parent', + field=mptt.fields.TreeForeignKey(blank=True, help_text='BuildOrder to which this build is allocated', null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='children', to='build.Build', verbose_name='Parent Build'), + ), + ] diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index f4a66e6b6f..ca4bb586fe 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -102,7 +102,7 @@ class Build(MPTTModel): blank=True, null=True, related_name='children', verbose_name=_('Parent Build'), - help_text=_('Parent build to which this build is allocated'), + help_text=_('BuildOrder to which this build is allocated'), ) part = models.ForeignKey( @@ -137,6 +137,15 @@ class Build(MPTTModel): help_text=_('Select location to take stock from for this build (leave blank to take from any stock location)') ) + destination = models.ForeignKey( + 'stock.StockLocation', + verbose_name=_('Destination Location'), + on_delete=models.SET_NULL, + related_name='incoming_builds', + null=True, blank=True, + help_text=_('Select location where the completed items will be stored'), + ) + quantity = models.PositiveIntegerField( verbose_name=_('Build Quantity'), default=1, diff --git a/InvenTree/build/templates/build/index.html b/InvenTree/build/templates/build/index.html index d72807f9f1..c15b2c2d33 100644 --- a/InvenTree/build/templates/build/index.html +++ b/InvenTree/build/templates/build/index.html @@ -43,7 +43,7 @@ InvenTree | {% trans "Build Orders" %} launchModalForm( "{% url 'build-create' %}", { - follow: true + follow: true, } ); }); diff --git a/InvenTree/build/views.py b/InvenTree/build/views.py index 1296e42fae..c1431b5730 100644 --- a/InvenTree/build/views.py +++ b/InvenTree/build/views.py @@ -412,8 +412,17 @@ class BuildCreate(AjaxCreateView): initials = super(BuildCreate, self).get_initial().copy() - # User has provided a Part ID - initials['part'] = self.request.GET.get('part', None) + part = self.request.GET.get('part', None) + + if part: + + try: + part = Part.objects.get(pk=part) + # User has provided a Part ID + initials['part'] = part + initials['destination'] = part.get_default_location() + except (ValueError, Part.DoesNotExist): + pass initials['reference'] = Build.getNextBuildNumber() diff --git a/InvenTree/part/templates/part/detail.html b/InvenTree/part/templates/part/detail.html index 2714347414..facb8003a6 100644 --- a/InvenTree/part/templates/part/detail.html +++ b/InvenTree/part/templates/part/detail.html @@ -41,7 +41,7 @@ {% if part.getLatestSerialNumber %} {{ part.getLatestSerialNumber }} {% else %} - {% trans "No serial numbers recorded" %} + {% trans "No serial numbers recorded" %} {% endif %} From 2df0f03a9a0ef58037b96fac210d8ba0b30a2196 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 20 Oct 2020 21:10:36 +1100 Subject: [PATCH 04/75] Change "ALLOCATED" to "PRODUCTION" --- InvenTree/InvenTree/status_codes.py | 8 ++++---- .../migrations/0023_auto_20201020_1009.py | 19 +++++++++++++++++++ InvenTree/build/views.py | 9 +++++++++ 3 files changed, 32 insertions(+), 4 deletions(-) create mode 100644 InvenTree/build/migrations/0023_auto_20201020_1009.py diff --git a/InvenTree/InvenTree/status_codes.py b/InvenTree/InvenTree/status_codes.py index 2032ec75d8..b527009ede 100644 --- a/InvenTree/InvenTree/status_codes.py +++ b/InvenTree/InvenTree/status_codes.py @@ -214,25 +214,25 @@ class BuildStatus(StatusCode): # Build status codes PENDING = 10 # Build is pending / active - ALLOCATED = 20 # Parts have been removed from stock + PRODUCTION = 20 # BuildOrder is in production CANCELLED = 30 # Build was cancelled COMPLETE = 40 # Build is complete options = { PENDING: _("Pending"), - ALLOCATED: _("Allocated"), + PRODUCTION: _("Production"), CANCELLED: _("Cancelled"), COMPLETE: _("Complete"), } colors = { PENDING: 'blue', - ALLOCATED: 'blue', + PRODUCTION: 'blue', COMPLETE: 'green', CANCELLED: 'red', } ACTIVE_CODES = [ PENDING, - ALLOCATED + PRODUCTION, ] diff --git a/InvenTree/build/migrations/0023_auto_20201020_1009.py b/InvenTree/build/migrations/0023_auto_20201020_1009.py new file mode 100644 index 0000000000..be5652d043 --- /dev/null +++ b/InvenTree/build/migrations/0023_auto_20201020_1009.py @@ -0,0 +1,19 @@ +# Generated by Django 3.0.7 on 2020-10-20 10:09 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('build', '0022_auto_20201020_0953'), + ] + + operations = [ + migrations.AlterField( + model_name='build', + name='status', + field=models.PositiveIntegerField(choices=[(10, 'Pending'), (20, 'Production'), (30, 'Cancelled'), (40, 'Complete')], default=10, help_text='Build status code', validators=[django.core.validators.MinValueValidator(0)], verbose_name='Build Status'), + ), + ] diff --git a/InvenTree/build/views.py b/InvenTree/build/views.py index c1431b5730..02e4a8d735 100644 --- a/InvenTree/build/views.py +++ b/InvenTree/build/views.py @@ -440,6 +440,15 @@ class BuildCreate(AjaxCreateView): 'success': _('Created new build'), } + def post_save(self, new_object): + """ + Called immediately after the build has been created. + """ + + build = new_object + + print("Created:", build) + class BuildUpdate(AjaxUpdateView): """ View for editing a Build object """ From 2e4613e702b776209c559bc3474be3c00044743e Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 20 Oct 2020 22:37:21 +1100 Subject: [PATCH 05/75] Updates to build forms / etc --- .../static/script/inventree/modals.js | 26 ++++++++++++++++ InvenTree/build/templates/build/index.html | 7 +---- InvenTree/part/templates/part/build.html | 13 +++----- InvenTree/templates/js/build.html | 31 +++++++++++++++++++ 4 files changed, 63 insertions(+), 14 deletions(-) diff --git a/InvenTree/InvenTree/static/script/inventree/modals.js b/InvenTree/InvenTree/static/script/inventree/modals.js index 49de96468f..d7d42ac941 100644 --- a/InvenTree/InvenTree/static/script/inventree/modals.js +++ b/InvenTree/InvenTree/static/script/inventree/modals.js @@ -134,6 +134,32 @@ function reloadFieldOptions(fieldName, options) { } +function enableField(fieldName, enabled, options={}) { + /* Enable (or disable) a particular field in a modal. + * + * Args: + * - fieldName: The name of the field + * - enabled: boolean enabled / disabled status + * - options: + */ + + var modal = options.modal || '#modal-form'; + + var field = getFieldByName(modal, fieldName); + + field.prop("disabled", !enabled); +} + +function clearField(fieldName, options={}) { + + var modal = options.modal || '#modal-form'; + + var field = getFieldByName(modal, fieldName); + + field.val(""); +} + + function partialMatcher(params, data) { /* Replacement function for the 'matcher' parameter for a select2 dropdown. diff --git a/InvenTree/build/templates/build/index.html b/InvenTree/build/templates/build/index.html index c15b2c2d33..44fc8cfecd 100644 --- a/InvenTree/build/templates/build/index.html +++ b/InvenTree/build/templates/build/index.html @@ -40,12 +40,7 @@ InvenTree | {% trans "Build Orders" %} $("#collapse-item-active").collapse().show(); $("#new-build").click(function() { - launchModalForm( - "{% url 'build-create' %}", - { - follow: true, - } - ); + newBuildOrder(); }); loadBuildTable($("#build-table"), { diff --git a/InvenTree/part/templates/part/build.html b/InvenTree/part/templates/part/build.html index bfd72a2f70..d7ca33c673 100644 --- a/InvenTree/part/templates/part/build.html +++ b/InvenTree/part/templates/part/build.html @@ -29,14 +29,11 @@ {% block js_ready %} {{ block.super }} $("#start-build").click(function() { - launchModalForm( - "{% url 'build-create' %}", - { - follow: true, - data: { - part: {{ part.id }} - } - }); + newBuildOrder({ + data: { + part: {{ part.id }}, + } + }); }); loadBuildTable($("#build-table"), { diff --git a/InvenTree/templates/js/build.html b/InvenTree/templates/js/build.html index 6a12b97bfd..b72d3d179a 100644 --- a/InvenTree/templates/js/build.html +++ b/InvenTree/templates/js/build.html @@ -1,6 +1,37 @@ {% load i18n %} {% load inventree_extras %} +function newBuildOrder(options={}) { + /* Launch modal form to create a new BuildOrder. + */ + + launchModalForm( + "{% url 'build-create' %}", + { + follow: true, + data: options.data || {}, + callback: [ + { + field: 'part', + action: function(value) { + inventreeGet( + `/api/part/${value}/`, {}, + { + success: function(response) { + + //enableField('serial_numbers', response.trackable); + //clearField('serial_numbers'); + } + } + ); + }, + } + ], + } + ) + +} + function loadBuildTable(table, options) { // Display a table of Build objects From 652c2dbcbecb99e2c1041884a6a9964d564f27c5 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 20 Oct 2020 22:37:55 +1100 Subject: [PATCH 06/75] Automagically disable 'serial_numbers' field for StockItemCreate form - Yay, ajax magic! --- InvenTree/stock/forms.py | 2 +- InvenTree/stock/views.py | 9 ++++----- InvenTree/templates/js/stock.html | 13 +++++++++++++ 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/InvenTree/stock/forms.py b/InvenTree/stock/forms.py index 548a03ae90..d9937f3106 100644 --- a/InvenTree/stock/forms.py +++ b/InvenTree/stock/forms.py @@ -108,7 +108,7 @@ class ConvertStockItemForm(HelperForm): class CreateStockItemForm(HelperForm): """ Form for creating a new StockItem """ - serial_numbers = forms.CharField(label='Serial numbers', required=False, help_text=_('Enter unique serial numbers (or leave blank)')) + serial_numbers = forms.CharField(label=_('Serial numbers'), required=False, help_text=_('Enter unique serial numbers (or leave blank)')) def __init__(self, *args, **kwargs): diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py index 65c8f30893..a60ce6b55c 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -1510,11 +1510,8 @@ class StockItemCreate(AjaxCreateView): # form.fields['part'].widget = HiddenInput() # Trackable parts get special consideration: - if part.trackable: - form.fields['delete_on_deplete'].widget = HiddenInput() - form.fields['delete_on_deplete'].initial = False - else: - form.fields['serial_numbers'].widget = HiddenInput() + form.fields['delete_on_deplete'].disabled = not part.trackable + form.fields['serial_numbers'].disabled = not part.trackable # If the part is NOT purchaseable, hide the supplier_part field if not part.purchaseable: @@ -1539,6 +1536,8 @@ class StockItemCreate(AjaxCreateView): # We must not provide *any* options for SupplierPart form.fields['supplier_part'].queryset = SupplierPart.objects.none() + form.fields['serial_numbers'].disabled = True + # Otherwise if the user has selected a SupplierPart, we know what Part they meant! if form['supplier_part'].value() is not None: pass diff --git a/InvenTree/templates/js/stock.html b/InvenTree/templates/js/stock.html index c4209733ef..150765a305 100644 --- a/InvenTree/templates/js/stock.html +++ b/InvenTree/templates/js/stock.html @@ -769,6 +769,7 @@ function createNewStockItem(options) { field: 'part', action: function(value) { + // Reload options for supplier part reloadFieldOptions( 'supplier_part', { @@ -782,6 +783,18 @@ function createNewStockItem(options) { } } ); + + // Disable serial number field if the part is not trackable + inventreeGet( + `/api/part/${value}/`, {}, + { + success: function(response) { + + enableField('serial_numbers', response.trackable); + clearField('serial_numbers'); + } + } + ); } }, ]; From 3bb247a1352f86e4a17ec2416b4d346c8659361e Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 20 Oct 2020 23:27:43 +1100 Subject: [PATCH 07/75] Create an initial stockitem output when a new build is created --- InvenTree/InvenTree/views.py | 8 ++++---- .../migrations/0024_auto_20201020_1144.py | 20 +++++++++++++++++++ InvenTree/build/models.py | 19 +++++++++++++++++- InvenTree/build/templates/build/detail.html | 15 +++++++++++++- InvenTree/build/views.py | 9 ++++----- InvenTree/stock/serializers.py | 3 ++- 6 files changed, 62 insertions(+), 12 deletions(-) create mode 100644 InvenTree/build/migrations/0024_auto_20201020_1144.py diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py index eb168ad547..6ddee71682 100644 --- a/InvenTree/InvenTree/views.py +++ b/InvenTree/InvenTree/views.py @@ -316,13 +316,13 @@ class AjaxCreateView(AjaxMixin, CreateView): - Handles form validation via AJAX POST requests """ - def pre_save(self, **kwargs): + def pre_save(self, form, request, **kwargs): """ Hook for doing something before the form is validated """ pass - def post_save(self, new_object, **kwargs): + def post_save(self, new_object, request, **kwargs): """ Hook for doing something with the created object after it is saved """ @@ -354,9 +354,9 @@ class AjaxCreateView(AjaxMixin, CreateView): if self.form.is_valid(): - self.pre_save() + self.pre_save(self.form, request) self.object = self.form.save() - self.post_save(self.object) + self.post_save(self.object, request) # Return the PK of the newly-created object data['pk'] = self.object.pk diff --git a/InvenTree/build/migrations/0024_auto_20201020_1144.py b/InvenTree/build/migrations/0024_auto_20201020_1144.py new file mode 100644 index 0000000000..2d7c649cd5 --- /dev/null +++ b/InvenTree/build/migrations/0024_auto_20201020_1144.py @@ -0,0 +1,20 @@ +# Generated by Django 3.0.7 on 2020-10-20 11:44 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('part', '0051_bomitem_optional'), + ('build', '0023_auto_20201020_1009'), + ] + + operations = [ + migrations.AlterField( + model_name='build', + name='part', + field=models.ForeignKey(help_text='Select part to build', limit_choices_to={'active': True, 'assembly': True, 'virtual': False}, on_delete=django.db.models.deletion.CASCADE, related_name='builds', to='part.Part', verbose_name='Part'), + ), + ] diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index ca4bb586fe..3ee6aad426 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -111,7 +111,6 @@ class Build(MPTTModel): on_delete=models.CASCADE, related_name='builds', limit_choices_to={ - 'is_template': False, 'assembly': True, 'active': True, 'virtual': False, @@ -226,6 +225,24 @@ class Build(MPTTModel): return new_ref + def createInitialStockItem(self, user): + """ + Create an initial output StockItem to be completed by this build. + """ + + output = StockModels.StockItem.objects.create( + part=self.part, # Link to the parent part + location=None, # No location (yet) until it is completed + quantity=self.quantity, + batch='', # The 'batch' code is not set until the item is completed + build=self, # Point back to this build + is_building=True, # Mark this StockItem as building + ) + + output.save() + + # TODO - Add a transaction note to the new StockItem + @transaction.atomic def cancelBuild(self, user): """ Mark the Build as CANCELLED diff --git a/InvenTree/build/templates/build/detail.html b/InvenTree/build/templates/build/detail.html index 6abbc69bc5..7d4c21aa5b 100644 --- a/InvenTree/build/templates/build/detail.html +++ b/InvenTree/build/templates/build/detail.html @@ -33,7 +33,20 @@ {% if build.take_from %} {{ build.take_from }} {% else %} - {% trans "Stock can be taken from any available location." %} + {% trans "Stock can be taken from any available location." %} + {% endif %} + + + + + {% trans "Destination" %} + + {% if build.destination %} + + {{ build.destination }} + + {% else %} + {% trans "Destination location not specified" %} {% endif %} diff --git a/InvenTree/build/views.py b/InvenTree/build/views.py index 02e4a8d735..e8aeccab36 100644 --- a/InvenTree/build/views.py +++ b/InvenTree/build/views.py @@ -439,15 +439,14 @@ class BuildCreate(AjaxCreateView): return { 'success': _('Created new build'), } - - def post_save(self, new_object): + + def post_save(self, new_object, request, **kwargs): """ - Called immediately after the build has been created. + Called immediately after a new Build object is created. """ build = new_object - - print("Created:", build) + build.createInitialStockItem(request.user) class BuildUpdate(AjaxUpdateView): diff --git a/InvenTree/stock/serializers.py b/InvenTree/stock/serializers.py index d257f12f97..5bea1c4aae 100644 --- a/InvenTree/stock/serializers.py +++ b/InvenTree/stock/serializers.py @@ -154,8 +154,9 @@ class StockItemSerializer(InvenTreeModelSerializer): 'allocated', 'batch', 'belongs_to', - 'customer', + 'build', 'build_order', + 'customer', 'in_stock', 'is_building', 'link', From fd6d6300378024643ac2fc159c66a5a36fccfece Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 20 Oct 2020 23:45:36 +1100 Subject: [PATCH 08/75] Improve grouping in Stock table --- InvenTree/templates/js/stock.html | 93 ++++++++++++++--------- InvenTree/templates/js/table_filters.html | 5 ++ 2 files changed, 64 insertions(+), 34 deletions(-) diff --git a/InvenTree/templates/js/stock.html b/InvenTree/templates/js/stock.html index 150765a305..df5f172d9e 100644 --- a/InvenTree/templates/js/stock.html +++ b/InvenTree/templates/js/stock.html @@ -257,6 +257,56 @@ function loadStockTable(table, options) { filters[key] = params[key]; } + function locationDetail(row) { + /* + * Function to display a "location" of a StockItem. + * + * Complicating factors: A StockItem may not actually *be* in a location! + * - Could be at a customer + * - Could be installed in another stock item + * - Could be assigned to a sales order + * - Could be currently in production! + * + * So, instead of being naive, we'll check! + */ + + // Display text + var text = ''; + + // URL (optional) + var url = ''; + + if (row.belongs_to) { + // StockItem is installed inside a different StockItem + text = `{% trans "Installed in Stock Item" %} ${row.belongs_to}`; + url = `/stock/item/${row.belongs_to}/installed/`; + } else if (row.customer) { + // StockItem has been assigned to a customer + text = "{% trans "Shipped to customer" %}"; + url = `/company/${row.customer}/assigned-stock/`; + } else if (row.sales_order) { + // StockItem has been assigned to a sales order + text = "{% trans "Assigned to Sales Order" %}"; + url = `/order/sales-order/${row.sales_order}/`; + } else if (row.is_building && row.build) { + // StockItem is currently being built! + text = "{% trans "In production" %}"; + url = `/build/${row.build}/`; + } else if (row.location) { + text = row.location_detail.pathstring; + url = `/stock/location/${row.location}/`; + } else { + text = "{% trans "No stock location set" %}"; + url = ''; + } + + if (url) { + return renderLink(text, url); + } else { + return text; + } + } + table.inventreeTable({ method: 'get', formatNoMatches: function() { @@ -353,28 +403,20 @@ function loadStockTable(table, options) { data.forEach(function(item) { - var loc = null; + var detail = locationDetail(item); - if (item.location_detail) { - loc = item.location_detail.pathstring; - } else { - loc = "{% trans "Undefined location" %}"; - } - - if (!locations.includes(loc)) { - locations.push(loc); + if (!locations.includes(detail)) { + locations.push(detail); } }); - if (locations.length > 1) { + if (locations.length == 1) { + // Single location, easy! + return locations[0]; + } else if (locations.length > 1) { return "In " + locations.length + " locations"; } else { - // A single location! - if (row.location_detail) { - return renderLink(row.location_detail.pathstring, `/stock/location/${row.location}/`); - } else { - return "{% trans "Undefined location" %}"; - } + return "{% trans "Undefined location" %}"; } } else if (field == 'notes') { var notes = []; @@ -519,24 +561,7 @@ function loadStockTable(table, options) { title: '{% trans "Location" %}', sortable: true, formatter: function(value, row, index, field) { - if (row.belongs_to) { - var text = "{% trans 'Installed in Stock Item ' %}" + row.belongs_to; - var url = `/stock/item/${row.belongs_to}/installed/`; - - return renderLink(text, url); - } else if (row.customer) { - var text = "{% trans "Shipped to customer" %}"; - return renderLink(text, `/company/${row.customer}/assigned-stock/`); - } else if (row.sales_order) { - var text = `{% trans "Assigned to sales order" %}`; - return renderLink(text, `/order/sales-order/${row.sales_order}/`); - } - else if (value) { - return renderLink(value, `/stock/location/${row.location}/`); - } - else { - return '{% trans "No stock location set" %}'; - } + return locationDetail(row); } }, { diff --git a/InvenTree/templates/js/table_filters.html b/InvenTree/templates/js/table_filters.html index a97d358828..9b73c3f311 100644 --- a/InvenTree/templates/js/table_filters.html +++ b/InvenTree/templates/js/table_filters.html @@ -65,6 +65,11 @@ function getAvailableTableFilters(tableKey) { title: '{% trans "In Stock" %}', description: '{% trans "Show items which are in stock" %}', }, + is_building: { + type: 'bool', + title: '{% trans "In Production" %}', + description: '{% trans "Show items which are in production" %}', + }, installed: { type: 'bool', title: '{% trans "Installed" %}', From e02536071d421305de339c012fafbb1a4f149ed5 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 20 Oct 2020 23:59:37 +1100 Subject: [PATCH 09/75] Add a "completed" field to the Build model - Keeps track of how many outputs have been produced - Will not be directly editable by the user --- .../migrations/0025_auto_20201020_1248.py | 24 ++ InvenTree/build/models.py | 23 +- InvenTree/build/serializers.py | 4 +- .../build/templates/build/build_base.html | 24 +- InvenTree/build/templates/build/detail.html | 207 +++++++++++------- InvenTree/stock/models.py | 2 +- .../stock/templates/stock/item_base.html | 23 +- 7 files changed, 183 insertions(+), 124 deletions(-) create mode 100644 InvenTree/build/migrations/0025_auto_20201020_1248.py diff --git a/InvenTree/build/migrations/0025_auto_20201020_1248.py b/InvenTree/build/migrations/0025_auto_20201020_1248.py new file mode 100644 index 0000000000..ecc0b73ac9 --- /dev/null +++ b/InvenTree/build/migrations/0025_auto_20201020_1248.py @@ -0,0 +1,24 @@ +# Generated by Django 3.0.7 on 2020-10-20 12:48 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('build', '0024_auto_20201020_1144'), + ] + + operations = [ + migrations.AddField( + model_name='build', + name='completed', + field=models.PositiveIntegerField(default=0, help_text='Number of stock items which have been completed', verbose_name='Completed items'), + ), + migrations.AlterField( + model_name='build', + name='quantity', + field=models.PositiveIntegerField(default=1, help_text='Number of stock items to build', validators=[django.core.validators.MinValueValidator(1)], verbose_name='Build Quantity'), + ), + ] diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index 3ee6aad426..a22aae196f 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -62,21 +62,6 @@ class Build(MPTTModel): def get_absolute_url(self): return reverse('build-detail', kwargs={'pk': self.id}) - - def clean(self): - """ - Validation for Build object. - """ - - super().clean() - - # Build quantity must be an integer - # Maybe in the future this will be adjusted? - - if not self.quantity == int(self.quantity): - raise ValidationError({ - 'quantity': _("Build quantity must be integer value for trackable parts") - }) reference = models.CharField( unique=True, @@ -149,7 +134,13 @@ class Build(MPTTModel): verbose_name=_('Build Quantity'), default=1, validators=[MinValueValidator(1)], - help_text=_('Number of parts to build') + help_text=_('Number of stock items to build') + ) + + completed = models.PositiveIntegerField( + verbose_name=_('Completed items'), + default=0, + help_text=_('Number of stock items which have been completed') ) status = models.PositiveIntegerField( diff --git a/InvenTree/build/serializers.py b/InvenTree/build/serializers.py index 53e75942f0..6367673ce9 100644 --- a/InvenTree/build/serializers.py +++ b/InvenTree/build/serializers.py @@ -38,6 +38,7 @@ class BuildSerializer(InvenTreeModelSerializer): 'url', 'title', 'creation_date', + 'completed', 'completion_date', 'part', 'part_detail', @@ -51,9 +52,10 @@ class BuildSerializer(InvenTreeModelSerializer): ] read_only_fields = [ - 'status', + 'completed', 'creation_date', 'completion_data', + 'status', 'status_text', ] diff --git a/InvenTree/build/templates/build/build_base.html b/InvenTree/build/templates/build/build_base.html index a366459908..3a94398e87 100644 --- a/InvenTree/build/templates/build/build_base.html +++ b/InvenTree/build/templates/build/build_base.html @@ -68,11 +68,6 @@ src="{% static 'img/blank_image.png' %}"

{% trans "Build Details" %}

- - - - - @@ -88,6 +83,11 @@ src="{% static 'img/blank_image.png' %}" + + + + + {% if build.parent %} @@ -102,20 +102,6 @@ src="{% static 'img/blank_image.png' %}" {% endif %} - - - - -
{% trans "Build Order Reference" %}{{ build }}
{% trans "Part" %}{% trans "Status" %} {% build_status_label build.status %}
{% trans "Progress" %} {{ build.completed }} / {{ build.quantity }}
{{ build.sales_order }}
{% trans "BOM Price" %} - {% if bom_price %} - {{ bom_price }} - {% if build.part.has_complete_bom_pricing == False %} -
{% trans "BOM pricing is incomplete" %} - {% endif %} - {% else %} - {% trans "No pricing information" %} - {% endif %} -
{% endblock %} diff --git a/InvenTree/build/templates/build/detail.html b/InvenTree/build/templates/build/detail.html index 7d4c21aa5b..b51d12f772 100644 --- a/InvenTree/build/templates/build/detail.html +++ b/InvenTree/build/templates/build/detail.html @@ -10,90 +10,133 @@
- - - - - - - - - - - - - - - - - - - - + + + + + {% endif %} + + + + + +
{% trans "Title" %}{{ build.title }}
{% trans "Part" %}{{ build.part.full_name }}
{% trans "Quantity" %}{{ build.quantity }}
{% trans "Stock Source" %} - {% if build.take_from %} - {{ build.take_from }} - {% else %} - {% trans "Stock can be taken from any available location." %} +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {% if build.batch %} + + + + + {% endif %} - - - - - - + + + + {% endif %} - - - - - - - -{% if build.batch %} - - - - - -{% endif %} -{% if build.link %} - - - - - -{% endif %} - - - - - -{% if build.is_active %} - - - - + + + + {% endif %} - - -{% endif %} -{% if build.completion_date %} - - - - - -{% endif %} -
{% trans "Description" %}{{ build.title }}
{% trans "Part" %}{{ build.part.full_name }}
{% trans "Quantity" %}{{ build.quantity }}
{% trans "Stock Source" %} + {% if build.take_from %} + {{ build.take_from }} + {% else %} + {% trans "Stock can be taken from any available location." %} + {% endif %} +
{% trans "Destination" %} + {% if build.destination %} + + {{ build.destination }} + + {% else %} + {% trans "Destination location not specified" %} + {% endif %} +
{% trans "Status" %}{% build_status_label build.status %}
{% trans "Progress" %}{{ build.completed }} / {{ build.quantity }}
{% trans "Batch" %}{{ build.batch }}
{% trans "Destination" %} - {% if build.destination %} - - {{ build.destination }} - - {% else %} - {% trans "Destination location not specified" %} + {% if build.parent %} +
{% trans "Parent Build" %}{{ build.parent }}
{% trans "Status" %}{% build_status_label build.status %}
{% trans "Batch" %}{{ build.batch }}
{% trans "External Link" %}{{ build.link }}
{% trans "Created" %}{{ build.creation_date }}
{% trans "Enough Parts?" %} - {% if build.can_build %} - {% trans "Yes" %} - {% else %} - {% trans "No" %} + {% if build.sales_order %} +
{% trans "Sales Order" %}{{ build.sales_order }}
{% trans "Completed" %}{{ build.completion_date }}{% if build.completed_by %}{{ build.completed_by }}{% endif %}
+ {% if build.link %} +
{% trans "External Link" %}{{ build.link }}
{% trans "Created" %}{{ build.creation_date }}
+ +
+ + + + + + + + {% if build.is_active %} + + + + + + {% endif %} + {% if build.completion_date %} + + + + + + {% endif %} + +
{% trans "BOM Price" %} + {% if bom_price %} + {{ bom_price }} + {% if build.part.has_complete_bom_pricing == False %} +
{% trans "BOM pricing is incomplete" %} + {% endif %} + {% else %} + {% trans "No pricing information" %} + {% endif %} +
{% trans "Enough Parts?" %} + {% if build.can_build %} + {% trans "Yes" %} + {% else %} + {% trans "No" %} + {% endif %} +
{% trans "Completed" %}{{ build.completion_date }}{% if build.completed_by %}{{ build.completed_by }}{% endif %}
+
+ {% endblock %} diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 1535ded420..f9079e1936 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -130,7 +130,7 @@ class StockItem(MPTTModel): status: Status of this StockItem (ref: InvenTree.status_codes.StockStatus) notes: Extra notes field build: Link to a Build (if this stock item was created from a build) - is_building: Boolean field indicating if this stock item is currently being built + is_building: Boolean field indicating if this stock item is currently being built (or is "in production") purchase_order: Link to a PurchaseOrder (if this stock item was created from a PurchaseOrder) infinite: If True this StockItem can never be exhausted sales_order: Link to a SalesOrder object (if the StockItem has been assigned to a SalesOrder) diff --git a/InvenTree/stock/templates/stock/item_base.html b/InvenTree/stock/templates/stock/item_base.html index d7eb987aab..2538dbd398 100644 --- a/InvenTree/stock/templates/stock/item_base.html +++ b/InvenTree/stock/templates/stock/item_base.html @@ -15,6 +15,20 @@ InvenTree | {% trans "Stock Item" %} - {{ item }} {% block pre_content %} {% include 'stock/loc_link.html' with location=item.location %} +{% if item.is_building %} +
+ {% trans "This stock item is in production and cannot be edited." %}
+ {% trans "Edit the stock item from the build view." %}
+ + {% if item.build %} + + {{ item.build }} + + {% endif %} + +
+{% endif %} + {% if item.hasRequiredTests and not item.passedAllRequiredTests %}
{% trans "This stock item has not passed all required tests" %} @@ -79,7 +93,6 @@ InvenTree | {% trans "Stock Item" %} - {{ item }}
-
@@ -99,7 +112,7 @@ InvenTree | {% trans "Stock Item" %} - {{ item }}
- {% if roles.stock.change %} + {% if roles.stock.change and not item.is_building %}
{% endif %} - {% if roles.stock.change %} + {% if roles.stock.change and not item.is_building %}
{% endif %}
- -
+ + + + +
+ {% for item in build.incomplete_outputs %} + {% include "build/allocation_card.html" with item=item complete=False %} + {% endfor %} +
{% endblock %} {% block js_ready %} {{ block.super }} + {% for item in build.incomplete_outputs %} + + // Get the build output as a javascript object + inventreeGet('{% url 'api-stock-detail' item.pk %}', {}, + { + success: function(response) { + loadBuildOutputAllocationTable( + {{ build.pk }}, + {{ build.part.pk }}, + response + ); + } + } + ); + {% endfor %} + var buildTable = $("#build-item-list"); // Calculate sum of allocations for a particular table row diff --git a/InvenTree/build/templates/build/allocation_card.html b/InvenTree/build/templates/build/allocation_card.html new file mode 100644 index 0000000000..c00eaf6bbf --- /dev/null +++ b/InvenTree/build/templates/build/allocation_card.html @@ -0,0 +1,29 @@ +{% load i18n %} + +
+ +
+
+
+
+
+
\ No newline at end of file diff --git a/InvenTree/build/templates/build/create_build_item.html b/InvenTree/build/templates/build/create_build_item.html index c08b987d36..8f58e884d6 100644 --- a/InvenTree/build/templates/build/create_build_item.html +++ b/InvenTree/build/templates/build/create_build_item.html @@ -1,9 +1,22 @@ {% extends "modal_form.html" %} +{% load i18n %} {% block pre_form_content %} +
+

+ {% trans "Select a stock item to allocate to the selected build output" %} +

+ {% if output %} +

+ {% trans "The allocated stock will be installed into the following build output:" %} +
+ {{ output }} +

+ {% endif %} +
{% if no_stock %} {% endif %} {% endblock %} \ No newline at end of file diff --git a/InvenTree/build/views.py b/InvenTree/build/views.py index e8aeccab36..8303c88f9f 100644 --- a/InvenTree/build/views.py +++ b/InvenTree/build/views.py @@ -492,23 +492,37 @@ class BuildItemDelete(AjaxDeleteView): class BuildItemCreate(AjaxCreateView): - """ View for allocating a new part to a build """ + """ + View for allocating a StockItems to a build output. + """ model = BuildItem form_class = forms.EditBuildItemForm ajax_template_name = 'build/create_build_item.html' - ajax_form_title = _('Allocate new Part') + ajax_form_title = _('Allocate stock to build output') role_required = 'build.add' + # The output StockItem against which the allocation is being made + output = None + + # The "part" which is being allocated to the output part = None + available_stock = None def get_context_data(self): - ctx = super(AjaxCreateView, self).get_context_data() + """ + Provide context data to the template which renders the form. + """ + + ctx = super().get_context_data() if self.part: ctx['part'] = self.part + if self.output: + ctx['output'] = self.output + if self.available_stock: ctx['stock'] = self.available_stock else: @@ -526,7 +540,28 @@ class BuildItemCreate(AjaxCreateView): build_id = form['build'].value() if build_id is not None: + """ + If the build has been provided, hide the widget to change the build selection. + Additionally, update the allowable selections for other fields. + """ form.fields['build'].widget = HiddenInput() + form.fields['install_into'].queryset = StockItem.objects.filter(build=build_id, is_building=True) + else: + """ + Build has *not* been selected + """ + pass + + # If the output stock item is specified, hide the input field + output_id = form['install_into'].value() + + if output_id is not None: + + try: + self.output = StockItem.objects.get(pk=output_id) + form.fields['install_into'].widget = HiddenInput() + except (ValueError, StockItem.DoesNotExist): + pass # If the sub_part is supplied, limit to matching stock items part_id = self.get_param('part') @@ -577,12 +612,15 @@ class BuildItemCreate(AjaxCreateView): """ Provide initial data for BomItem. Look for the folllowing in the GET data: - build: pk of the Build object + - part: pk of the Part object which we are assigning + - output: pk of the StockItem object into which the allocated stock will be installed """ initials = super(AjaxCreateView, self).get_initial().copy() build_id = self.get_param('build') part_id = self.get_param('part') + output_id = self.get_param('install_into') # Reference to a Part object part = None @@ -593,6 +631,9 @@ class BuildItemCreate(AjaxCreateView): # Reference to a Build object build = None + # Reference to a StockItem object + output = None + if part_id: try: part = Part.objects.get(pk=part_id) @@ -623,7 +664,7 @@ class BuildItemCreate(AjaxCreateView): if item_id: try: item = StockItem.objects.get(pk=item_id) - except: + except (ValueError, StockItem.DoesNotExist): pass # If a StockItem is not selected, try to auto-select one @@ -639,6 +680,17 @@ class BuildItemCreate(AjaxCreateView): else: quantity = min(quantity, item.unallocated_quantity()) + # If the output has been specified + print("output_id:", output_id) + if output_id: + try: + output = StockItem.objects.get(pk=output_id) + initials['install_into'] = output + print("Output:", output) + except (ValueError, StockItem.DoesNotExist): + pass + print("no output found") + if quantity is not None: initials['quantity'] = quantity diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index d427409c71..b289b3e0ad 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -934,8 +934,10 @@ class Part(MPTTModel): def required_parts(self): """ Return a list of parts required to make this part (list of BOM items) """ parts = [] + for bom in self.bom_items.all().select_related('sub_part'): parts.append(bom.sub_part) + return parts def get_allowed_bom_items(self): diff --git a/InvenTree/templates/js/build.html b/InvenTree/templates/js/build.html index 886657a2bc..4118e6ed54 100644 --- a/InvenTree/templates/js/build.html +++ b/InvenTree/templates/js/build.html @@ -29,9 +29,155 @@ function newBuildOrder(options={}) { ], } ) - } + +function loadBuildOutputAllocationTable(buildId, partId, output, options={}) { + /* + * Load the "allocation table" for a particular build output. + * + * Args: + * - buildId: The PK of the Build object + * - partId: The PK of the Part object + * - output: The StockItem object which is the "output" of the build + * - options: + * -- table: The #id of the table (will be auto-calculated if not provided) + */ + + var outputId = output.pk; + + var table = options.table || `#allocation-table-${outputId}`; + + function reloadTable() { + // Reload the entire build allocation table + $(table).bootstrapTable('refresh'); + } + + function setupCallbacks() { + // Register button callbacks once table data are loaded + + // Callback for 'allocate' button + $(table).find(".button-add").click(function() { + + // Primary key of the 'sub_part' + var pk = $(this).attr('pk'); + + // Extract row data from the table + var idx = $(this).closest('tr').attr('data-index'); + var row = $(table).bootstrapTable('getData')[idx]; + + // Launch form to allocate new stock against this output + launchModalForm("{% url 'build-item-create' %}", { + success: reloadTable, + data: { + part: pk, + build: buildId, + install_into: outputId, + }, + secondary: [ + { + field: 'stock_item', + label: '{% trans "New Stock Item" %}', + title: '{% trans "Create new Stock Item" %}', + url: '{% url "stock-item-create" %}', + data: { + part: pk, + }, + }, + ] + }); + }); + } + + // Load table of BOM items + $(table).inventreeTable({ + url: "{% url 'api-bom-list' %}", + queryParams: { + part: partId, + sub_part_detail: true, + }, + formatNoMatches: function() { + return "{% trans "No BOM items found" %}"; + }, + name: 'build-allocation', + onPostBody: setupCallbacks, + onLoadSuccess: function(tableData) { + // Once the BOM data are loaded, request allocation data for this build output + + inventreeGet('/api/build/item/', + { + build: buildId, + output: outputId, + }, + { + success: function(data) { + // TODO + } + } + ); + }, + showColumns: false, + detailViewByClick: true, + detailView: true, + detailFilter: function(index, row) { + return row.allocations != null; + }, + detailFormatter: function(index, row, element) { + // TODO + return '---'; + }, + columns: [ + { + field: 'pk', + visible: false, + }, + { + field: 'sub_part_detail.full_name', + title: "{% trans "Required Part" %}", + sortable: true, + formatter: function(value, row, index, field) { + var url = `/part/${row.sub_part}/`; + var thumb = row.sub_part_detail.thumbnail; + var name = row.sub_part_detail.full_name; + + var html = imageHoverIcon(thumb) + renderLink(name, url); + + return html; + } + }, + { + field: 'reference', + title: '{% trans "Reference" %}', + }, + { + field: 'allocated', + title: '{% trans "Allocated" %}', + formatter: function(value, row, index, field) { + var allocated = value || 0; + var required = row.quantity * output.quantity; + + return makeProgressBar(allocated, required); + } + }, + { + field: 'actions', + title: '{% trans "Actions" %}', + formatter: function(value, row, index, field) { + // Generate action buttons for this build output + var html = `
`; + + html += makeIconButton('fa-plus icon-green', 'button-add', row.sub_part, '{% trans "Allocate stock" %}'); + + html += '
'; + + return html; + } + }, + ] + }); +} + + function loadBuildTable(table, options) { // Display a table of Build objects From d37cdd8e50471b48381363d32781557d030684aa Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 22 Oct 2020 23:49:23 +1100 Subject: [PATCH 15/75] Improved filtering for stockitems going into a buildallocation --- InvenTree/build/views.py | 37 ++++++++++++++++++------------------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/InvenTree/build/views.py b/InvenTree/build/views.py index 8303c88f9f..ea4d1905d4 100644 --- a/InvenTree/build/views.py +++ b/InvenTree/build/views.py @@ -566,14 +566,18 @@ class BuildItemCreate(AjaxCreateView): # If the sub_part is supplied, limit to matching stock items part_id = self.get_param('part') + # We need to precisely control which StockItem objects the user can choose to allocate + stock_filter = form.fields['stock_item'].queryset + + # Restrict to only items which are "in stock" + stock_filter = stock_filter.filter(StockItem.IN_STOCK_FILTER) + if part_id: try: self.part = Part.objects.get(pk=part_id) - - query = form.fields['stock_item'].queryset - + # Only allow StockItem objects which match the current part - query = query.filter(part=part_id) + stock_filter = stock_filter.filter(part=part_id) if build_id is not None: try: @@ -581,31 +585,26 @@ class BuildItemCreate(AjaxCreateView): if build.take_from is not None: # Limit query to stock items that are downstream of the 'take_from' location - query = query.filter(location__in=[loc for loc in build.take_from.getUniqueChildren()]) + stock_filter = stock_filter.filter(location__in=[loc for loc in build.take_from.getUniqueChildren()]) except Build.DoesNotExist: pass # Exclude StockItem objects which are already allocated to this build and part - query = query.exclude(id__in=[item.stock_item.id for item in BuildItem.objects.filter(build=build_id, stock_item__part=part_id)]) - - form.fields['stock_item'].queryset = query - - stocks = query.all() - self.available_stock = stocks - - # If there is only one item selected, select it - if len(stocks) == 1: - form.fields['stock_item'].initial = stocks[0].id - # There is no stock available - elif len(stocks) == 0: - # TODO - Add a message to the form describing the problem - pass + stock_filter = stock_filter.exclude(id__in=[item.stock_item.id for item in BuildItem.objects.filter(build=build_id, stock_item__part=part_id)]) except Part.DoesNotExist: self.part = None pass + form.fields['stock_item'].query = stock_filter + + self.available_stock = stock_filter.all() + + # If there is only a single stockitem available, select it! + if len(self.available_stock) == 1: + form.fields['stock_item'].initial = self.available_stock[0].pk + return form def get_initial(self): From ae20db0ec6d1aa1a68b2c314a79ef8bac8f2f09d Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 22 Oct 2020 23:57:07 +1100 Subject: [PATCH 16/75] Add actions for the sub-table allocation list --- InvenTree/templates/js/build.html | 136 +++++++++++++++++++++++++++++- 1 file changed, 132 insertions(+), 4 deletions(-) diff --git a/InvenTree/templates/js/build.html b/InvenTree/templates/js/build.html index 4118e6ed54..0169fc0276 100644 --- a/InvenTree/templates/js/build.html +++ b/InvenTree/templates/js/build.html @@ -100,6 +100,7 @@ function loadBuildOutputAllocationTable(buildId, partId, output, options={}) { return "{% trans "No BOM items found" %}"; }, name: 'build-allocation', + uniqueId: 'sub_part', onPostBody: setupCallbacks, onLoadSuccess: function(tableData) { // Once the BOM data are loaded, request allocation data for this build output @@ -111,11 +112,38 @@ function loadBuildOutputAllocationTable(buildId, partId, output, options={}) { }, { success: function(data) { - // TODO + // Iterate through the returned data, and group by the part they point to + var allocations = {}; + + data.forEach(function(item) { + + // Group BuildItem objects by part + var part = item.part; + var key = parseInt(part); + + if (!(key in allocations)) { + allocations[key] = new Array(); + } + + allocations[key].push(item); + }); + + // Now update the allocations for each row in the table + for (var key in allocations) { + // Select the associated row in the table + var tableRow = $(table).bootstrapTable('getRowByUniqueId', key); + + // Set the allocation list for that row + tableRow.allocations = allocations[key]; + + // Push the updated row back into the main table + $(table).bootstrapTable('updateByUniqueId', key, tableRow, true); + } } } ); }, + sortable: true, showColumns: false, detailViewByClick: true, detailView: true, @@ -123,8 +151,99 @@ function loadBuildOutputAllocationTable(buildId, partId, output, options={}) { return row.allocations != null; }, detailFormatter: function(index, row, element) { - // TODO - return '---'; + // Contruct an 'inner table' which shows which stock items have been allocated + + var subTableId = `allocation-table-${row.pk}`; + + var html = `
`; + + element.html(html); + + var lineItem = row; + + var subTable = $(`#${subTableId}`); + + subTable.bootstrapTable({ + data: row.allocations, + showHeader: true, + columns: [ + { + width: '50%', + field: 'quantity', + title: '{% trans "Assigned Stock" %}', + formatter: function(value, row, index, field) { + var text = ''; + + var url = ''; + + if (row.serial && row.quantity == 1) { + text = `{% trans "Serial Number" %}: ${row.serial}`; + } else { + text = `{% trans "Quantity" %}: ${row.quantity}`; + } + + {% if build.status == BuildStatus.COMPLETE %} + url = `/stock/item/${row.pk}/`; + {% else %} + url = `/stock/item/${row.stock_item}/`; + {% endif %} + + return renderLink(text, url); + } + }, + { + field: 'location', + title: '{% trans "Location" %}', + formatter: function(value, row, index, field) { + if (row.stock_item_detail.location) { + var text = row.stock_item_detail.location_name; + var url = `/stock/location/${row.stock_item_detail.location}/`; + + return renderLink(text, url); + } else { + return '{% trans "No location set" %}'; + } + } + }, + { + field: 'actions', + formatter: function(value, row, index, field) { + /* Actions available for a particular stock item allocation: + * + * - Edit the allocation quantity + * - Delete the allocation + */ + + var pk = row.pk; + + var html = `
`; + + html += makeIconButton('fa-edit icon-blue', 'button-allocation-edit', pk, '{% trans "Edit stock allocation" %}'); + + html += makeIconButton('fa-trash-alt icon-red', 'button-allocation-delete', pk, '{% trans "Delete stock allocation" %}'); + + html += `
`; + + return html; + } + } + ] + }); + + // Assign button callbacks to the newly created allocation buttons + subTable.find('.button-allocation-edit').click(function() { + var pk = $(this).attr('pk'); + launchModalForm(`/build/item/${pk}/edit/`, { + success: reloadTable, + }); + }); + + subTable.find('.button-allocation-delete').click(function() { + var pk = $(this).attr('pk'); + launchModalForm(`/build/item/${pk}/delete/`, { + success: reloadTable, + }); + }); }, columns: [ { @@ -148,12 +267,21 @@ function loadBuildOutputAllocationTable(buildId, partId, output, options={}) { { field: 'reference', title: '{% trans "Reference" %}', + sortable: true, }, { field: 'allocated', title: '{% trans "Allocated" %}', + sortable: true, formatter: function(value, row, index, field) { - var allocated = value || 0; + var allocated = 0; + + if (row.allocations) { + row.allocations.forEach(function(item) { + allocated += item.quantity; + }); + } + var required = row.quantity * output.quantity; return makeProgressBar(allocated, required); From 23ac83d2a8165529419dc186e02896a0df1174bc Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 22 Oct 2020 23:59:21 +1100 Subject: [PATCH 17/75] Change extension on "dynamic" js files - Yay, the editor now highlights code properly! --- InvenTree/InvenTree/urls.py | 16 ++++++++-------- .../templates/js/{barcode.html => barcode.js} | 0 InvenTree/templates/js/{bom.html => bom.js} | 0 InvenTree/templates/js/{build.html => build.js} | 0 .../templates/js/{company.html => company.js} | 0 InvenTree/templates/js/{order.html => order.js} | 0 InvenTree/templates/js/{part.html => part.js} | 0 InvenTree/templates/js/{stock.html => stock.js} | 0 .../js/{table_filters.html => table_filters.js} | 0 9 files changed, 8 insertions(+), 8 deletions(-) rename InvenTree/templates/js/{barcode.html => barcode.js} (100%) rename InvenTree/templates/js/{bom.html => bom.js} (100%) rename InvenTree/templates/js/{build.html => build.js} (100%) rename InvenTree/templates/js/{company.html => company.js} (100%) rename InvenTree/templates/js/{order.html => order.js} (100%) rename InvenTree/templates/js/{part.html => part.js} (100%) rename InvenTree/templates/js/{stock.html => stock.js} (100%) rename InvenTree/templates/js/{table_filters.html => table_filters.js} (100%) diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index 0b4292cbd9..7015e00d14 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -84,14 +84,14 @@ settings_urls = [ # Some javascript files are served 'dynamically', allowing them to pass through the Django translation layer dynamic_javascript_urls = [ - url(r'^barcode.js', DynamicJsView.as_view(template_name='js/barcode.html'), name='barcode.js'), - url(r'^part.js', DynamicJsView.as_view(template_name='js/part.html'), name='part.js'), - url(r'^stock.js', DynamicJsView.as_view(template_name='js/stock.html'), name='stock.js'), - url(r'^build.js', DynamicJsView.as_view(template_name='js/build.html'), name='build.js'), - url(r'^order.js', DynamicJsView.as_view(template_name='js/order.html'), name='order.js'), - url(r'^company.js', DynamicJsView.as_view(template_name='js/company.html'), name='company.js'), - url(r'^bom.js', DynamicJsView.as_view(template_name='js/bom.html'), name='bom.js'), - url(r'^table_filters.js', DynamicJsView.as_view(template_name='js/table_filters.html'), name='table_filters.js'), + url(r'^barcode.js', DynamicJsView.as_view(template_name='js/barcode.js'), name='barcode.js'), + url(r'^part.js', DynamicJsView.as_view(template_name='js/part.js'), name='part.js'), + url(r'^stock.js', DynamicJsView.as_view(template_name='js/stock.js'), name='stock.js'), + url(r'^build.js', DynamicJsView.as_view(template_name='js/build.js'), name='build.js'), + url(r'^order.js', DynamicJsView.as_view(template_name='js/order.js'), name='order.js'), + url(r'^company.js', DynamicJsView.as_view(template_name='js/company.js'), name='company.js'), + url(r'^bom.js', DynamicJsView.as_view(template_name='js/bom.js'), name='bom.js'), + url(r'^table_filters.js', DynamicJsView.as_view(template_name='js/table_filters.js'), name='table_filters.js'), ] urlpatterns = [ diff --git a/InvenTree/templates/js/barcode.html b/InvenTree/templates/js/barcode.js similarity index 100% rename from InvenTree/templates/js/barcode.html rename to InvenTree/templates/js/barcode.js diff --git a/InvenTree/templates/js/bom.html b/InvenTree/templates/js/bom.js similarity index 100% rename from InvenTree/templates/js/bom.html rename to InvenTree/templates/js/bom.js diff --git a/InvenTree/templates/js/build.html b/InvenTree/templates/js/build.js similarity index 100% rename from InvenTree/templates/js/build.html rename to InvenTree/templates/js/build.js diff --git a/InvenTree/templates/js/company.html b/InvenTree/templates/js/company.js similarity index 100% rename from InvenTree/templates/js/company.html rename to InvenTree/templates/js/company.js diff --git a/InvenTree/templates/js/order.html b/InvenTree/templates/js/order.js similarity index 100% rename from InvenTree/templates/js/order.html rename to InvenTree/templates/js/order.js diff --git a/InvenTree/templates/js/part.html b/InvenTree/templates/js/part.js similarity index 100% rename from InvenTree/templates/js/part.html rename to InvenTree/templates/js/part.js diff --git a/InvenTree/templates/js/stock.html b/InvenTree/templates/js/stock.js similarity index 100% rename from InvenTree/templates/js/stock.html rename to InvenTree/templates/js/stock.js diff --git a/InvenTree/templates/js/table_filters.html b/InvenTree/templates/js/table_filters.js similarity index 100% rename from InvenTree/templates/js/table_filters.html rename to InvenTree/templates/js/table_filters.js From 6245d65ebca59ee4c5f7bd815650416d2caa4c4b Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 23 Oct 2020 00:08:40 +1100 Subject: [PATCH 18/75] Tweaks --- InvenTree/build/models.py | 6 +++--- .../build/templates/build/edit_build_item.html | 10 ++++++++++ InvenTree/build/views.py | 15 +++++---------- InvenTree/templates/js/build.js | 4 ++-- 4 files changed, 20 insertions(+), 15 deletions(-) create mode 100644 InvenTree/build/templates/build/edit_build_item.html diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index 166d70afac..c42f52ec1a 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -22,7 +22,7 @@ from markdownx.models import MarkdownxField from mptt.models import MPTTModel, TreeForeignKey from InvenTree.status_codes import BuildStatus -from InvenTree.helpers import increment, getSetting +from InvenTree.helpers import increment, getSetting, normalize from InvenTree.validators import validate_build_order_reference import InvenTree.fields @@ -589,8 +589,8 @@ class BuildItem(models.Model): if 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 + n=normalize(self.quantity), + q=normalize(self.stock_item.quantity) ))] if self.stock_item.quantity - self.stock_item.allocation_count() + self.quantity < self.quantity: diff --git a/InvenTree/build/templates/build/edit_build_item.html b/InvenTree/build/templates/build/edit_build_item.html new file mode 100644 index 0000000000..99cad71ba2 --- /dev/null +++ b/InvenTree/build/templates/build/edit_build_item.html @@ -0,0 +1,10 @@ +{% extends "modal_form.html" %} +{% load i18n %} + +{% block pre_form_content %} +
+

+ {% trans "Alter the quantity of stock allocated to the build output" %} +

+
+{% endblock %} \ No newline at end of file diff --git a/InvenTree/build/views.py b/InvenTree/build/views.py index ea4d1905d4..3d3a03b284 100644 --- a/InvenTree/build/views.py +++ b/InvenTree/build/views.py @@ -700,7 +700,7 @@ class BuildItemEdit(AjaxUpdateView): """ View to edit a BuildItem object """ model = BuildItem - ajax_template_name = 'modal_form.html' + ajax_template_name = 'build/edit_build_item.html' form_class = forms.EditBuildItemForm ajax_form_title = _('Edit Stock Allocation') role_required = 'build.change' @@ -720,14 +720,9 @@ class BuildItemEdit(AjaxUpdateView): form = super(BuildItemEdit, self).get_form() - query = StockItem.objects.all() - - if build_item.stock_item: - part_id = build_item.stock_item.part.id - query = query.filter(part=part_id) - - form.fields['stock_item'].queryset = query - - form.fields['build'].widget = HiddenInput() + # Hide fields which we do not wish the user to edit + for field in ['build', 'stock_item', 'install_into']: + if form[field].value(): + form.fields[field].widget = HiddenInput() return form diff --git a/InvenTree/templates/js/build.js b/InvenTree/templates/js/build.js index 0169fc0276..1b330f63b5 100644 --- a/InvenTree/templates/js/build.js +++ b/InvenTree/templates/js/build.js @@ -252,7 +252,7 @@ function loadBuildOutputAllocationTable(buildId, partId, output, options={}) { }, { field: 'sub_part_detail.full_name', - title: "{% trans "Required Part" %}", + title: '{% trans "Required Part" %}', sortable: true, formatter: function(value, row, index, field) { var url = `/part/${row.sub_part}/`; @@ -326,7 +326,7 @@ function loadBuildTable(table, options) { $(table).inventreeTable({ method: 'get', formatNoMatches: function() { - return "{% trans "No builds matching query" %}"; + return '{% trans "No builds matching query" %}'; }, url: options.url, queryParams: filters, From 33c454ed5a200d1a5ba976b98038d0446be72f0e Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 23 Oct 2020 00:51:01 +1100 Subject: [PATCH 19/75] Add action buttons to each build output --- .../templates/build/allocation_card.html | 12 ++- .../templates/build/delete_build_item.html | 11 ++- InvenTree/build/views.py | 3 - InvenTree/templates/js/build.js | 89 ++++++++++++++++++- 4 files changed, 103 insertions(+), 12 deletions(-) diff --git a/InvenTree/build/templates/build/allocation_card.html b/InvenTree/build/templates/build/allocation_card.html index c00eaf6bbf..6e9faceb22 100644 --- a/InvenTree/build/templates/build/allocation_card.html +++ b/InvenTree/build/templates/build/allocation_card.html @@ -1,6 +1,6 @@ {% load i18n %} -
+