Merge remote-tracking branch 'inventree/master'

This commit is contained in:
Oliver Walters 2019-05-12 16:39:02 +10:00
commit 142a7659bd
43 changed files with 281 additions and 101 deletions

View File

@ -225,7 +225,7 @@ class Build(models.Model):
'Removed {n} items to build {m} x {part}'.format( 'Removed {n} items to build {m} x {part}'.format(
n=item.quantity, n=item.quantity,
m=self.quantity, m=self.quantity,
part=self.part.name part=self.part.full_name
) )
) )
@ -370,7 +370,7 @@ class BuildItem(models.Model):
if self.stock_item is not None and self.stock_item.part is not None: if self.stock_item is not None and self.stock_item.part is not None:
if self.stock_item.part not in self.build.part.required_parts(): if self.stock_item.part not in self.build.part.required_parts():
errors['stock_item'] = [_("Selected stock item not found in BOM for part '{p}'".format(p=self.build.part.name))] errors['stock_item'] = [_("Selected stock item not found in BOM for part '{p}'".format(p=self.build.part.full_name))]
if self.stock_item is not None and self.quantity > self.stock_item.quantity: if self.stock_item is not None and self.quantity > self.stock_item.quantity:
errors['quantity'] = [_("Allocated quantity ({n}) must not exceed available quantity ({q})".format( errors['quantity'] = [_("Allocated quantity ({n}) must not exceed available quantity ({q})".format(

View File

@ -44,7 +44,7 @@ class BuildItemSerializer(InvenTreeModelSerializer):
""" Serializes a BuildItem object """ """ Serializes a BuildItem object """
part = serializers.IntegerField(source='stock_item.part.pk', read_only=True) part = serializers.IntegerField(source='stock_item.part.pk', read_only=True)
part_name = serializers.CharField(source='stock_item.part.name', read_only=True) part_name = serializers.CharField(source='stock_item.part.full_name', read_only=True)
stock_item_detail = StockItemSerializerBrief(source='stock_item', read_only=True) stock_item_detail = StockItemSerializerBrief(source='stock_item', read_only=True)
class Meta: class Meta:

View File

@ -14,7 +14,7 @@ InvenTree | Allocate Parts
<div class='col-sm-6'> <div class='col-sm-6'>
<h4><a href="{% url 'build-detail' build.id %}">{{ build.title }}</a></h4> <h4><a href="{% url 'build-detail' build.id %}">{{ build.title }}</a></h4>
{{ build.quantity }} x {{ build.part.name }} {{ build.quantity }} x {{ build.part.lonname }}
</div> </div>
<div class='col-sm-6'> <div class='col-sm-6'>
<div class='btn-group' style='float: right;'> <div class='btn-group' style='float: right;'>
@ -47,7 +47,7 @@ InvenTree | Allocate Parts
loadAllocationTable( loadAllocationTable(
$("#allocate-table-id-{{ bom_item.sub_part.id }}"), $("#allocate-table-id-{{ bom_item.sub_part.id }}"),
{{ bom_item.sub_part.id }}, {{ bom_item.sub_part.id }},
"{{ bom_item.sub_part.name }}", "{{ bom_item.sub_part.full_name }}",
"{% url 'api-build-item-list' %}?build={{ build.id }}&part={{ bom_item.sub_part.id }}", "{% url 'api-build-item-list' %}?build={{ build.id }}&part={{ bom_item.sub_part.id }}",
{% multiply build.quantity bom_item.quantity %}, {% multiply build.quantity bom_item.quantity %},
$("#new-item-{{ bom_item.sub_part.id }}") $("#new-item-{{ bom_item.sub_part.id }}")

View File

@ -9,7 +9,7 @@
<img class='hover-img-large' src="{% if item.sub_part.image %}{{ item.sub_part.image.url }}{% endif %}"> <img class='hover-img-large' src="{% if item.sub_part.image %}{{ item.sub_part.image.url }}{% endif %}">
</div> </div>
<div> <div>
{{ item.sub_part.name }}<br> {{ item.sub_part.full_name }}<br>
<small><i>{{ item.sub_part.description }}</i></small> <small><i>{{ item.sub_part.description }}</i></small>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -4,7 +4,7 @@
{{ block.super }} {{ block.super }}
<b>Build: {{ build.title }}</b> - {{ build.quantity }} x {{ build.part.name }} <b>Build: {{ build.title }}</b> - {{ build.quantity }} x {{ build.part.full_name }}
<br><br> <br><br>
Automatically allocate stock to this build? Automatically allocate stock to this build?
<hr> <hr>
@ -27,7 +27,7 @@ Automatically allocate stock to this build?
</a> </a>
</td> </td>
<td> <td>
{{ item.stock_item.part.name }}<br> {{ item.stock_item.part.full_name }}<br>
<i>{{ item.stock_item.part.description }}</i> <i>{{ item.stock_item.part.description }}</i>
</td> </td>
<td>{{ item.quantity }}</td> <td>{{ item.quantity }}</td>

View File

@ -23,7 +23,7 @@
{% for build in builds %} {% for build in builds %}
<tr> <tr>
<td><a href="{% url 'build-detail' build.id %}">{{ build.title }}</a></td> <td><a href="{% url 'build-detail' build.id %}">{{ build.title }}</a></td>
<td><a href="{% url 'part-build' build.part.id %}">{{ build.part.name }}</a></td> <td><a href="{% url 'part-build' build.part.id %}">{{ build.part.full_name }}</a></td>
<td>{{ build.quantity }}</td> <td>{{ build.quantity }}</td>
<td>{% include "build_status.html" with build=build %} <td>{% include "build_status.html" with build=build %}
{% if completed %} {% if completed %}

View File

@ -1,7 +1,7 @@
{% extends "modal_form.html" %} {% extends "modal_form.html" %}
{% block pre_form_content %} {% block pre_form_content %}
<b>Build: {{ build.title }}</b> - {{ build.quantity }} x {{ build.part.name }} <b>Build: {{ build.title }}</b> - {{ build.quantity }} x {{ build.part.full_name }}
<br> <br>
Are you sure you want to mark this build as complete? Are you sure you want to mark this build as complete?
<hr> <hr>
@ -24,7 +24,7 @@ The following items will be removed from stock:
</a> </a>
</td> </td>
<td> <td>
{{ item.stock_item.part.name }}<br> {{ item.stock_item.part.full_name }}<br>
<i>{{ item.stock_item.part.description }}</i> <i>{{ item.stock_item.part.description }}</i>
</td> </td>
<td>{{ item.quantity }}</td> <td>{{ item.quantity }}</td>
@ -42,7 +42,7 @@ The following items will be created:
<img class='hover-img-thumb' src='{{ build.part.image.url }}'> <img class='hover-img-thumb' src='{{ build.part.image.url }}'>
<img class='hover-img-large' src='{{ build.part.image.url }}'> <img class='hover-img-large' src='{{ build.part.image.url }}'>
</a> </a>
{{ build.quantity }} x {{ build.part.name }} {{ build.quantity }} x {{ build.part.full_name }}
</div> </div>
{% endblock %} {% endblock %}

View File

@ -11,7 +11,7 @@ InvenTree | Build - {{ build }}
<div class='col-sm-6'> <div class='col-sm-6'>
<h3>Build Details</h3> <h3>Build Details</h3>
<p><b>{{ build.title }}</b>{% include "build_status.html" with build=build %}</p> <p><b>{{ build.title }}</b>{% include "build_status.html" with build=build %}</p>
<p>Building {{ build.quantity }} &times {{ build.part.name }}</p> <p>Building {{ build.quantity }} &times {{ build.part.full_name }}</p>
</div> </div>
<div class='col-sm-6'> <div class='col-sm-6'>
<h3> <h3>
@ -40,7 +40,7 @@ InvenTree | Build - {{ build }}
<td>Title</td><td>{{ build.title }}</td> <td>Title</td><td>{{ build.title }}</td>
</tr> </tr>
<tr> <tr>
<td>Part</td><td><a href="{% url 'part-build' build.part.id %}">{{ build.part.name }}</a></td> <td>Part</td><td><a href="{% url 'part-build' build.part.id %}">{{ build.part.full_name }}</a></td>
</tr> </tr>
<tr> <tr>
<td>Quantity</td><td>{{ build.quantity }}</td> <td>Quantity</td><td>{{ build.quantity }}</td>
@ -109,7 +109,7 @@ InvenTree | Build - {{ build }}
<tbody> <tbody>
{% for item in build.required_parts %} {% for item in build.required_parts %}
<tr> <tr>
<td><a href="{% url 'part-detail' item.part.id %}">{{ item.part.name }}</a></td> <td><a href="{% url 'part-detail' item.part.id %}">{{ item.part.full_name }}</a></td>
<td>{{ item.quantity }}</td> <td>{{ item.quantity }}</td>
<td>{{ item.part.total_stock }}</td> <td>{{ item.part.total_stock }}</td>
<td>{{ item.allocated }}</td> <td>{{ item.allocated }}</td>

View File

@ -7,7 +7,7 @@ Are you sure you want to delete company '{{ company.name }}'?
If this supplier is deleted, these supplier part entries will also be deleted.</p> If this supplier is deleted, these supplier part entries will also be deleted.</p>
<ul class='list-group'> <ul class='list-group'>
{% for part in company.parts.all %} {% for part in company.parts.all %}
<li class='list-group-item'><b>{{ part.SKU }}</b> - <i>{{ part.part.name }}</i></li> <li class='list-group-item'><b>{{ part.SKU }}</b> - <i>{{ part.part.full_name }}</i></li>
{% endfor %} {% endfor %}
</ul> </ul>
{% endif %} {% endif %}

View File

@ -51,7 +51,7 @@
}, },
{ {
sortable: true, sortable: true,
field: 'part_detail.name', field: 'part_detail.full_name',
title: 'Part', title: 'Part',
formatter: function(value, row, index, field) { formatter: function(value, row, index, field) {
return imageHoverIcon(row.part_detail.image_url) + renderLink(value, '/part/' + row.part + '/suppliers/'); return imageHoverIcon(row.part_detail.image_url) + renderLink(value, '/part/' + row.part + '/suppliers/');
@ -74,7 +74,18 @@
sortable: true, sortable: true,
field: 'MPN', field: 'MPN',
title: 'MPN', title: 'MPN',
} },
{
field: 'URL',
title: 'URL',
formatter: function(value, row, index, field) {
if (value) {
return renderLink(value, value);
} else {
return '';
}
}
},
], ],
url: "{% url 'api-part-supplier-list' %}" url: "{% url 'api-part-supplier-list' %}"
}); });

View File

@ -37,7 +37,7 @@ InvenTree | {{ company.name }} - Parts
<td>Internal Part</td> <td>Internal Part</td>
<td> <td>
{% if part.part %} {% if part.part %}
<a href="{% url 'part-suppliers' part.part.id %}">{{ part.part.name }}</a> <a href="{% url 'part-suppliers' part.part.id %}">{{ part.part.full_name }}</a>
{% endif %} {% endif %}
</td> </td>
</tr> </tr>

View File

@ -9,7 +9,7 @@ from .models import BomItem
class PartAdmin(ImportExportModelAdmin): class PartAdmin(ImportExportModelAdmin):
list_display = ('long_name', 'IPN', 'description', 'total_stock', 'category') list_display = ('full_name', 'description', 'total_stock', 'category')
class PartCategoryAdmin(ImportExportModelAdmin): class PartCategoryAdmin(ImportExportModelAdmin):

View File

@ -24,6 +24,21 @@ class PartImageForm(HelperForm):
] ]
class BomValidateForm(HelperForm):
""" Simple confirmation form for BOM validation.
User is presented with a single checkbox input,
to confirm that the BOM for this part is valid
"""
validate = forms.BooleanField(required=False, initial=False, help_text='Confirm that the BOM is correct')
class Meta:
model = Part
fields = [
'validate'
]
class BomExportForm(HelperForm): class BomExportForm(HelperForm):
# TODO - Define these choices somewhere else, and import them here # TODO - Define these choices somewhere else, and import them here

View File

@ -0,0 +1,31 @@
# Generated by Django 2.2 on 2019-05-12 02:46
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('part', '0021_auto_20190510_2220'),
]
operations = [
migrations.AddField(
model_name='part',
name='bom_checked_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='boms_checked', to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='part',
name='bom_checked_date',
field=models.DateField(blank=True, null=True),
),
migrations.AddField(
model_name='part',
name='bom_checksum',
field=models.CharField(blank=True, help_text='Stored BOM checksum', max_length=128),
),
]

View File

@ -16,7 +16,7 @@ from django.core.exceptions import ValidationError
from django.urls import reverse from django.urls import reverse
from django.conf import settings from django.conf import settings
from django.db import models from django.db import models, transaction
from django.core.validators import MinValueValidator from django.core.validators import MinValueValidator
from django.contrib.staticfiles.templatetags.staticfiles import static from django.contrib.staticfiles.templatetags.staticfiles import static
@ -24,7 +24,9 @@ from django.contrib.auth.models import User
from django.db.models.signals import pre_delete from django.db.models.signals import pre_delete
from django.dispatch import receiver from django.dispatch import receiver
from datetime import datetime
from fuzzywuzzy import fuzz from fuzzywuzzy import fuzz
import hashlib
from InvenTree import helpers from InvenTree import helpers
from InvenTree import validators from InvenTree import validators
@ -202,14 +204,30 @@ class Part(models.Model):
] ]
def __str__(self): def __str__(self):
return "{n} - {d}".format(n=self.long_name, d=self.description) return "{n} - {d}".format(n=self.full_name, d=self.description)
@property @property
def long_name(self): def full_name(self):
name = self.name """ Format a 'full name' for this Part.
- IPN (if not null)
- Part name
- Part variant (if not null)
Elements are joined by the | character
"""
elements = []
if self.IPN:
elements.append(self.IPN)
elements.append(self.name)
if self.variant: if self.variant:
name += " | " + self.variant elements.append(self.variant)
return name
return ' | '.join(elements)
def get_absolute_url(self): def get_absolute_url(self):
""" Return the web URL for viewing this part """ """ Return the web URL for viewing this part """
@ -284,23 +302,23 @@ class Part(models.Model):
consumable = models.BooleanField(default=True, help_text='Can this part be used to build other parts?') consumable = models.BooleanField(default=True, help_text='Can this part be used to build other parts?')
# Is this part "trackable"?
# Trackable parts can have unique instances
# which are assigned serial numbers (or batch numbers)
# and can have their movements tracked
trackable = models.BooleanField(default=False, help_text='Does this part have tracking for unique items?') trackable = models.BooleanField(default=False, help_text='Does this part have tracking for unique items?')
# Is this part "purchaseable"?
purchaseable = models.BooleanField(default=True, help_text='Can this part be purchased from external suppliers?') purchaseable = models.BooleanField(default=True, help_text='Can this part be purchased from external suppliers?')
# Can this part be sold to customers?
salable = models.BooleanField(default=False, help_text="Can this part be sold to customers?") salable = models.BooleanField(default=False, help_text="Can this part be sold to customers?")
# Is this part active?
active = models.BooleanField(default=True, help_text='Is this part active?') active = models.BooleanField(default=True, help_text='Is this part active?')
notes = models.TextField(blank=True) notes = models.TextField(blank=True)
bom_checksum = models.CharField(max_length=128, blank=True, help_text='Stored BOM checksum')
bom_checked_by = models.ForeignKey(User, on_delete=models.SET_NULL, blank=True, null=True,
related_name='boms_checked')
bom_checked_date = models.DateField(blank=True, null=True)
def format_barcode(self): def format_barcode(self):
""" Return a JSON string for formatting a barcode for this Part object """ """ Return a JSON string for formatting a barcode for this Part object """
@ -451,13 +469,59 @@ class Part(models.Model):
@property @property
def bom_count(self): def bom_count(self):
""" Return the number of items contained in the BOM for this part """
return self.bom_items.count() return self.bom_items.count()
@property @property
def used_in_count(self): def used_in_count(self):
""" Return the number of part BOMs that this part appears in """
return self.used_in.count() return self.used_in.count()
def get_bom_hash(self):
""" Return a checksum hash for the BOM for this part.
Used to determine if the BOM has changed (and needs to be signed off!)
For hash is calculated from the following fields of each BOM item:
- Part.full_name (if the part name changes, the BOM checksum is invalidated)
- quantity
- Note field
returns a string representation of a hash object which can be compared with a stored value
"""
hash = hashlib.md5('bom seed'.encode())
for item in self.bom_items.all():
hash.update(str(item.sub_part.full_name).encode())
hash.update(str(item.quantity).encode())
hash.update(str(item.note).encode())
return str(hash.digest())
@property
def is_bom_valid(self):
""" Check if the BOM is 'valid' - if the calculated checksum matches the stored value
"""
return self.get_bom_hash() == self.bom_checksum
@transaction.atomic
def validate_bom(self, user):
""" Validate the BOM (mark the BOM as validated by the given User.
- Calculates and stores the hash for the BOM
- Saves the current date and the checking user
"""
self.bom_checksum = self.get_bom_hash()
self.bom_checked_by = user
self.bom_checked_date = datetime.now().date()
self.save()
def required_parts(self): def required_parts(self):
""" Return a list of parts required to make this part (list of BOM items) """
parts = [] parts = []
for bom in self.bom_items.all(): for bom in self.bom_items.all():
parts.append(bom.sub_part) parts.append(bom.sub_part)
@ -465,7 +529,7 @@ class Part(models.Model):
@property @property
def supplier_count(self): def supplier_count(self):
# Return the number of supplier parts available for this part """ Return the number of supplier parts available for this part """
return self.supplier_parts.count() return self.supplier_parts.count()
def export_bom(self, **kwargs): def export_bom(self, **kwargs):
@ -480,7 +544,7 @@ class Part(models.Model):
for it in self.bom_items.all(): for it in self.bom_items.all():
line = [] line = []
line.append(it.sub_part.name) line.append(it.sub_part.full_name)
line.append(it.sub_part.description) line.append(it.sub_part.description)
line.append(it.quantity) line.append(it.quantity)
line.append(it.note) line.append(it.note)
@ -611,8 +675,8 @@ class BomItem(models.Model):
def __str__(self): def __str__(self):
return "{n} x {child} to make {parent}".format( return "{n} x {child} to make {parent}".format(
parent=self.part.name, parent=self.part.full_name,
child=self.sub_part.name, child=self.sub_part.full_name,
n=self.quantity) n=self.quantity)

View File

@ -40,8 +40,7 @@ class PartBriefSerializer(serializers.ModelSerializer):
fields = [ fields = [
'pk', 'pk',
'url', 'url',
'name', 'full_name',
'variant',
'description', 'description',
'available_stock', 'available_stock',
'image_url', 'image_url',
@ -63,6 +62,7 @@ class PartSerializer(serializers.ModelSerializer):
fields = [ fields = [
'pk', 'pk',
'url', # Link to the part detail page 'url', # Link to the part detail page
'full_name',
'name', 'name',
'variant', 'variant',
'image_url', 'image_url',
@ -86,7 +86,7 @@ class PartSerializer(serializers.ModelSerializer):
class PartStarSerializer(InvenTreeModelSerializer): class PartStarSerializer(InvenTreeModelSerializer):
""" Serializer for a PartStar object """ """ Serializer for a PartStar object """
partname = serializers.CharField(source='part.name', read_only=True) partname = serializers.CharField(source='part.full_name', read_only=True)
username = serializers.CharField(source='user.username', read_only=True) username = serializers.CharField(source='user.username', read_only=True)
class Meta: class Meta:
@ -145,6 +145,7 @@ class SupplierPartSerializer(serializers.ModelSerializer):
'SKU', 'SKU',
'manufacturer', 'manufacturer',
'MPN', 'MPN',
'URL',
] ]

View File

@ -17,7 +17,7 @@
{% for allocation in part.build_allocation %} {% for allocation in part.build_allocation %}
<tr> <tr>
<td><a href="{% url 'build-detail' allocation.build.id %}">{{ allocation.build.title }}</a></td> <td><a href="{% url 'build-detail' allocation.build.id %}">{{ allocation.build.title }}</a></td>
<td>{{ allocation.build.quantity }} &times <a href="{% url 'part-detail' allocation.build.part.id %}">{{ allocation.build.part.name }}</a></td> <td>{{ allocation.build.quantity }} &times <a href="{% url 'part-detail' allocation.build.part.id %}">{{ allocation.build.part.full_name }}</a></td>
<td>{{ allocation.quantity }}</td> <td>{{ allocation.quantity }}</td>
<td>{% include "build_status.html" with build=allocation.build %}</td> <td>{% include "build_status.html" with build=allocation.build %}</td>
</tr> </tr>

View File

@ -4,6 +4,6 @@ Deleting this entry will remove the BOM row from the following part:
<ul class='list-group'> <ul class='list-group'>
<li class='list-group-item'> <li class='list-group-item'>
<b>{{ item.part.name }}</b> - <i>{{ item.part.description }}</i> <b>{{ item.part.full_name }}</b> - <i>{{ item.part.description }}</i>
</li> </li>
</ul> </ul>

View File

@ -4,8 +4,8 @@
<h3>BOM Item</h3> <h3>BOM Item</h3>
<table class="table table-striped"> <table class="table table-striped">
<tr><td>Parent</td><td><a href="{% url 'part-bom' item.part.id %}">{{ item.part.name }}</a></td></tr> <tr><td>Parent</td><td><a href="{% url 'part-bom' item.part.id %}">{{ item.part.full_name }}</a></td></tr>
<tr><td>Child</td><td><a href="{% url 'part-used-in' item.sub_part.id %}">{{ item.sub_part.name }}</a></td></tr> <tr><td>Child</td><td><a href="{% url 'part-used-in' item.sub_part.id %}">{{ item.sub_part.full_name }}</a></td></tr>
<tr><td>Quantity</td><td>{{ item.quantity }}</td></tr> <tr><td>Quantity</td><td>{{ item.quantity }}</td></tr>
</table> </table>

View File

@ -11,6 +11,21 @@
<h3>Bill of Materials</h3> <h3>Bill of Materials</h3>
{% if part.bom_checked_date %}
{% if part.is_bom_valid %}
<div class='alert alert-block alert-info'>
{% else %}
<div class='alert alert-block alert-danger'>
The BOM for <i>{{ part.full_name }}</i> has changed, and must be validated.<br>
{% endif %}
The BOM for <i>{{ part.full_name }}</i> was last checked by {{ part.bom_checked_by }} on {{ part.bom_checked_date }}
</div>
{% else %}
<div class='alert alert-danger alert-block'>
<b>The BOM for <i>{{ part.full_name }}</i> has not been validated.</b>
</div>
{% endif %}
<div id='button-toolbar'> <div id='button-toolbar'>
{% if editing_enabled %} {% if editing_enabled %}
<div class='btn-group' style='float: right;'> <div class='btn-group' style='float: right;'>
@ -24,6 +39,9 @@
<span class='caret'></span> <span class='caret'></span>
</button> </button>
<ul class='dropdown-menu'> <ul class='dropdown-menu'>
{% if part.is_bom_valid == False %}
<li><a href='#' id='validate-bom' title='Validate BOM'>Validate BOM</a></li>
{% endif %}
<li><a href='#' id='edit-bom' title='Edit BOM'>Edit BOM</a></li> <li><a href='#' id='edit-bom' title='Edit BOM'>Edit BOM</a></li>
<li><a href='#' id='export-bom' title='Export BOM'>Export BOM</a></li> <li><a href='#' id='export-bom' title='Export BOM'>Export BOM</a></li>
</ul> </ul>
@ -72,6 +90,15 @@
{% else %} {% else %}
$("#validate-bom").click(function() {
launchModalForm(
"{% url 'bom-validate' part.id %}",
{
reload: true,
}
);
});
$("#edit-bom").click(function () { $("#edit-bom").click(function () {
location.href = "{% url 'part-bom' part.id %}?edit=True"; location.href = "{% url 'part-bom' part.id %}?edit=True";
}); });

View File

@ -0,0 +1,5 @@
{% extends "modal_form.html" %}
{% block pre_form_content %}
Confirm that the Bill of Materials (BOM) is valid for:<br><i>{{ part.full_name }}</i>
{% endblock %}

View File

@ -27,7 +27,7 @@ the top level 'Parts' category.
</p> </p>
<ul class='list-group'> <ul class='list-group'>
{% for part in category.parts.all %} {% for part in category.parts.all %}
<li class='list-group-item'><b>{{ part.long_name }}</b> - <i>{{ part.description }}</i></li> <li class='list-group-item'><b>{{ part.full_name }}</b> - <i>{{ part.description }}</i></li>
{% endfor %} {% endfor %}
</ul> </ul>
{% endif %} {% endif %}

View File

@ -10,7 +10,7 @@
<ul class='list-group'> <ul class='list-group'>
{% for match in matches %} {% for match in matches %}
<li class='list-group-item list-group-item-condensed'> <li class='list-group-item list-group-item-condensed'>
{{ match.part.name }} - <i>{{ match.part.description }}</i> ({{ match.ratio }}%) {{ match.part.full_name }} - <i>{{ match.part.description }}</i> ({{ match.ratio }}%)
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>

View File

@ -16,7 +16,6 @@
{% if part.active %} {% if part.active %}
<li><a href='#' id='duplicate-part' title='Duplicate Part'>Duplicate</a></li> <li><a href='#' id='duplicate-part' title='Duplicate Part'>Duplicate</a></li>
<li><a href="#" id='edit-part' title='Edit part'>Edit</a></li> <li><a href="#" id='edit-part' title='Edit part'>Edit</a></li>
<li><a href='#' id='stocktake-part' title='Stocktake'>Stocktake</a></li>
<hr> <hr>
<li><a href="#" id='deactivate-part' title='Deactivate part'>Deactivate</a></li> <li><a href="#" id='deactivate-part' title='Deactivate part'>Deactivate</a></li>
{% else %} {% else %}
@ -34,7 +33,7 @@
<table class='table table-striped'> <table class='table table-striped'>
<tr> <tr>
<td>Part name</td> <td>Part name</td>
<td>{{ part.long_name }}</td> <td>{{ part.full_name }}</td>
</tr> </tr>
<tr> <tr>
<td>Description</td> <td>Description</td>
@ -46,6 +45,12 @@
<td>{{ part.IPN }}</td> <td>{{ part.IPN }}</td>
</tr> </tr>
{% endif %} {% endif %}
{% if part.URL %}
<tr>
<td>URL</td>
<td><a href="{{ part.URL }}">{{ part.URL }}</a></td>
</tr>
{% endif %}
<tr> <tr>
<td>Category</td> <td>Category</td>
<td> <td>
@ -147,7 +152,7 @@
$('#activate-part').click(function() { $('#activate-part').click(function() {
showQuestionDialog( showQuestionDialog(
'Activate Part?', 'Activate Part?',
'Are you sure you wish to reactivate {{ part.long_name }}?', 'Are you sure you wish to reactivate {{ part.full_name }}?',
{ {
accept_text: 'Activate', accept_text: 'Activate',
accept: function() { accept: function() {
@ -169,7 +174,7 @@
$('#deactivate-part').click(function() { $('#deactivate-part').click(function() {
showQuestionDialog( showQuestionDialog(
'Deactivate Part?', 'Deactivate Part?',
`Are you sure you wish to deactivate {{ part.long_name }}?<br> `Are you sure you wish to deactivate {{ part.full_name }}?<br>
`, `,
{ {
accept_text: 'Deactivate', accept_text: 'Deactivate',
@ -198,16 +203,4 @@
}); });
}); });
$('#stocktake-part').click(function() {
adjustStock({
action: 'stocktake',
query: {
part: {{ part.id }},
},
success: function() {
location.reload();
}
});
});
{% endblock %} {% endblock %}

View File

@ -4,7 +4,7 @@
{% block page_title %} {% block page_title %}
{% if part %} {% if part %}
InvenTree | Part - {{ part.long_name }} InvenTree | Part - {{ part.full_name }}
{% elif category %} {% elif category %}
InvenTree | Part Category - {{ category }} InvenTree | Part Category - {{ category }}
{% else %} {% else %}

View File

@ -7,7 +7,7 @@
<div class="row"> <div class="row">
{% if part.active == False %} {% if part.active == False %}
<div class='alert alert-danger' style='display: block;'> <div class='alert alert-danger' style='display: block;'>
This part ({{ part.long_name }}) is not active: This part ({{ part.full_name }}) is not active:
</div> </div>
{% endif %} {% endif %}
<div class="col-sm-6"> <div class="col-sm-6">
@ -24,7 +24,7 @@
</div> </div>
<div class="media-body"> <div class="media-body">
<h4> <h4>
{{ part.long_name }} {{ part.full_name }}
</h4> </h4>
{% if part.variant %} {% if part.variant %}
<p>Variant: {{ part.variant }}</p> <p>Variant: {{ part.variant }}</p>

View File

@ -1,10 +1,10 @@
Are you sure you want to delete part '{{ part.long_name }}'? Are you sure you want to delete part '{{ part.full_name }}'?
{% if part.used_in_count %} {% if part.used_in_count %}
<p>This part is used in BOMs for {{ part.used_in_count }} other parts. If you delete this part, the BOMs for the following parts will be updated: <p>This part is used in BOMs for {{ part.used_in_count }} other parts. If you delete this part, the BOMs for the following parts will be updated:
<ul class="list-group"> <ul class="list-group">
{% for child in part.used_in.all %} {% for child in part.used_in.all %}
<li class='list-group-item'>{{ child.part.name }} - {{ child.part.description }}</li> <li class='list-group-item'>{{ child.part.full_name }} - {{ child.part.description }}</li>
{% endfor %} {% endfor %}
</p> </p>
{% endif %} {% endif %}
@ -30,5 +30,5 @@ Are you sure you want to delete part '{{ part.long_name }}'?
{% endif %} {% endif %}
{% if part.serials.all|length > 0 %} {% if part.serials.all|length > 0 %}
<p>There are {{ part.serials.all|length }} unique parts tracked for '{{ part.long_name }}'. Deleting this part will permanently remove this tracking information.</p> <p>There are {{ part.serials.all|length }} unique parts tracked for '{{ part.full_name }}'. Deleting this part will permanently remove this tracking information.</p>
{% endif %} {% endif %}

View File

@ -48,7 +48,7 @@
$("#supplier-table").bootstrapTable({ $("#supplier-table").bootstrapTable({
sortable: true, sortable: true,
search: true, search: true,
formatNoMatches: function() { return "No supplier parts available for {{ part.long_name }}"; }, formatNoMatches: function() { return "No supplier parts available for {{ part.full_name }}"; },
queryParams: function(p) { queryParams: function(p) {
return { return {
part: {{ part.id }} part: {{ part.id }}

View File

@ -12,7 +12,7 @@
{% endif %} {% endif %}
{% if part.buildable %} {% if part.buildable %}
<li{% ifequal tab 'bom' %} class="active"{% endifequal %}> <li{% ifequal tab 'bom' %} class="active"{% endifequal %}>
<a href="{% url 'part-bom' part.id %}">BOM<span class="badge">{{ part.bom_count }}</span></a></li> <a href="{% url 'part-bom' part.id %}">BOM<span class="badge{% if part.is_bom_valid == False %} badge-alert{% endif %}">{{ part.bom_count }}</span></a></li>
<li{% ifequal tab 'build' %} class="active"{% endifequal %}> <li{% ifequal tab 'build' %} class="active"{% endifequal %}>
<a href="{% url 'part-build' part.id %}">Build<span class='badge'>{{ part.active_builds|length }}</span></a></li> <a href="{% url 'part-build' part.id %}">Build<span class='badge'>{{ part.active_builds|length }}</span></a></li>
{% endif %} {% endif %}

View File

@ -4,7 +4,7 @@
{% include 'part/tabs.html' with tab='track' %} {% include 'part/tabs.html' with tab='track' %}
Part tracking for {{ part.long_name }} Part tracking for {{ part.full_name }}
<table class="table table-striped"> <table class="table table-striped">
<tr> <tr>

View File

@ -27,7 +27,7 @@
$("#used-table").bootstrapTable({ $("#used-table").bootstrapTable({
sortable: true, sortable: true,
search: true, search: true,
formatNoMatches: function() { return "{{ part.long_name }} is not used to make any other parts"; }, formatNoMatches: function() { return "{{ part.full_name }} is not used to make any other parts"; },
queryParams: function(p) { queryParams: function(p) {
return { return {
sub_part: {{ part.id }} sub_part: {{ part.id }}
@ -43,7 +43,7 @@
field: 'part_detail', field: 'part_detail',
title: 'Part', title: 'Part',
formatter: function(value, row, index, field) { formatter: function(value, row, index, field) {
return imageHoverIcon(row.part_detail.image_url) + renderLink(value.name, value.url + 'bom/'); return imageHoverIcon(row.part_detail.image_url) + renderLink(value.full_name, value.url + 'bom/');
} }
}, },
{ {

View File

@ -35,6 +35,7 @@ part_detail_urls = [
url(r'^edit/?', views.PartEdit.as_view(), name='part-edit'), url(r'^edit/?', views.PartEdit.as_view(), name='part-edit'),
url(r'^delete/?', views.PartDelete.as_view(), name='part-delete'), url(r'^delete/?', views.PartDelete.as_view(), name='part-delete'),
url(r'^bom-export/?', views.BomDownload.as_view(), name='bom-export'), url(r'^bom-export/?', views.BomDownload.as_view(), name='bom-export'),
url(r'^validate-bom/', views.BomValidate.as_view(), name='bom-validate'),
url(r'^track/?', views.PartDetail.as_view(template_name='part/track.html'), name='part-track'), 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'^attachments/?', views.PartDetail.as_view(template_name='part/attachments.html'), name='part-attachments'),

View File

@ -331,12 +331,50 @@ class PartEdit(AjaxUpdateView):
return form return form
class BomValidate(AjaxUpdateView):
""" Modal form view for validating a part BOM """
model = Part
ajax_form_title = "Validate BOM"
ajax_template_name = 'part/bom_validate.html'
context_object_name = 'part'
form_class = part_forms.BomValidateForm
def get_context(self):
return {
'part': self.get_object(),
}
def get(self, request, *args, **kwargs):
form = self.get_form()
return self.renderJsonResponse(request, form, context=self.get_context())
def post(self, request, *args, **kwargs):
form = self.get_form()
part = self.get_object()
confirmed = str2bool(request.POST.get('validate', False))
if confirmed:
part.validate_bom(request.user)
else:
form.errors['validate'] = ['Confirm that the BOM is valid']
data = {
'form_valid': confirmed
}
return self.renderJsonResponse(request, form, data, context=self.get_context())
class BomExport(AjaxView): class BomExport(AjaxView):
model = Part model = Part
ajax_form_title = 'Export BOM' ajax_form_title = 'Export BOM'
ajax_template_name = 'part/bom_export.html' ajax_template_name = 'part/bom_export.html'
context_object_name = 'part'
form_class = part_forms.BomExportForm form_class = part_forms.BomExportForm
def get_object(self): def get_object(self):
@ -345,17 +383,6 @@ class BomExport(AjaxView):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
form = self.form_class() form = self.form_class()
"""
part = self.get_object()
context = {
'part': part
}
if request.is_ajax():
passs
"""
return self.renderJsonResponse(request, form) return self.renderJsonResponse(request, form)
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):

View File

@ -14,6 +14,7 @@
color: #ffcc00; color: #ffcc00;
} }
/* CSS overrides for treeview */ /* CSS overrides for treeview */
.expand-icon { .expand-icon {
font-size: 11px; font-size: 11px;
@ -97,6 +98,10 @@
margin-left: 10px; margin-left: 10px;
} }
.badge-alert {
background-color: #f33;
}
.part-thumb { .part-thumb {
width: 200px; width: 200px;
height: 200px; height: 200px;
@ -219,6 +224,10 @@
pointer-events: all; pointer-events: all;
} }
.alert-block {
display: block;
}
.btn { .btn {
margin-left: 2px; margin-left: 2px;
margin-right: 2px; margin-right: 2px;

View File

@ -93,7 +93,7 @@ function loadBomTable(table, options) {
title: 'Part', title: 'Part',
sortable: true, sortable: true,
formatter: function(value, row, index, field) { formatter: function(value, row, index, field) {
return imageHoverIcon(value.image_url) + renderLink(value.name, value.url); return imageHoverIcon(value.image_url) + renderLink(value.full_name, value.url);
} }
} }
); );

View File

@ -123,11 +123,7 @@ function loadPartTable(table, url, options={}) {
title: 'Part', title: 'Part',
sortable: true, sortable: true,
formatter: function(value, row, index, field) { formatter: function(value, row, index, field) {
var name = row.name; var name = row.full_name;
if (row.variant) {
name = name + " | " + row.variant;
}
var display = imageHoverIcon(row.image_url) + renderLink(name, row.url); var display = imageHoverIcon(row.image_url) + renderLink(name, row.url);
if (!row.active) { if (!row.active) {

View File

@ -393,7 +393,7 @@ function loadStockTable(table, options) {
visible: false, visible: false,
}, },
{ {
field: 'part.name', field: 'part.full_name',
title: 'Part', title: 'Part',
sortable: true, sortable: true,
formatter: function(value, row, index, field) { formatter: function(value, row, index, field) {

View File

@ -180,7 +180,7 @@ class StockItem(models.Model):
reverse('api-stock-detail', kwargs={'pk': self.id}), reverse('api-stock-detail', kwargs={'pk': self.id}),
{ {
'part_id': self.part.id, 'part_id': self.part.id,
'part_name': self.part.name 'part_name': self.part.full_name
} }
) )
@ -464,7 +464,7 @@ class StockItem(models.Model):
def __str__(self): def __str__(self):
s = '{n} x {part}'.format( s = '{n} x {part}'.format(
n=self.quantity, n=self.quantity,
part=self.part.name) part=self.part.full_name)
if self.location: if self.location:
s += ' @ {loc}'.format(loc=self.location.name) s += ' @ {loc}'.format(loc=self.location.name)

View File

@ -32,7 +32,7 @@ class StockItemSerializerBrief(serializers.ModelSerializer):
""" Brief serializers for a StockItem """ """ Brief serializers for a StockItem """
location_name = serializers.CharField(source='location', read_only=True) location_name = serializers.CharField(source='location', read_only=True)
part_name = serializers.CharField(source='part.name', read_only=True) part_name = serializers.CharField(source='part.full_name', read_only=True)
class Meta: class Meta:
model = StockItem model = StockItem

View File

@ -5,7 +5,7 @@
<div class='row'> <div class='row'>
<div class='col-sm-6'> <div class='col-sm-6'>
<h3>Stock Item Details</h3> <h3>Stock Item Details</h3>
<p><i>{{ item.quantity }} &times {{ item.part.name }}</i></p> <p><i>{{ item.quantity }} &times {{ item.part.full_name }}</i></p>
<p> <p>
<div class='btn-group'> <div class='btn-group'>
{% include "qr_button.html" %} {% include "qr_button.html" %}
@ -39,7 +39,7 @@
<table class="table table-striped"> <table class="table table-striped">
<tr> <tr>
<td>Part</td> <td>Part</td>
<td><a href="{% url 'part-stock' item.part.id %}">{{ item.part.name }}</td> <td><a href="{% url 'part-stock' item.part.id %}">{{ item.part.full_name }}</td>
</tr> </tr>
{% if item.belongs_to %} {% if item.belongs_to %}
<tr> <tr>

View File

@ -2,4 +2,4 @@ Are you sure you want to delete this stock item?
<br> <br>
This will remove <b>{{ item.quantity }}</b> units of <b>{{ item.part.name }}</b> from stock. This will remove <b>{{ item.quantity }}</b> units of <b>{{ item.part.full_name }}</b> from stock.

View File

@ -30,7 +30,7 @@ If this location is deleted, these items will be moved to the top level 'Stock'
<ul class='list-group'> <ul class='list-group'>
{% for item in location.stock_items.all %} {% for item in location.stock_items.all %}
<li class='list-group-item'><b>{{ item.part.name }}</b> - <i>{{ item.part.description }}</i><span class='badge'>{{ item.quantity }}</span></li> <li class='list-group-item'><b>{{ item.part.full_name }}</b> - <i>{{ item.part.description }}</i><span class='badge'>{{ item.quantity }}</span></li>
{% endfor %} {% endfor %}
</ul> </ul>
{% endif %} {% endif %}

View File

@ -8,7 +8,7 @@
</tr> </tr>
{% for part in parts %} {% for part in parts %}
<tr> <tr>
<td><a href="{% url 'part-detail' part.id %}">{{ part.long_name }}</a></td> <td><a href="{% url 'part-detail' part.id %}">{{ part.full_name }}</a></td>
<td>{{ part.description }}</td> <td>{{ part.description }}</td>
<td>{{ part.total_stock }}</td> <td>{{ part.total_stock }}</td>
<td>{{ part.allocation_count }}</td> <td>{{ part.allocation_count }}</td>