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 = [
'title',
'part',
'parent',
'sales_order',
'quantity',
'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.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,
status = models.PositiveIntegerField(
verbose_name=_('Build Status'),
default=BuildStatus.PENDING,
choices=BuildStatus.items(),
validators=[MinValueValidator(0)],
help_text=_('Build status'))
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,
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 <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']
if q:
return int(q)
else:
return 0
return allocated['q']
def getUnallocatedQuantity(self, part):
""" Calculate the quantity of <part> which still needs to be allocated to this build.

View File

@ -11,18 +11,15 @@ InvenTree | Allocate Parts
{% include "build/tabs.html" with tab='allocate' %}
{% if editing %}
{% include "build/allocate_edit.html" %}
{% else %}
{% include "build/allocate_view.html" %}
{% endif %}
<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>
{% endblock %}
<table class='table table-striped table-condensed' id='build-item-list' data-toolbar='#build-item-toolbar'></table>
{% 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 %}
@ -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 = `<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' %}"; },
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" %}');
}

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>
{% if build.is_active %}
<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 type='button' class='btn btn-default btn-glyph' id='build-cancel' title='Cancel Build'>
<span class='fas fa-times-circle icon-red'/>

View File

@ -396,6 +396,8 @@ class BuildCreate(AjaxCreateView):
# User has provided a Part ID
initials['part'] = self.request.GET.get('part', None)
initials['parent'] = self.request.GET.get('parent', None)
# User has provided a SalesOrder ID
initials['sales_order'] = self.request.GET.get('sales_order', None)
@ -540,27 +542,64 @@ class BuildItemCreate(AjaxCreateView):
build_id = self.get_param('build')
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:
try:
part = Part.objects.get(pk=part_id)
initials['part'] = part
except Part.DoesNotExist:
part = None
else:
part = None
pass
if build_id:
try:
build = Build.objects.get(pk=build_id)
initials['build'] = build
# Try to work out how many parts to allocate
if part:
unallocated = build.getUnallocatedQuantity(part)
initials['quantity'] = unallocated
except Build.DoesNotExist:
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

View File

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

View File

@ -426,6 +426,14 @@ class StockItem(MPTTModel):
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):
""" 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)
part_name = serializers.CharField(source='part.full_name', read_only=True)
quantity = serializers.FloatField()
class Meta:
model = StockItem