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(
n=item.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.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:
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 """
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)
class Meta:

View File

@ -14,7 +14,7 @@ InvenTree | Allocate Parts
<div class='col-sm-6'>
<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 class='col-sm-6'>
<div class='btn-group' style='float: right;'>
@ -47,7 +47,7 @@ InvenTree | Allocate Parts
loadAllocationTable(
$("#allocate-table-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 }}",
{% multiply build.quantity bom_item.quantity %},
$("#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 %}">
</div>
<div>
{{ item.sub_part.name }}<br>
{{ item.sub_part.full_name }}<br>
<small><i>{{ item.sub_part.description }}</i></small>
</div>
{% endblock %}

View File

@ -4,7 +4,7 @@
{{ 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>
Automatically allocate stock to this build?
<hr>
@ -27,7 +27,7 @@ Automatically allocate stock to this build?
</a>
</td>
<td>
{{ item.stock_item.part.name }}<br>
{{ item.stock_item.part.full_name }}<br>
<i>{{ item.stock_item.part.description }}</i>
</td>
<td>{{ item.quantity }}</td>

View File

@ -23,7 +23,7 @@
{% for build in builds %}
<tr>
<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>{% include "build_status.html" with build=build %}
{% if completed %}

View File

@ -1,7 +1,7 @@
{% extends "modal_form.html" %}
{% 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>
Are you sure you want to mark this build as complete?
<hr>
@ -24,7 +24,7 @@ The following items will be removed from stock:
</a>
</td>
<td>
{{ item.stock_item.part.name }}<br>
{{ item.stock_item.part.full_name }}<br>
<i>{{ item.stock_item.part.description }}</i>
</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-large' src='{{ build.part.image.url }}'>
</a>
{{ build.quantity }} x {{ build.part.name }}
{{ build.quantity }} x {{ build.part.full_name }}
</div>
{% endblock %}

View File

@ -11,7 +11,7 @@ InvenTree | Build - {{ build }}
<div class='col-sm-6'>
<h3>Build Details</h3>
<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 class='col-sm-6'>
<h3>
@ -40,7 +40,7 @@ InvenTree | Build - {{ build }}
<td>Title</td><td>{{ build.title }}</td>
</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>
<td>Quantity</td><td>{{ build.quantity }}</td>
@ -109,7 +109,7 @@ InvenTree | Build - {{ build }}
<tbody>
{% for item in build.required_parts %}
<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.part.total_stock }}</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>
<ul class='list-group'>
{% 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 %}
</ul>
{% endif %}

View File

@ -51,7 +51,7 @@
},
{
sortable: true,
field: 'part_detail.name',
field: 'part_detail.full_name',
title: 'Part',
formatter: function(value, row, index, field) {
return imageHoverIcon(row.part_detail.image_url) + renderLink(value, '/part/' + row.part + '/suppliers/');
@ -74,7 +74,18 @@
sortable: true,
field: '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' %}"
});

View File

@ -37,7 +37,7 @@ InvenTree | {{ company.name }} - Parts
<td>Internal Part</td>
<td>
{% 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 %}
</td>
</tr>

View File

@ -9,7 +9,7 @@ from .models import BomItem
class PartAdmin(ImportExportModelAdmin):
list_display = ('long_name', 'IPN', 'description', 'total_stock', 'category')
list_display = ('full_name', 'description', 'total_stock', 'category')
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):
# 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.conf import settings
from django.db import models
from django.db import models, transaction
from django.core.validators import MinValueValidator
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.dispatch import receiver
from datetime import datetime
from fuzzywuzzy import fuzz
import hashlib
from InvenTree import helpers
from InvenTree import validators
@ -202,14 +204,30 @@ class Part(models.Model):
]
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
def long_name(self):
name = self.name
def full_name(self):
""" 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:
name += " | " + self.variant
return name
elements.append(self.variant)
return ' | '.join(elements)
def get_absolute_url(self):
""" 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?')
# 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?')
# Is this part "purchaseable"?
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?")
# Is this part active?
active = models.BooleanField(default=True, help_text='Is this part active?')
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):
""" Return a JSON string for formatting a barcode for this Part object """
@ -451,13 +469,59 @@ class Part(models.Model):
@property
def bom_count(self):
""" Return the number of items contained in the BOM for this part """
return self.bom_items.count()
@property
def used_in_count(self):
""" Return the number of part BOMs that this part appears in """
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):
""" Return a list of parts required to make this part (list of BOM items) """
parts = []
for bom in self.bom_items.all():
parts.append(bom.sub_part)
@ -465,7 +529,7 @@ class Part(models.Model):
@property
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()
def export_bom(self, **kwargs):
@ -480,7 +544,7 @@ class Part(models.Model):
for it in self.bom_items.all():
line = []
line.append(it.sub_part.name)
line.append(it.sub_part.full_name)
line.append(it.sub_part.description)
line.append(it.quantity)
line.append(it.note)
@ -611,8 +675,8 @@ class BomItem(models.Model):
def __str__(self):
return "{n} x {child} to make {parent}".format(
parent=self.part.name,
child=self.sub_part.name,
parent=self.part.full_name,
child=self.sub_part.full_name,
n=self.quantity)

View File

@ -34,14 +34,13 @@ class PartBriefSerializer(serializers.ModelSerializer):
url = serializers.CharField(source='get_absolute_url', read_only=True)
image_url = serializers.CharField(source='get_image_url', read_only=True)
class Meta:
model = Part
fields = [
'pk',
'url',
'name',
'variant',
'full_name',
'description',
'available_stock',
'image_url',
@ -63,6 +62,7 @@ class PartSerializer(serializers.ModelSerializer):
fields = [
'pk',
'url', # Link to the part detail page
'full_name',
'name',
'variant',
'image_url',
@ -86,7 +86,7 @@ class PartSerializer(serializers.ModelSerializer):
class PartStarSerializer(InvenTreeModelSerializer):
""" 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)
class Meta:
@ -145,6 +145,7 @@ class SupplierPartSerializer(serializers.ModelSerializer):
'SKU',
'manufacturer',
'MPN',
'URL',
]

View File

@ -17,7 +17,7 @@
{% for allocation in part.build_allocation %}
<tr>
<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>{% include "build_status.html" with build=allocation.build %}</td>
</tr>

View File

@ -4,6 +4,6 @@ Deleting this entry will remove the BOM row from the following part:
<ul class='list-group'>
<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>
</ul>

View File

@ -4,8 +4,8 @@
<h3>BOM Item</h3>
<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>Child</td><td><a href="{% url 'part-used-in' item.sub_part.id %}">{{ item.sub_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.full_name }}</a></td></tr>
<tr><td>Quantity</td><td>{{ item.quantity }}</td></tr>
</table>

View File

@ -11,6 +11,21 @@
<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'>
{% if editing_enabled %}
<div class='btn-group' style='float: right;'>
@ -24,6 +39,9 @@
<span class='caret'></span>
</button>
<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='export-bom' title='Export BOM'>Export BOM</a></li>
</ul>
@ -72,6 +90,15 @@
{% else %}
$("#validate-bom").click(function() {
launchModalForm(
"{% url 'bom-validate' part.id %}",
{
reload: true,
}
);
});
$("#edit-bom").click(function () {
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>
<ul class='list-group'>
{% 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 %}
</ul>
{% endif %}

View File

@ -10,7 +10,7 @@
<ul class='list-group'>
{% for match in matches %}
<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>
{% endfor %}
</ul>

View File

@ -16,7 +16,6 @@
{% if part.active %}
<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='stocktake-part' title='Stocktake'>Stocktake</a></li>
<hr>
<li><a href="#" id='deactivate-part' title='Deactivate part'>Deactivate</a></li>
{% else %}
@ -34,7 +33,7 @@
<table class='table table-striped'>
<tr>
<td>Part name</td>
<td>{{ part.long_name }}</td>
<td>{{ part.full_name }}</td>
</tr>
<tr>
<td>Description</td>
@ -46,6 +45,12 @@
<td>{{ part.IPN }}</td>
</tr>
{% endif %}
{% if part.URL %}
<tr>
<td>URL</td>
<td><a href="{{ part.URL }}">{{ part.URL }}</a></td>
</tr>
{% endif %}
<tr>
<td>Category</td>
<td>
@ -147,7 +152,7 @@
$('#activate-part').click(function() {
showQuestionDialog(
'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: function() {
@ -169,7 +174,7 @@
$('#deactivate-part').click(function() {
showQuestionDialog(
'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',
@ -198,16 +203,4 @@
});
});
$('#stocktake-part').click(function() {
adjustStock({
action: 'stocktake',
query: {
part: {{ part.id }},
},
success: function() {
location.reload();
}
});
});
{% endblock %}

View File

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

View File

@ -7,7 +7,7 @@
<div class="row">
{% if part.active == False %}
<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>
{% endif %}
<div class="col-sm-6">
@ -24,7 +24,7 @@
</div>
<div class="media-body">
<h4>
{{ part.long_name }}
{{ part.full_name }}
</h4>
{% if part.variant %}
<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 %}
<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">
{% 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 %}
</p>
{% endif %}
@ -30,5 +30,5 @@ Are you sure you want to delete part '{{ part.long_name }}'?
{% endif %}
{% 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 %}

View File

@ -48,7 +48,7 @@
$("#supplier-table").bootstrapTable({
sortable: 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) {
return {
part: {{ part.id }}

View File

@ -12,7 +12,7 @@
{% endif %}
{% if part.buildable %}
<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 %}>
<a href="{% url 'part-build' part.id %}">Build<span class='badge'>{{ part.active_builds|length }}</span></a></li>
{% endif %}

View File

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

View File

@ -27,7 +27,7 @@
$("#used-table").bootstrapTable({
sortable: 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) {
return {
sub_part: {{ part.id }}
@ -43,7 +43,7 @@
field: 'part_detail',
title: 'Part',
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'^delete/?', views.PartDelete.as_view(), name='part-delete'),
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'^attachments/?', views.PartDetail.as_view(template_name='part/attachments.html'), name='part-attachments'),

View File

@ -331,12 +331,50 @@ class PartEdit(AjaxUpdateView):
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):
model = Part
ajax_form_title = 'Export BOM'
ajax_template_name = 'part/bom_export.html'
context_object_name = 'part'
form_class = part_forms.BomExportForm
def get_object(self):
@ -345,17 +383,6 @@ class BomExport(AjaxView):
def get(self, request, *args, **kwargs):
form = self.form_class()
"""
part = self.get_object()
context = {
'part': part
}
if request.is_ajax():
passs
"""
return self.renderJsonResponse(request, form)
def post(self, request, *args, **kwargs):

View File

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

View File

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

View File

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

View File

@ -180,7 +180,7 @@ class StockItem(models.Model):
reverse('api-stock-detail', kwargs={'pk': self.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):
s = '{n} x {part}'.format(
n=self.quantity,
part=self.part.name)
part=self.part.full_name)
if self.location:
s += ' @ {loc}'.format(loc=self.location.name)

View File

@ -32,7 +32,7 @@ class StockItemSerializerBrief(serializers.ModelSerializer):
""" Brief serializers for a StockItem """
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:
model = StockItem

View File

@ -5,7 +5,7 @@
<div class='row'>
<div class='col-sm-6'>
<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>
<div class='btn-group'>
{% include "qr_button.html" %}
@ -39,7 +39,7 @@
<table class="table table-striped">
<tr>
<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>
{% if item.belongs_to %}
<tr>

View File

@ -2,4 +2,4 @@ Are you sure you want to delete this stock item?
<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'>
{% 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 %}
</ul>
{% endif %}

View File

@ -8,7 +8,7 @@
</tr>
{% for part in parts %}
<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.total_stock }}</td>
<td>{{ part.allocation_count }}</td>