Add verbose names for multiple Build model fields

- Improve methods for different models
This commit is contained in:
Oliver Walters 2020-04-25 23:17:07 +10:00
parent 181d1d6b91
commit 01a68270ea
10 changed files with 318 additions and 90 deletions

View File

@ -22,6 +22,7 @@ class EditBuildForm(HelperForm):
fields = [ fields = [
'title', 'title',
'part', 'part',
'parent',
'sales_order', 'sales_order',
'quantity', 'quantity',
'take_from', 'take_from',

View File

@ -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'),
),
]

View File

@ -14,6 +14,7 @@ from django.core.exceptions import ValidationError
from django.urls import reverse from django.urls import reverse
from django.db import models, transaction from django.db import models, transaction
from django.db.models import Sum from django.db.models import Sum
from django.db.models.functions import Coalesce
from django.core.validators import MinValueValidator from django.core.validators import MinValueValidator
from markdownx.models import MarkdownxField from markdownx.models import MarkdownxField
@ -53,6 +54,7 @@ class Build(MPTTModel):
return reverse('build-detail', kwargs={'pk': self.id}) return reverse('build-detail', kwargs={'pk': self.id})
title = models.CharField( title = models.CharField(
verbose_name=_('Build Title'),
blank=False, blank=False,
max_length=100, max_length=100,
help_text=_('Brief description of the build') help_text=_('Brief description of the build')
@ -62,11 +64,14 @@ class Build(MPTTModel):
'self', 'self',
on_delete=models.DO_NOTHING, on_delete=models.DO_NOTHING,
blank=True, null=True, 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 = models.ForeignKey(
'part.Part', 'part.Part',
verbose_name=_('Part'),
on_delete=models.CASCADE, on_delete=models.CASCADE,
related_name='builds', related_name='builds',
limit_choices_to={ limit_choices_to={
@ -80,6 +85,7 @@ class Build(MPTTModel):
sales_order = models.ForeignKey( sales_order = models.ForeignKey(
'order.SalesOrder', 'order.SalesOrder',
verbose_name=_('Sales Order Reference'),
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
related_name='builds', related_name='builds',
null=True, blank=True, null=True, blank=True,
@ -88,6 +94,7 @@ class Build(MPTTModel):
take_from = models.ForeignKey( take_from = models.ForeignKey(
'stock.StockLocation', 'stock.StockLocation',
verbose_name=_('Source Location'),
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
related_name='sourcing_builds', related_name='sourcing_builds',
null=True, blank=True, null=True, blank=True,
@ -95,32 +102,48 @@ class Build(MPTTModel):
) )
quantity = models.PositiveIntegerField( quantity = models.PositiveIntegerField(
verbose_name=_('Build Quantity'),
default=1, default=1,
validators=[MinValueValidator(1)], validators=[MinValueValidator(1)],
help_text=_('Number of parts to build') help_text=_('Number of parts to build')
) )
status = models.PositiveIntegerField(default=BuildStatus.PENDING, status = models.PositiveIntegerField(
choices=BuildStatus.items(), verbose_name=_('Build Status'),
validators=[MinValueValidator(0)], default=BuildStatus.PENDING,
help_text=_('Build status')) choices=BuildStatus.items(),
validators=[MinValueValidator(0)],
help_text=_('Build status code')
)
batch = models.CharField(max_length=100, blank=True, null=True, batch = models.CharField(
help_text=_('Batch code for this build output')) 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) creation_date = models.DateField(auto_now_add=True, editable=False)
completion_date = models.DateField(null=True, blank=True) completion_date = models.DateField(null=True, blank=True)
completed_by = models.ForeignKey(User, completed_by = models.ForeignKey(
on_delete=models.SET_NULL, User,
blank=True, null=True, on_delete=models.SET_NULL,
related_name='builds_completed' 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 @property
def output_count(self): def output_count(self):
@ -302,22 +325,21 @@ class Build(MPTTModel):
try: try:
item = BomItem.objects.get(part=self.part.id, sub_part=part.id) 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: except BomItem.DoesNotExist:
return 0 q = 0
print("required quantity:", q, "*", self.quantity)
return q * self.quantity
def getAllocatedQuantity(self, part): def getAllocatedQuantity(self, part):
""" Calculate the total number of <part> currently allocated to this build """ Calculate the total number of <part> 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'] return allocated['q']
if q:
return int(q)
else:
return 0
def getUnallocatedQuantity(self, part): def getUnallocatedQuantity(self, part):
""" Calculate the quantity of <part> which still needs to be allocated to this build. """ Calculate the quantity of <part> which still needs to be allocated to this build.

View File

@ -11,20 +11,17 @@ InvenTree | Allocate Parts
{% include "build/tabs.html" with tab='allocate' %} {% include "build/tabs.html" with tab='allocate' %}
{% if editing %} <div id='build-item-toolbar'>
{% include "build/allocate_edit.html" %} <div class='btn-group'>
{% else %} <button class='btn btn-primary' type='button' id='btn-allocate' title='Allocate Stock'>{% trans "Allocate" %}</button>
{% include "build/allocate_view.html" %} <button class='btn btn-primary' type='button' id='btn-order-parts' title='Order Parts'>{% trans "Order Parts" %}</button>
{% endif %} </div>
</div>
<table class='table table-striped table-condensed' id='build-item-list' data-toolbar='#build-item-toolbar'></table>
{% endblock %} {% endblock %}
{% block js_load %}
{{ block.super }}
<script src="{% static 'script/inventree/part.js' %}"></script>
<script src="{% static 'script/inventree/build.js' %}"></script>
{% endblock %}
{% block js_ready %} {% block js_ready %}
{{ block.super }} {{ block.super }}
@ -45,16 +42,149 @@ InvenTree | Allocate Parts
return quantity; 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({ buildTable.inventreeTable({
uniqueId: 'sub_part', uniqueId: 'sub_part',
url: "{% url 'api-bom-list' %}", url: "{% url 'api-bom-list' %}",
onPostBody: setupCallbacks,
detailViewByClick: true, detailViewByClick: true,
detailView: true, detailView: true,
detailFilter: function(index, row) { detailFilter: function(index, row) {
return row.allocations != null; return row.allocations != null;
}, },
detailFormatter: function(index, row, element) { 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 = `<div class='sub-table'><table class='table table-condensed table-striped' id='${subTableId}'></table></div>`;
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 = `<div class='btn-group float-right' role='group'>`;
{% 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 += `</div>`;
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' %}"; }, formatNoMatches: function() { return "{% trans 'No BOM items found' %}"; },
onLoadSuccess: function(tableData) { onLoadSuccess: function(tableData) {
@ -172,7 +302,7 @@ InvenTree | Allocate Parts
html += makeIconButton('fa-shopping-cart', 'button-buy', pk, '{% trans "Buy 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" %}'); html += makeIconButton('fa-tools', 'button-build', pk, '{% trans "Build parts" %}');
} }

View File

@ -1,42 +0,0 @@
{% load i18n %}
{% load inventree_extras %}
<h4>{% trans "Allocated Parts" %}</h4>
<hr>
<div id='build-item-toolbar'>
<div class='btn-group'>
<button class='btn btn-primary' type='button' id='btn-allocate' title='Allocate Stock'>{% trans "Allocate" %}</button>
<button class='btn btn-primary' type='button' id='btn-order-parts' title='Order Parts'>{% trans "Order Parts" %}</button>
</div>
</div>
<table class='table table-striped table-condensed' id='build-item-list' data-toolbar='#build-item-toolbar'></table>
<table class='table table-striped table-condensed' id='build-list' data-sorting='true'>
<thead>
<tr>
<th data-sortable='true'>{% trans "Part" %}</th>
<th>{% trans "Description" %}</th>
<th data-sortable='true'>{% trans "Available" %}</th>
<th data-sortable='true'>{% trans "Required" %}</th>
<th data-sortable='true'>{% trans "Allocated" %}</th>
<th data-sortable='true'>{% trans "On Order" %}</th>
</tr>
</thead>
<tbody>
{% for item in build.required_parts %}
<tr {% if build.status == BuildStatus.PENDING %}class='{% if item.part.total_stock > item.quantity %}rowvalid{% else %}rowinvalid{% endif %}'{% endif %}>
<td>
{% include "hover_image.html" with image=item.part.image hover=True %}
<a class='hover-icon'a href="{% url 'part-detail' item.part.id %}">{{ item.part.full_name }}</a>
</td>
<td>{{ item.part.description }}</td>
<td>{% decimal item.part.total_stock %}</td>
<td>{% decimal item.quantity %}</td>
<td>{{ item.allocated }}</td>
<td>{% decimal item.part.on_order %}</td>
</tr>
{% endfor %}
</tbody>
</table>

View File

@ -36,7 +36,7 @@ src="{% static 'img/blank_image.png' %}"
</button> </button>
{% if build.is_active %} {% if build.is_active %}
<button type='button' class='btn btn-default' id='build-complete' title="Complete Build"> <button type='button' class='btn btn-default' id='build-complete' title="Complete Build">
<span class='fas fa-paper-plane'/> <span class='fas fa-tools'/>
</button> </button>
<button type='button' class='btn btn-default btn-glyph' id='build-cancel' title='Cancel Build'> <button type='button' class='btn btn-default btn-glyph' id='build-cancel' title='Cancel Build'>
<span class='fas fa-times-circle icon-red'/> <span class='fas fa-times-circle icon-red'/>

View File

@ -396,6 +396,8 @@ class BuildCreate(AjaxCreateView):
# User has provided a Part ID # User has provided a Part ID
initials['part'] = self.request.GET.get('part', None) initials['part'] = self.request.GET.get('part', None)
initials['parent'] = self.request.GET.get('parent', None)
# User has provided a SalesOrder ID # User has provided a SalesOrder ID
initials['sales_order'] = self.request.GET.get('sales_order', None) initials['sales_order'] = self.request.GET.get('sales_order', None)
@ -540,27 +542,64 @@ class BuildItemCreate(AjaxCreateView):
build_id = self.get_param('build') build_id = self.get_param('build')
part_id = self.get_param('part') part_id = self.get_param('part')
# Reference to a Part object
part = None
# Reference to a StockItem object
item = None
# Reference to a Build object
build = None
if part_id: if part_id:
try: try:
part = Part.objects.get(pk=part_id) part = Part.objects.get(pk=part_id)
initials['part'] = part
except Part.DoesNotExist: except Part.DoesNotExist:
part = None pass
else:
part = None
if build_id: if build_id:
try: try:
build = Build.objects.get(pk=build_id) build = Build.objects.get(pk=build_id)
initials['build'] = build initials['build'] = build
# Try to work out how many parts to allocate
if part:
unallocated = build.getUnallocatedQuantity(part)
initials['quantity'] = unallocated
except Build.DoesNotExist: except Build.DoesNotExist:
pass pass
quantity = self.request.GET.get('quantity', None)
if quantity is not None:
quantity = float(quantity)
if quantity is None:
# Work out how many parts remain to be alloacted for the build
if part:
quantity = build.getUnallocatedQuantity(part)
item_id = self.get_param('item')
# If the request specifies a particular StockItem
if item_id:
try:
item = StockItem.objects.get(pk=item_id)
except:
pass
# If a StockItem is not selected, try to auto-select one
if item is None and part is not None:
items = StockItem.objects.filter(part=part)
if items.count() == 1:
item = items.first()
# Finally, if a StockItem is selected, ensure the quantity is not too much
if item is not None:
if quantity is None:
quantity = item.unallocated_quantity()
else:
quantity = min(quantity, item.unallocated_quantity())
if quantity is not None:
initials['quantity'] = quantity
return initials return initials

View File

@ -254,8 +254,6 @@ function setupCallbacks() {
var row = table.bootstrapTable('getData')[idx]; var row = table.bootstrapTable('getData')[idx];
console.log('Row ' + idx + ' - ' + row.pk + ', ' + row.quantity);
var quantity = 1; var quantity = 1;
if (row.allocated < row.quantity) { if (row.allocated < row.quantity) {

View File

@ -426,6 +426,14 @@ class StockItem(MPTTModel):
return self.build_allocation_count() + self.sales_order_allocation_count() return self.build_allocation_count() + self.sales_order_allocation_count()
def unallocated_quantity(self):
"""
Return the quantity of this StockItem which is *not* allocated
"""
return max(self.quantity - self.allocation_count(), 0)
def can_delete(self): def can_delete(self):
""" Can this stock item be deleted? It can NOT be deleted under the following circumstances: """ Can this stock item be deleted? It can NOT be deleted under the following circumstances:

View File

@ -34,6 +34,7 @@ class StockItemSerializerBrief(InvenTreeModelSerializer):
location_name = serializers.CharField(source='location', read_only=True) location_name = serializers.CharField(source='location', read_only=True)
part_name = serializers.CharField(source='part.full_name', read_only=True) part_name = serializers.CharField(source='part.full_name', read_only=True)
quantity = serializers.FloatField()
class Meta: class Meta:
model = StockItem model = StockItem