Merge remote-tracking branch 'inventree/master'

This commit is contained in:
Oliver Walters 2019-05-10 23:29:16 +10:00
commit f11cee7197
18 changed files with 151 additions and 30 deletions

View File

@ -32,13 +32,6 @@ DEBUG = True
ALLOWED_HOSTS = ['*'] ALLOWED_HOSTS = ['*']
CORS_ORIGIN_WHITELIST = [ CORS_ORIGIN_WHITELIST = [
"""
TODO - Implement a proper CORS whitelist strategy here.
- The CORS headers should be set per-application and not hard-coded into settings.py
- Provide an external settings.yaml file which defines extra options
- Then the site admin can adjust these without touching tracked files
"""
] ]
if DEBUG: if DEBUG:

View File

@ -0,0 +1,15 @@
"""
Custom field validators for InvenTree
"""
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
def validate_part_name(value):
# Prevent some illegal characters in part names
for c in ['/', '\\', '|', '#', '$']:
if c in str(value):
raise ValidationError(
_('Invalid character in part name')
)

View File

@ -65,6 +65,7 @@ class EditPartForm(HelperForm):
fields = [ fields = [
'category', 'category',
'name', 'name',
'variant',
'description', 'description',
'IPN', 'IPN',
'URL', 'URL',

View File

@ -0,0 +1,27 @@
# Generated by Django 2.2 on 2019-05-10 10:22
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('part', '0019_auto_20190508_2332'),
]
operations = [
migrations.AddField(
model_name='part',
name='variant',
field=models.CharField(blank=True, help_text='Part variant or revision code', max_length=32),
),
migrations.AlterField(
model_name='part',
name='name',
field=models.CharField(help_text='Part name', max_length=100),
),
migrations.AlterUniqueTogether(
name='part',
unique_together={('name', 'variant')},
),
]

View File

@ -0,0 +1,19 @@
# Generated by Django 2.2 on 2019-05-10 12:20
import InvenTree.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('part', '0020_auto_20190510_2022'),
]
operations = [
migrations.AlterField(
model_name='part',
name='name',
field=models.CharField(help_text='Part name', max_length=100, validators=[InvenTree.validators.validate_part_name]),
),
]

View File

@ -25,6 +25,7 @@ from django.db.models.signals import pre_delete
from django.dispatch import receiver from django.dispatch import receiver
from InvenTree import helpers from InvenTree import helpers
from InvenTree import validators
from InvenTree.models import InvenTreeTree from InvenTree.models import InvenTreeTree
from company.models import Company from company.models import Company
@ -124,6 +125,7 @@ class Part(models.Model):
Attributes: Attributes:
name: Brief name for this part name: Brief name for this part
variant: Optional variant number for this part - Must be unique for the part name
description: Longer form description of the part description: Longer form description of the part
category: The PartCategory to which this part belongs category: The PartCategory to which this part belongs
IPN: Internal part number (optional) IPN: Internal part number (optional)
@ -142,6 +144,23 @@ class Part(models.Model):
notes: Additional notes field for this part notes: Additional notes field for this part
""" """
class Meta:
verbose_name = "Part"
verbose_name_plural = "Parts"
unique_together = [
('name', 'variant')
]
def __str__(self):
return "{n} - {d}".format(n=self.long_name, d=self.description)
@property
def long_name(self):
name = self.name
if self.variant:
name += " | " + self.variant
return name
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 """
return reverse('part-detail', kwargs={'pk': self.id}) return reverse('part-detail', kwargs={'pk': self.id})
@ -154,7 +173,11 @@ class Part(models.Model):
else: else:
return static('/img/blank_image.png') return static('/img/blank_image.png')
name = models.CharField(max_length=100, unique=True, blank=False, help_text='Part name (must be unique)') 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')
description = models.CharField(max_length=250, blank=False, help_text='Part description') description = models.CharField(max_length=250, blank=False, help_text='Part description')
@ -228,9 +251,6 @@ class Part(models.Model):
notes = models.TextField(blank=True) notes = models.TextField(blank=True)
def __str__(self):
return "{n} - {d}".format(n=self.name, d=self.description)
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 """
@ -243,10 +263,6 @@ class Part(models.Model):
} }
) )
class Meta:
verbose_name = "Part"
verbose_name_plural = "Parts"
@property @property
def category_path(self): def category_path(self):
if self.category: if self.category:

View File

@ -41,9 +41,10 @@ class PartBriefSerializer(serializers.ModelSerializer):
'pk', 'pk',
'url', 'url',
'name', 'name',
'image_url', 'variant',
'description', 'description',
'available_stock', 'available_stock',
'image_url',
] ]
@ -63,6 +64,7 @@ class PartSerializer(serializers.ModelSerializer):
'pk', 'pk',
'url', # Link to the part detail page 'url', # Link to the part detail page
'name', 'name',
'variant',
'image_url', 'image_url',
'IPN', 'IPN',
'URL', # Link to an external URL (optional) 'URL', # Link to an external URL (optional)

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.name }}</b> - <i>{{ part.description }}</i></li> <li class='list-group-item'><b>{{ part.long_name }}</b> - <i>{{ part.description }}</i></li>
{% endfor %} {% endfor %}
</ul> </ul>
{% endif %} {% endif %}

View File

@ -34,7 +34,7 @@
<table class='table table-striped'> <table class='table table-striped'>
<tr> <tr>
<td>Part name</td> <td>Part name</td>
<td>{{ part.name }}</td> <td>{{ part.long_name }}</td>
</tr> </tr>
<tr> <tr>
<td>Description</td> <td>Description</td>
@ -147,7 +147,7 @@
$('#activate-part').click(function() { $('#activate-part').click(function() {
showQuestionDialog( showQuestionDialog(
'Activate Part?', 'Activate Part?',
'Are you sure you wish to reactivate {{ part.name }}?', 'Are you sure you wish to reactivate {{ part.long_name }}?',
{ {
accept_text: 'Activate', accept_text: 'Activate',
accept: function() { accept: function() {
@ -169,7 +169,7 @@
$('#deactivate-part').click(function() { $('#deactivate-part').click(function() {
showQuestionDialog( showQuestionDialog(
'Deactivate Part?', 'Deactivate Part?',
`Are you sure you wish to deactivate {{ part.name }}?<br> `Are you sure you wish to deactivate {{ part.long_name }}?<br>
`, `,
{ {
accept_text: 'Deactivate', accept_text: 'Deactivate',

View File

@ -4,7 +4,7 @@
{% block page_title %} {% block page_title %}
{% if part %} {% if part %}
InvenTree | Part - {{ part.name }} InvenTree | Part - {{ part.long_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.name }}) is not active: This part ({{ part.long_name }}) is not active:
</div> </div>
{% endif %} {% endif %}
<div class="col-sm-6"> <div class="col-sm-6">
@ -24,8 +24,11 @@
</div> </div>
<div class="media-body"> <div class="media-body">
<h4> <h4>
{{ part.name }} {{ part.long_name }}
</h4> </h4>
{% if part.variant %}
<p>Variant: {{ part.variant }}</p>
{% endif %}
<p><i>{{ part.description }}</i></p> <p><i>{{ part.description }}</i></p>
<p> <p>
<div class='btn-group'> <div class='btn-group'>

View File

@ -1,4 +1,4 @@
Are you sure you want to delete part '{{ part.name }}'? Are you sure you want to delete part '{{ part.long_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:
@ -30,5 +30,5 @@ Are you sure you want to delete part '{{ part.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.name }}'. Deleting this part will permanently remove this tracking information.</p> <p>There are {{ part.serials.all|length }} unique parts tracked for '{{ part.long_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.name }}"; }, formatNoMatches: function() { return "No supplier parts available for {{ part.long_name }}"; },
queryParams: function(p) { queryParams: function(p) {
return { return {
part: {{ part.id }} part: {{ part.id }}

View File

@ -4,7 +4,7 @@
{% include 'part/tabs.html' with tab='track' %} {% include 'part/tabs.html' with tab='track' %}
Part tracking for {{ part.name }} Part tracking for {{ part.long_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.name }} is not used to make any other parts"; }, formatNoMatches: function() { return "{{ part.long_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 }}

View File

@ -5,6 +5,44 @@ function makeOption(id, title) {
} }
function partialMatcher(params, data) {
/* Replacement function for the 'matcher' parameter for a select2 dropdown.
Intead of performing an exact match search, a partial match search is performed.
This splits the search term by the space ' ' character and matches each segment.
Segments can appear out of order and are not case sensitive
Args:
params.term : search query
data.text : text to match
*/
// Quickly check for an empty search query
if ($.trim(params.term) == '') {
return data;
}
// Do not display the item if there is no 'text' property
if (typeof data.text === 'undefined') {
return null;
}
var search_terms = params.term.toLowerCase().trim().split(' ');
var match_text = data.text.toLowerCase().trim();
for (var ii = 0; ii < search_terms.length; ii++) {
if (!match_text.includes(search_terms[ii])) {
// Text must contain each search term
return null;
}
}
// Default: match!
return data;
}
function attachSelect(modal) { function attachSelect(modal) {
/* Attach 'select2' functionality to any drop-down list in the modal. /* Attach 'select2' functionality to any drop-down list in the modal.
* Provides search filtering for dropdown items * Provides search filtering for dropdown items
@ -14,6 +52,7 @@ function attachSelect(modal) {
dropdownParent: $(modal), dropdownParent: $(modal),
// dropdownAutoWidth parameter is required to work properly with modal forms // dropdownAutoWidth parameter is required to work properly with modal forms
dropdownAutoWidth: false, dropdownAutoWidth: false,
matcher: partialMatcher,
}); });
$(modal + ' .select2-container').addClass('select-full-width'); $(modal + ' .select2-container').addClass('select-full-width');

View File

@ -123,7 +123,13 @@ 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 display = imageHoverIcon(row.image_url) + renderLink(value, row.url); var name = row.name;
if (row.variant) {
name = name + " | " + row.variant;
}
var display = imageHoverIcon(row.image_url) + renderLink(name, row.url);
if (!row.active) { if (!row.active) {
display = display + "<span class='label label-warning' style='float: right;'>INACTIVE</span>"; display = display + "<span class='label label-warning' style='float: right;'>INACTIVE</span>";
} }

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.name }}</a></td> <td><a href="{% url 'part-detail' part.id %}">{{ part.long_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>