mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge pull request #370 from SchrodingersGat/variants
Part templates / variants
This commit is contained in:
commit
2bd2ffed62
19
InvenTree/build/migrations/0003_auto_20190525_2355.py
Normal file
19
InvenTree/build/migrations/0003_auto_20190525_2355.py
Normal 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'),
|
||||
),
|
||||
]
|
19
InvenTree/build/migrations/0004_auto_20190525_2356.py
Normal file
19
InvenTree/build/migrations/0004_auto_20190525_2356.py
Normal 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'),
|
||||
),
|
||||
]
|
@ -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
|
||||
},
|
||||
|
19
InvenTree/company/migrations/0004_auto_20190525_2354.py
Normal file
19
InvenTree/company/migrations/0004_auto_20190525_2354.py
Normal 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'),
|
||||
),
|
||||
]
|
19
InvenTree/company/migrations/0005_auto_20190525_2356.py
Normal file
19
InvenTree/company/migrations/0005_auto_20190525_2356.py
Normal 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'),
|
||||
),
|
||||
]
|
@ -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',
|
||||
)
|
||||
|
||||
|
@ -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>
|
||||
|
@ -93,6 +93,8 @@ class EditPartForm(HelperForm):
|
||||
'name',
|
||||
'IPN',
|
||||
'variant',
|
||||
'is_template',
|
||||
'variant_of',
|
||||
'description',
|
||||
'keywords',
|
||||
'URL',
|
||||
|
24
InvenTree/part/migrations/0003_auto_20190525_2226.py
Normal file
24
InvenTree/part/migrations/0003_auto_20190525_2226.py
Normal 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'),
|
||||
),
|
||||
]
|
24
InvenTree/part/migrations/0004_auto_20190525_2356.py
Normal file
24
InvenTree/part/migrations/0004_auto_20190525_2356.py
Normal 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'),
|
||||
),
|
||||
]
|
@ -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
|
||||
|
@ -85,6 +85,8 @@ class PartSerializer(serializers.ModelSerializer):
|
||||
'full_name',
|
||||
'name',
|
||||
'IPN',
|
||||
'is_template',
|
||||
'variant_of',
|
||||
'variant',
|
||||
'description',
|
||||
'keywords',
|
||||
|
@ -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 %}
|
||||
|
@ -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>
|
||||
|
@ -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'>
|
||||
|
@ -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 %}
|
||||
|
@ -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>
|
||||
|
||||
|
63
InvenTree/part/templates/part/variants.html
Normal file
63
InvenTree/part/templates/part/variants.html
Normal 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 %}
|
@ -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'),
|
||||
|
||||
|
@ -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>";
|
||||
|
@ -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>";
|
||||
|
@ -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,
|
||||
|
@ -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',
|
||||
|
@ -34,6 +34,7 @@ class CreateStockItemForm(HelperForm):
|
||||
'location',
|
||||
'quantity',
|
||||
'batch',
|
||||
'serial',
|
||||
'delete_on_deplete',
|
||||
'status',
|
||||
'notes',
|
||||
|
19
InvenTree/stock/migrations/0002_auto_20190525_2226.py
Normal file
19
InvenTree/stock/migrations/0002_auto_20190525_2226.py
Normal 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'),
|
||||
),
|
||||
]
|
19
InvenTree/stock/migrations/0003_auto_20190525_2303.py
Normal file
19
InvenTree/stock/migrations/0003_auto_20190525_2303.py
Normal 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'),
|
||||
),
|
||||
]
|
19
InvenTree/stock/migrations/0004_auto_20190525_2356.py
Normal file
19
InvenTree/stock/migrations/0004_auto_20190525_2356.py
Normal 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'),
|
||||
),
|
||||
]
|
@ -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')
|
||||
|
@ -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
|
||||
|
@ -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">
|
||||
|
Loading…
Reference in New Issue
Block a user