Merge pull request #370 from SchrodingersGat/variants

Part templates / variants
This commit is contained in:
Oliver 2019-05-26 00:48:14 +10:00 committed by GitHub
commit 2bd2ffed62
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 433 additions and 34 deletions

View File

@ -0,0 +1,19 @@
# Generated by Django 2.2 on 2019-05-25 13:55
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('build', '0002_auto_20190520_2204'),
]
operations = [
migrations.AlterField(
model_name='build',
name='part',
field=models.ForeignKey(help_text='Select part to build', limit_choices_to={'active': True, 'buildable': True, 'has_variants': False}, on_delete=django.db.models.deletion.CASCADE, related_name='builds', to='part.Part'),
),
]

View File

@ -0,0 +1,19 @@
# Generated by Django 2.2 on 2019-05-25 13:56
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('build', '0003_auto_20190525_2355'),
]
operations = [
migrations.AlterField(
model_name='build',
name='part',
field=models.ForeignKey(help_text='Select part to build', limit_choices_to={'active': True, 'buildable': True, 'is_template': False}, on_delete=django.db.models.deletion.CASCADE, related_name='builds', to='part.Part'),
),
]

View File

@ -50,6 +50,7 @@ class Build(models.Model):
part = models.ForeignKey('part.Part', on_delete=models.CASCADE,
related_name='builds',
limit_choices_to={
'is_template': False,
'buildable': True,
'active': True
},

View File

@ -0,0 +1,19 @@
# Generated by Django 2.2 on 2019-05-25 13:54
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('company', '0003_remove_supplierpart_minimum'),
]
operations = [
migrations.AlterField(
model_name='supplierpart',
name='part',
field=models.ForeignKey(help_text='Select part', limit_choices_to={'has_variants': False, 'purchaseable': True}, on_delete=django.db.models.deletion.CASCADE, related_name='supplier_parts', to='part.Part'),
),
]

View File

@ -0,0 +1,19 @@
# Generated by Django 2.2 on 2019-05-25 13:56
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('company', '0004_auto_20190525_2354'),
]
operations = [
migrations.AlterField(
model_name='supplierpart',
name='part',
field=models.ForeignKey(help_text='Select part', limit_choices_to={'is_template': False, 'purchaseable': True}, on_delete=django.db.models.deletion.CASCADE, related_name='supplier_parts', to='part.Part'),
),
]

View File

@ -188,7 +188,10 @@ class SupplierPart(models.Model):
part = models.ForeignKey('part.Part', on_delete=models.CASCADE,
related_name='supplier_parts',
limit_choices_to={'purchaseable': True},
limit_choices_to={
'purchaseable': True,
'is_template': False,
},
help_text='Select part',
)

View File

@ -84,7 +84,7 @@ InvenTree | {{ company.name }} - Parts
<td>{{ pb.quantity }}</td>
<td>{{ pb.cost }}
<div class='btn-group' style='float: right;'>
<button title='Edit Price Break' class='btn btn-primary pb-edit-button btn-sm' type='button' url="{% url 'price-break-edit' pb.id %}"><span class='glyphicon glyphicon-small glyphicon-pencil'></span></button>
<button title='Edit Price Break' class='btn btn-primary pb-edit-button btn-sm' type='button' url="{% url 'price-break-edit' pb.id %}"><span class='glyphicon glyphicon-small glyphicon-edit'></span></button>
<button title='Delete Price Break' class='btn btn-danger pb-delete-button btn-sm' type='button' url="{% url 'price-break-delete' pb.id %}"><span class='glyphicon glyphicon-small glyphicon-trash'></span></button>
</div>
</td>

View File

@ -93,6 +93,8 @@ class EditPartForm(HelperForm):
'name',
'IPN',
'variant',
'is_template',
'variant_of',
'description',
'keywords',
'URL',

View File

@ -0,0 +1,24 @@
# Generated by Django 2.2 on 2019-05-25 12:26
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('part', '0002_auto_20190520_2204'),
]
operations = [
migrations.AddField(
model_name='part',
name='has_variants',
field=models.BooleanField(default=False, help_text='Is this part a template part?'),
),
migrations.AddField(
model_name='part',
name='variant_of',
field=models.ForeignKey(blank=True, help_text='Is this part a variant of another part?', limit_choices_to={'active': True, 'has_variants': True}, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='variants', to='part.Part'),
),
]

View File

@ -0,0 +1,24 @@
# Generated by Django 2.2 on 2019-05-25 13:56
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('part', '0003_auto_20190525_2226'),
]
operations = [
migrations.RenameField(
model_name='part',
old_name='has_variants',
new_name='is_template',
),
migrations.AlterField(
model_name='part',
name='variant_of',
field=models.ForeignKey(blank=True, help_text='Is this part a variant of another part?', limit_choices_to={'active': True, 'is_template': True}, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='variants', to='part.Part'),
),
]

View File

@ -192,6 +192,7 @@ class Part(models.Model):
description: Longer form description of the part
keywords: Optional keywords for improving part search results
IPN: Internal part number (optional)
is_template: If True, this part is a 'template' part and cannot be instantiated as a StockItem
URL: Link to an external page with more information about this part (e.g. internal Wiki)
image: Image of this part
default_location: Where the item is normally stored (may be null)
@ -252,12 +253,32 @@ class Part(models.Model):
else:
return static('/img/blank_image.png')
def clean(self):
""" Perform cleaning operations for the Part model """
if self.is_template and self.variant_of is not None:
raise ValidationError({
'is_template': _("Part cannot be a template part if it is a variant of another part"),
'variant_of': _("Part cannot be a variant of another part if it is already a template"),
})
name = models.CharField(max_length=100, blank=False, help_text='Part name',
validators=[validators.validate_part_name]
)
variant = models.CharField(max_length=32, blank=True, help_text='Part variant or revision code')
is_template = models.BooleanField(default=False, help_text='Is this part a template part?')
variant_of = models.ForeignKey('part.Part', related_name='variants',
null=True, blank=True,
limit_choices_to={
'is_template': True,
'active': True,
},
on_delete=models.SET_NULL,
help_text='Is this part a variant of another part?')
description = models.CharField(max_length=250, blank=False, help_text='Part description')
keywords = models.CharField(max_length=250, blank=True, help_text='Part keywords to improve visibility in search results')
@ -501,7 +522,10 @@ class Part(models.Model):
Part may be stored in multiple locations
"""
total = self.stock_entries.aggregate(total=Sum('quantity'))['total']
if self.is_template:
total = sum([variant.total_stock for variant in self.variants.all()])
else:
total = self.stock_entries.aggregate(total=Sum('quantity'))['total']
if total:
return total
@ -747,6 +771,21 @@ class Part(models.Model):
return data.export(file_format)
@property
def attachment_count(self):
""" Count the number of attachments for this part.
If the part is a variant of a template part,
include the number of attachments for the template part.
"""
n = self.attachments.count()
if self.variant_of:
n += self.variant_of.attachments.count()
return n
def attach_file(instance, filename):
""" Function for storing a file for a PartAttachment

View File

@ -85,6 +85,8 @@ class PartSerializer(serializers.ModelSerializer):
'full_name',
'name',
'IPN',
'is_template',
'variant_of',
'variant',
'description',
'keywords',

View File

@ -30,12 +30,26 @@
<td>{{ attachment.comment }}</td>
<td>
<div class='btn-group' style='float: right;'>
<button type='button' class='btn btn-primary attachment-edit-button' url="{% url 'part-attachment-edit' attachment.id %}" data-toggle='tooltip' title='Edit attachment ({{ attachment.basename }})'><span class='glyphicon glyphicon-small glyphicon-pencil'></span></button>
<button type='button' class='btn btn-primary attachment-edit-button' url="{% url 'part-attachment-edit' attachment.id %}" data-toggle='tooltip' title='Edit attachment ({{ attachment.basename }})'><span class='glyphicon glyphicon-small glyphicon-edit'></span></button>
<button type='button' class='btn btn-danger attachment-delete-button' url="{% url 'part-attachment-delete' attachment.id %}" data-toggle='tooltip' title='Delete attachment ({{ attachment.basename }})'><span class='glyphicon glyphicon-small glyphicon-trash'></span></button>
</div>
</td>
</tr>
{% endfor %}
{% if part.variant_of and part.variant_of.attachments.count > 0 %}
<tr>
<td colspan='3'>
Attachments for template part <b><i>{{ part.variant_of.full_name }}</i></b>
</td>
</tr>
{% for attachment in part.variant_of.attachments.all %}
<tr>
<td><a href='/media/{{ attachment.attachment }}'>{{ attachment.basename }}</a></td>
<td>{{ attachment.comment }}</td>
<td></td>
</tr>
{% endfor %}
{% endif %}
</table>
{% endblock %}

View File

@ -52,6 +52,12 @@
<td><b>Description</b></td>
<td>{{ part.description }}</td>
</tr>
{% if part.variant_of %}
<tr>
<td><b>Variant Of</b></td>
<td><a href="{% url 'part-detail' part.variant_of.id %}">{{ part.variant_of.full_name }}</a></td>
</tr>
{% endif %}
{% if part.keywords %}
<tr>
<td><b>Keywords</b></td>

View File

@ -4,12 +4,24 @@
{% block content %}
{% if part.active == False %}
<div class='alert alert-danger alert-block'>
This part is not active:
</div>
{% endif %}
{% if part.is_template %}
<div class='alert alert-info alert-block'>
This part is a <i>template part</i>.<br>
It is not a <i>real</i> part, but real parts can be based on this template.
</div>
{% endif %}
{% if part.variant_of %}
<div class='alert alert-info alert-block'>
This part is a variant of <b><a href="{% url 'part-detail' part.variant_of.id %}">{{ part.variant_of.full_name }}</a></b>
</div>
{% endif %}
<div class="row">
{% if part.active == False %}
<div class='alert alert-danger' style='display: block;'>
This part ({{ part.full_name }}) is not active:
</div>
{% endif %}
<div class="col-sm-6">
<div class="media">
<div class="media-left">
@ -35,10 +47,12 @@
<button type='button' class='btn btn-default btn-glyph' id='toggle-starred' title='Star this part'>
<span id='part-star-icon' class='starred-part glyphicon {% if starred %}glyphicon-star{% else %}glyphicon-star-empty{% endif %}'/>
</button>
{% if part.is_template == False %}
{% include "qr_button.html" %}
<button type='button' class='btn btn-default btn-glyph' id='price-button' title='Show pricing information'>
<span id='part-price-icon' class='part-price glyphicon glyphicon-usd'/>
</button>
{% endif %}
</div>
</p>
<table class='table table-condensed'>

View File

@ -13,6 +13,12 @@
</div>
<hr>
{% if part.is_template %}
<div class='alert alert-info alert-block'>
Showing stock for all variants of <i>{{ part.full_name }}</i>
</div>
{% endif %}
{% include "stock_table.html" %}
{% endblock %}

View File

@ -2,6 +2,11 @@
<li{% ifequal tab 'detail' %} class="active"{% endifequal %}>
<a href="{% url 'part-detail' part.id %}">Details</a>
</li>
{% if part.is_template %}
<li{% ifequal tab 'variants' %} class='active'{% endifequal %}>
<a href="{% url 'part-variants' part.id %}">Variants <span class='badge'>{{ part.variants.count }}</span></span></a>
</li>
{% endif %}
<li{% ifequal tab 'stock' %} class="active"{% endifequal %}>
<a href="{% url 'part-stock' part.id %}">Stock <span class="badge">{{ part.total_stock }}</span></a>
</li>
@ -20,7 +25,7 @@
<li{% ifequal tab 'used' %} class="active"{% endifequal %}>
<a href="{% url 'part-used-in' part.id %}">Used In{% if part.used_in_count > 0 %}<span class="badge">{{ part.used_in_count }}</span>{% endif %}</a></li>
{% endif %}
{% if part.purchaseable %}
{% if part.purchaseable and part.is_template == False %}
<li{% ifequal tab 'suppliers' %} class="active"{% endifequal %}>
<a href="{% url 'part-suppliers' part.id %}">Suppliers
<span class="badge">{{ part.supplier_count }}</span>
@ -35,7 +40,7 @@
</a></li>
{% endif %}
<li{% ifequal tab 'attachments' %} class="active"{% endifequal %}>
<a href="{% url 'part-attachments' part.id %}">Attachments {% if part.attachments.all|length > 0 %}<span class="badge">{{ part.attachments.all|length }}</span>{% endif %}</a>
<a href="{% url 'part-attachments' part.id %}">Attachments {% if part.attachment_count > 0 %}<span class="badge">{{ part.attachment_count }}</span>{% endif %}</a>
</li>
</ul>

View File

@ -0,0 +1,63 @@
{% extends "part/part_base.html" %}
{% load static %}
{% block details %}
{% include "part/tabs.html" with tab='variants' %}
<div class='row'>
<div class='col-sm-6'>
<h4>Part Variants</h4>
</div>
<div class='col-sm-6'>
</div>
</div>
<hr>
<div id='button-toolbar'>
<div class='btn-group'>
{% if part.is_template and part.active %}
<button class='btn btn-success' id='new-variant' title='Create new variant'>New Variant</button>
{% endif %}
</div>
</div>
<table class='table table-striped table-condensed' id='variant-table' data-toolbar='#button-toolbar'>
<thead>
<tr>
<th>Variant</th>
<th>Description</th>
<th>Stock</th>
</tr>
</thead>
<tbody>
{% for variant in part.variants.all %}
<tr>
<td>
<div class='hover-icon media-left' style='float: left;'>
<img class='hover-img-thumb' src="{% if variant.image %}{{ variant.image.url }}{% else %}{% static 'img/blank_image.png' %}{% endif %}">
{% if variant.image %}
<img class='hover-img-large' src="{{ variant.image.url }}">
{% endif %}
</div>
<a href="{% url 'part-detail' variant.id %}">{{ variant.full_name }}</a>
</td>
<td>{{ variant.description }}</td>
<td>{{ variant.total_stock }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}
{% block js_ready %}
{{ block.super }}
$('#variant-table').bootstrapTable({
search: true,
sortable: true,
});
{% endblock %}

View File

@ -26,14 +26,15 @@ part_detail_urls = [
url(r'^duplicate/', views.PartDuplicate.as_view(), name='part-duplicate'),
url(r'^pricing/', views.PartPricing.as_view(), name='part-pricing'),
url(r'^track/?', views.PartDetail.as_view(template_name='part/track.html'), name='part-track'),
url(r'^attachments/?', views.PartDetail.as_view(template_name='part/attachments.html'), name='part-attachments'),
url(r'^variants/?', views.PartDetail.as_view(template_name='part/variants.html'), name='part-variants'),
url(r'^stock/?', views.PartDetail.as_view(template_name='part/stock.html'), name='part-stock'),
url(r'^allocation/?', views.PartDetail.as_view(template_name='part/allocation.html'), name='part-allocation'),
url(r'^bom/?', views.PartDetail.as_view(template_name='part/bom.html'), name='part-bom'),
url(r'^build/?', views.PartDetail.as_view(template_name='part/build.html'), name='part-build'),
url(r'^stock/?', views.PartDetail.as_view(template_name='part/stock.html'), name='part-stock'),
url(r'^used/?', views.PartDetail.as_view(template_name='part/used_in.html'), name='part-used-in'),
url(r'^allocation/?', views.PartDetail.as_view(template_name='part/allocation.html'), name='part-allocation'),
url(r'^suppliers/?', views.PartDetail.as_view(template_name='part/supplier.html'), name='part-suppliers'),
url(r'^track/?', views.PartDetail.as_view(template_name='part/track.html'), name='part-track'),
url(r'^attachments/?', views.PartDetail.as_view(template_name='part/attachments.html'), name='part-attachments'),
url(r'^qr_code/?', views.PartQRCode.as_view(), name='part-qr'),

View File

@ -189,7 +189,7 @@ function loadBomTable(table, options) {
if (options.editable) {
cols.push({
formatter: function(value, row, index, field) {
var bEdit = "<button title='Edit BOM Item' class='btn btn-primary bom-edit-button btn-sm' type='button' url='/part/bom/" + row.pk + "/edit'><span class='glyphicon glyphicon-small glyphicon-pencil'></span></button>";
var bEdit = "<button title='Edit BOM Item' class='btn btn-primary bom-edit-button btn-sm' type='button' url='/part/bom/" + row.pk + "/edit'><span class='glyphicon glyphicon-small glyphicon-edit'></span></button>";
var bDelt = "<button title='Delete BOM Item' class='btn btn-danger bom-delete-button btn-sm' type='button' url='/part/bom/" + row.pk + "/delete'><span class='glyphicon glyphicon-small glyphicon-trash'></span></button>";
return "<div class='btn-group'>" + bEdit + bDelt + "</div>";

View File

@ -40,7 +40,7 @@ function loadAllocationTable(table, part_id, part, url, required, button) {
formatter: function(value, row, index, field) {
var html = value;
var bEdit = "<button class='btn btn-primary item-edit-button btn-sm' type='button' title='Edit stock allocation' url='/build/item/" + row.pk + "/edit/'><span class='glyphicon glyphicon-small glyphicon-pencil'></span></button>";
var bEdit = "<button class='btn btn-primary item-edit-button btn-sm' type='button' title='Edit stock allocation' url='/build/item/" + row.pk + "/edit/'><span class='glyphicon glyphicon-small glyphicon-edit'></span></button>";
var bDel = "<button class='btn btn-danger item-del-button btn-sm' type='button' title='Delete stock allocation' url='/build/item/" + row.pk + "/delete/'><span class='glyphicon glyphicon-small glyphicon-trash'></span></button>";
html += "<div class='btn-group' style='float: right;'>" + bEdit + bDel + "</div>";

View File

@ -124,7 +124,12 @@ function loadPartTable(table, url, options={}) {
sortable: true,
formatter: function(value, row, index, field) {
if (row.is_template) {
value = '<i>' + value + '</i>';
}
var display = imageHoverIcon(row.image_url) + renderLink(value, row.url);
if (!row.active) {
display = display + "<span class='label label-warning' style='float: right;'>INACTIVE</span>";
}
@ -135,6 +140,14 @@ function loadPartTable(table, url, options={}) {
sortable: true,
field: 'description',
title: 'Description',
formatter: function(value, row, index, field) {
if (row.is_template) {
value = '<i>' + value + '</i>';
}
return value;
}
},
{
sortable: true,

View File

@ -11,7 +11,7 @@ from django.urls import reverse
from .models import StockLocation, StockItem
from .models import StockItemTracking
from part.models import PartCategory
from part.models import Part, PartCategory
from .serializers import StockItemSerializer, StockQuantitySerializer
from .serializers import LocationSerializer
@ -263,12 +263,28 @@ class StockList(generics.ListCreateAPIView):
we may wish to also request stock items from all child locations.
"""
# Does the client wish to filter by stock location?
loc_id = self.request.query_params.get('location', None)
# Start with all objects
stock_list = StockItem.objects.all()
# Does the client wish to filter by the Part ID?
part_id = self.request.query_params.get('part', None)
if part_id:
try:
part = Part.objects.get(pk=part_id)
# If the part is a Template part, select stock items for any "variant" parts under that template
if part.is_template:
stock_list = stock_list.filter(part__in=[part.id for part in Part.objects.filter(variant_of=part_id)])
else:
stock_list = stock_list.filter(part=part_id)
except Part.DoesNotExist:
pass
# Does the client wish to filter by stock location?
loc_id = self.request.query_params.get('location', None)
if loc_id:
try:
location = StockLocation.objects.get(pk=loc_id)
@ -312,7 +328,6 @@ class StockList(generics.ListCreateAPIView):
]
filter_fields = [
'part',
'supplier_part',
'customer',
'belongs_to',

View File

@ -34,6 +34,7 @@ class CreateStockItemForm(HelperForm):
'location',
'quantity',
'batch',
'serial',
'delete_on_deplete',
'status',
'notes',

View File

@ -0,0 +1,19 @@
# Generated by Django 2.2 on 2019-05-25 12:26
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('stock', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='stockitem',
name='part',
field=models.ForeignKey(help_text='Base part', limit_choices_to={'active': True, 'has_variants': True}, on_delete=django.db.models.deletion.CASCADE, related_name='stock_items', to='part.Part'),
),
]

View File

@ -0,0 +1,19 @@
# Generated by Django 2.2 on 2019-05-25 13:03
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('stock', '0002_auto_20190525_2226'),
]
operations = [
migrations.AlterField(
model_name='stockitem',
name='part',
field=models.ForeignKey(help_text='Base part', limit_choices_to={'active': True, 'has_variants': False}, on_delete=django.db.models.deletion.CASCADE, related_name='stock_items', to='part.Part'),
),
]

View File

@ -0,0 +1,19 @@
# Generated by Django 2.2 on 2019-05-25 13:56
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('stock', '0003_auto_20190525_2303'),
]
operations = [
migrations.AlterField(
model_name='stockitem',
name='part',
field=models.ForeignKey(help_text='Base part', limit_choices_to={'active': True, 'is_template': False}, on_delete=django.db.models.deletion.CASCADE, related_name='stock_items', to='part.Part'),
),
]

View File

@ -115,6 +115,22 @@ class StockItem(models.Model):
system=True
)
def validate_unique(self, exclude=None):
super(StockItem, self).validate_unique(exclude)
# If the Part object is a variant (of a template part),
# ensure that the serial number is unique
# across all variants of the same template part
try:
if self.serial is not None and self.part.variant_of is not None:
if StockItem.objects.filter(part__variant_of=self.part.variant_of, serial=self.serial).exclude(id=self.id).exists():
raise ValidationError({
'serial': _('A part with this serial number already exists for template part {part}'.format(part=self.part.variant_of))
})
except Part.DoesNotExist:
pass
def clean(self):
""" Validate the StockItem object (separate to field validation)
@ -135,11 +151,18 @@ class StockItem(models.Model):
})
if self.part is not None:
# A trackable part must have a serial number
if self.part.trackable and not self.serial:
raise ValidationError({
'serial': _('Serial number must be set for trackable items')
})
# A template part cannot be instantiated as a StockItem
if self.part.is_template:
raise ValidationError({
'part': _('Stock item cannot be created for a template Part')
})
except Part.DoesNotExist:
# This gets thrown if self.supplier_part is null
# TODO - Find a test than can be perfomed...
@ -186,7 +209,12 @@ class StockItem(models.Model):
}
)
part = models.ForeignKey('part.Part', on_delete=models.CASCADE, related_name='stock_items', help_text='Base part')
part = models.ForeignKey('part.Part', on_delete=models.CASCADE,
related_name='stock_items', help_text='Base part',
limit_choices_to={
'is_template': False,
'active': True,
})
supplier_part = models.ForeignKey('company.SupplierPart', blank=True, null=True, on_delete=models.SET_NULL,
help_text='Select a matching supplier part for this stock item')

View File

@ -208,28 +208,31 @@ class StockItemCreate(AjaxCreateView):
try:
part = Part.objects.get(id=part_id)
parts = form.fields['supplier_part'].queryset
parts = parts.filter(part=part.id)
# Hide the 'part' field (as a valid part is selected)
form.fields['part'].widget = HiddenInput()
# If the part is NOT purchaseable, hide the supplier_part field
if not part.purchaseable:
form.fields['supplier_part'].widget = HiddenInput()
form.fields['supplier_part'].queryset = parts
else:
# Pre-select the allowable SupplierPart options
parts = form.fields['supplier_part'].queryset
parts = parts.filter(part=part.id)
# If there is one (and only one) supplier part available, pre-select it
all_parts = parts.all()
if len(all_parts) == 1:
form.fields['supplier_part'].queryset = parts
# TODO - This does NOT work for some reason? Ref build.views.BuildItemCreate
form.fields['supplier_part'].initial = all_parts[0].id
# If there is one (and only one) supplier part available, pre-select it
all_parts = parts.all()
if len(all_parts) == 1:
# TODO - This does NOT work for some reason? Ref build.views.BuildItemCreate
form.fields['supplier_part'].initial = all_parts[0].id
except Part.DoesNotExist:
pass
# Hide the 'part' field
form.fields['part'].widget = HiddenInput()
# Otherwise if the user has selected a SupplierPart, we know what Part they meant!
elif form['supplier_part'].value() is not None:
pass

View File

@ -1,6 +1,8 @@
<div id='button-toolbar'>
<div class='button-toolbar container-fluid' style='float: right;'>
{% if part.is_template == False %}
<button class="btn btn-success" id='item-create'>New Stock Item</button>
{% endif %}
<div class="dropdown" style='float: right;'>
<button id='stock-options' class="btn btn-primary dropdown-toggle" type="button" data-toggle="dropdown">Options<span class="caret"></span></button>
<ul class="dropdown-menu">