diff --git a/InvenTree/build/forms.py b/InvenTree/build/forms.py index 011aab6a65..d9f497da4a 100644 --- a/InvenTree/build/forms.py +++ b/InvenTree/build/forms.py @@ -22,6 +22,7 @@ class EditBuildForm(HelperForm): fields = [ 'title', 'part', + 'parent', 'sales_order', 'quantity', 'take_from', diff --git a/InvenTree/build/migrations/0014_auto_20200425_1243.py b/InvenTree/build/migrations/0014_auto_20200425_1243.py new file mode 100644 index 0000000000..c8148b6c1b --- /dev/null +++ b/InvenTree/build/migrations/0014_auto_20200425_1243.py @@ -0,0 +1,71 @@ +# Generated by Django 3.0.5 on 2020-04-25 12:43 + +import InvenTree.fields +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion +import markdownx.models +import mptt.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('part', '0035_auto_20200406_0045'), + ('stock', '0031_auto_20200422_0209'), + ('order', '0029_auto_20200423_1042'), + ('build', '0013_auto_20200425_0507'), + ] + + operations = [ + migrations.AlterField( + model_name='build', + name='batch', + field=models.CharField(blank=True, help_text='Batch code for this build output', max_length=100, null=True, verbose_name='Batch Code'), + ), + migrations.AlterField( + model_name='build', + name='link', + field=InvenTree.fields.InvenTreeURLField(blank=True, help_text='Link to external URL', verbose_name='External Link'), + ), + migrations.AlterField( + model_name='build', + name='notes', + field=markdownx.models.MarkdownxField(blank=True, help_text='Extra build notes', verbose_name='Notes'), + ), + migrations.AlterField( + model_name='build', + name='parent', + field=mptt.fields.TreeForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='children', to='build.Build', verbose_name='Parent Build'), + ), + migrations.AlterField( + model_name='build', + name='part', + field=models.ForeignKey(help_text='Select part to build', limit_choices_to={'active': True, 'assembly': True, 'is_template': False, 'virtual': False}, on_delete=django.db.models.deletion.CASCADE, related_name='builds', to='part.Part', verbose_name='Part'), + ), + migrations.AlterField( + model_name='build', + name='quantity', + field=models.PositiveIntegerField(default=1, help_text='Number of parts to build', validators=[django.core.validators.MinValueValidator(1)], verbose_name='Build Quantity'), + ), + migrations.AlterField( + model_name='build', + name='sales_order', + field=models.ForeignKey(blank=True, help_text='SalesOrder to which this build is allocated', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='builds', to='order.SalesOrder', verbose_name='Sales Order Reference'), + ), + migrations.AlterField( + model_name='build', + name='status', + field=models.PositiveIntegerField(choices=[(10, 'Pending'), (20, 'Allocated'), (30, 'Cancelled'), (40, 'Complete')], default=10, help_text='Build status code', validators=[django.core.validators.MinValueValidator(0)], verbose_name='Build Status'), + ), + migrations.AlterField( + model_name='build', + name='take_from', + field=models.ForeignKey(blank=True, help_text='Select location to take stock from for this build (leave blank to take from any stock location)', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sourcing_builds', to='stock.StockLocation', verbose_name='Source Location'), + ), + migrations.AlterField( + model_name='build', + name='title', + field=models.CharField(help_text='Brief description of the build', max_length=100, verbose_name='Build Title'), + ), + ] diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index a8b00fcb47..2b97b2d6d4 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -14,6 +14,7 @@ from django.core.exceptions import ValidationError from django.urls import reverse from django.db import models, transaction from django.db.models import Sum +from django.db.models.functions import Coalesce from django.core.validators import MinValueValidator from markdownx.models import MarkdownxField @@ -53,6 +54,7 @@ class Build(MPTTModel): return reverse('build-detail', kwargs={'pk': self.id}) title = models.CharField( + verbose_name=_('Build Title'), blank=False, max_length=100, help_text=_('Brief description of the build') @@ -62,11 +64,14 @@ class Build(MPTTModel): 'self', on_delete=models.DO_NOTHING, blank=True, null=True, - related_name='children' + related_name='children', + verbose_name=_('Parent Build'), + help_text=_('Parent build to which this build is allocated'), ) part = models.ForeignKey( 'part.Part', + verbose_name=_('Part'), on_delete=models.CASCADE, related_name='builds', limit_choices_to={ @@ -80,6 +85,7 @@ class Build(MPTTModel): sales_order = models.ForeignKey( 'order.SalesOrder', + verbose_name=_('Sales Order Reference'), on_delete=models.SET_NULL, related_name='builds', null=True, blank=True, @@ -88,6 +94,7 @@ class Build(MPTTModel): take_from = models.ForeignKey( 'stock.StockLocation', + verbose_name=_('Source Location'), on_delete=models.SET_NULL, related_name='sourcing_builds', null=True, blank=True, @@ -95,32 +102,48 @@ class Build(MPTTModel): ) quantity = models.PositiveIntegerField( + verbose_name=_('Build Quantity'), default=1, validators=[MinValueValidator(1)], help_text=_('Number of parts to build') ) - status = models.PositiveIntegerField(default=BuildStatus.PENDING, - choices=BuildStatus.items(), - validators=[MinValueValidator(0)], - help_text=_('Build status')) + status = models.PositiveIntegerField( + verbose_name=_('Build Status'), + default=BuildStatus.PENDING, + choices=BuildStatus.items(), + validators=[MinValueValidator(0)], + help_text=_('Build status code') + ) - batch = models.CharField(max_length=100, blank=True, null=True, - help_text=_('Batch code for this build output')) + batch = models.CharField( + verbose_name=_('Batch Code'), + max_length=100, + blank=True, + null=True, + help_text=_('Batch code for this build output') + ) creation_date = models.DateField(auto_now_add=True, editable=False) completion_date = models.DateField(null=True, blank=True) - completed_by = models.ForeignKey(User, - on_delete=models.SET_NULL, - blank=True, null=True, - related_name='builds_completed' - ) + completed_by = models.ForeignKey( + User, + on_delete=models.SET_NULL, + blank=True, null=True, + related_name='builds_completed' + ) - link = InvenTreeURLField(blank=True, help_text=_('Link to external URL')) + link = InvenTreeURLField( + verbose_name=_('External Link'), + blank=True, help_text=_('Link to external URL') + ) - notes = MarkdownxField(blank=True, help_text=_('Extra build notes')) + notes = MarkdownxField( + verbose_name=_('Notes'), + blank=True, help_text=_('Extra build notes') + ) @property def output_count(self): @@ -302,22 +325,21 @@ class Build(MPTTModel): try: item = BomItem.objects.get(part=self.part.id, sub_part=part.id) - return item.get_required_quantity(self.quantity) + q = item.quantity except BomItem.DoesNotExist: - return 0 + q = 0 + + print("required quantity:", q, "*", self.quantity) + + return q * self.quantity def getAllocatedQuantity(self, part): """ Calculate the total number of currently allocated to this build """ - allocated = BuildItem.objects.filter(build=self.id, stock_item__part=part.id).aggregate(Sum('quantity')) + allocated = BuildItem.objects.filter(build=self.id, stock_item__part=part.id).aggregate(q=Coalesce(Sum('quantity'), 0)) - q = allocated['quantity__sum'] - - if q: - return int(q) - else: - return 0 + return allocated['q'] def getUnallocatedQuantity(self, part): """ Calculate the quantity of which still needs to be allocated to this build. diff --git a/InvenTree/build/templates/build/allocate.html b/InvenTree/build/templates/build/allocate.html index 0bf495aa2e..201e7a48ed 100644 --- a/InvenTree/build/templates/build/allocate.html +++ b/InvenTree/build/templates/build/allocate.html @@ -11,20 +11,17 @@ InvenTree | Allocate Parts {% include "build/tabs.html" with tab='allocate' %} -{% if editing %} -{% include "build/allocate_edit.html" %} -{% else %} -{% include "build/allocate_view.html" %} -{% endif %} +
+
+ + +
+
+ +
{% endblock %} -{% block js_load %} -{{ block.super }} - - -{% endblock %} - {% block js_ready %} {{ block.super }} @@ -45,16 +42,149 @@ InvenTree | Allocate Parts return quantity; } + function getUnallocated(row) { + // Return the number of items remaining to be allocated for a given row + return {{ build.quantity }} * row.quantity - sumAllocations(row); + } + + function reloadTable() { + // Reload the build allocation table + buildTable.bootstrapTable('refresh'); + } + + function setupCallbacks() { + // Register button callbacks once the table data are loaded + + buildTable.find(".button-add").click(function() { + var pk = $(this).attr('pk'); + + // Extract row data from the table + var idx = $(this).closest('tr').attr('data-index'); + var row = buildTable.bootstrapTable('getData')[idx]; + + launchModalForm('/build/item/new/', { + success: reloadTable, + data: { + part: row.sub_part, + build: {{ build.id }}, + quantity: getUnallocated(row), + }, + }); + }); + + + buildTable.find(".button-build").click(function() { + // Start a new build for the sub_part + + var pk = $(this).attr('pk'); + + // Extract row data from the table + var idx = $(this).closest('tr').attr('data-index'); + var row = buildTable.bootstrapTable('getData')[idx]; + + launchModalForm('/build/new/', { + follow: true, + data: { + part: row.sub_part, + parent: {{ build.id }}, + quantity: getUnallocated(row), + }, + }); + + }); + + buildTable.find(".button-buy").click(function() { + var pk = $(this).attr('pk'); + + // Extract row data from the table + var idx = $(this).closest('tr').attr('data-index'); + var row = buildTable.bootstrapTable('getData')[idx]; + + launchModalForm("{% url 'order-parts' %}", { + data: { + parts: [row.sub_part], + }, + }); + }); + } + buildTable.inventreeTable({ uniqueId: 'sub_part', url: "{% url 'api-bom-list' %}", + onPostBody: setupCallbacks, detailViewByClick: true, detailView: true, detailFilter: function(index, row) { return row.allocations != null; }, detailFormatter: function(index, row, element) { - return "Hello world"; + // Construct an 'inner table' which shows the stock allocations + + var subTableId = `allocation-table-${row.pk}`; + + var html = `
`; + + element.html(html); + + var lineItem = row; + + var subTable = $(`#${subTableId}`); + + subTable.bootstrapTable({ + data: row.allocations, + showHeader: false, + columns: [ + { + width: '50%', + field: 'quantity', + title: 'Quantity', + formatter: function(value, row, index, field) { + return renderLink(value, `/stock/item/${row.stock_item}/`); + }, + }, + { + field: 'location', + title: '{% trans "Location" %}', + formatter: function(value, row, index, field) { + return renderLink(row.stock_item_detail.location_name, `/stock/location/${row.stock_item_detail.location}/`); + } + }, + { + field: 'buttons', + title: 'Actions', + formatter: function(value, row) { + + var pk = row.pk; + + var html = `
`; + + {% if build.status == BuildStatus.PENDING %} + html += makeIconButton('fa-edit', 'button-allocation-edit', pk, '{% trans "Edit stock allocation" %}'); + html += makeIconButton('fa-trash-alt', 'button-allocation-delete', pk, '{% trans "Delete stock allocation" %}'); + {% endif %} + + 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, + }); + }); }, formatNoMatches: function() { return "{% trans 'No BOM items found' %}"; }, onLoadSuccess: function(tableData) { @@ -172,7 +302,7 @@ InvenTree | Allocate Parts html += makeIconButton('fa-shopping-cart', 'button-buy', pk, '{% trans "Buy parts" %}'); } - if (row.sub_part.assembly) { + if (row.sub_part_detail.assembly) { html += makeIconButton('fa-tools', 'button-build', pk, '{% trans "Build parts" %}'); } diff --git a/InvenTree/build/templates/build/allocate_view.html b/InvenTree/build/templates/build/allocate_view.html deleted file mode 100644 index ed404a8fc1..0000000000 --- a/InvenTree/build/templates/build/allocate_view.html +++ /dev/null @@ -1,42 +0,0 @@ -{% load i18n %} -{% load inventree_extras %} - -

{% trans "Allocated Parts" %}

-
- -
-
- - -
-
- -
- - - - - - - - - - - - - - {% for item in build.required_parts %} - - - - - - - - - {% endfor %} - -
{% trans "Part" %}{% trans "Description" %}{% trans "Available" %}{% trans "Required" %}{% trans "Allocated" %}{% trans "On Order" %}
- {% include "hover_image.html" with image=item.part.image hover=True %} - {{ item.part.full_name }} - {{ item.part.description }}{% decimal item.part.total_stock %}{% decimal item.quantity %}{{ item.allocated }}{% decimal item.part.on_order %}
\ No newline at end of file diff --git a/InvenTree/build/templates/build/build_base.html b/InvenTree/build/templates/build/build_base.html index f83b6c3e79..20eb3b0672 100644 --- a/InvenTree/build/templates/build/build_base.html +++ b/InvenTree/build/templates/build/build_base.html @@ -36,7 +36,7 @@ src="{% static 'img/blank_image.png' %}" {% if build.is_active %}