mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Add verbose names for multiple Build model fields
- Improve methods for different models
This commit is contained in:
parent
181d1d6b91
commit
01a68270ea
@ -22,6 +22,7 @@ class EditBuildForm(HelperForm):
|
||||
fields = [
|
||||
'title',
|
||||
'part',
|
||||
'parent',
|
||||
'sales_order',
|
||||
'quantity',
|
||||
'take_from',
|
||||
|
71
InvenTree/build/migrations/0014_auto_20200425_1243.py
Normal file
71
InvenTree/build/migrations/0014_auto_20200425_1243.py
Normal 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'),
|
||||
),
|
||||
]
|
@ -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.
|
||||
|
@ -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" %}');
|
||||
}
|
||||
|
||||
|
@ -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>
|
@ -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'/>
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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:
|
||||
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user