mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge remote-tracking branch 'inventree/master'
This commit is contained in:
commit
f11cee7197
@ -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:
|
||||||
|
15
InvenTree/InvenTree/validators.py
Normal file
15
InvenTree/InvenTree/validators.py
Normal 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')
|
||||||
|
)
|
@ -65,6 +65,7 @@ class EditPartForm(HelperForm):
|
|||||||
fields = [
|
fields = [
|
||||||
'category',
|
'category',
|
||||||
'name',
|
'name',
|
||||||
|
'variant',
|
||||||
'description',
|
'description',
|
||||||
'IPN',
|
'IPN',
|
||||||
'URL',
|
'URL',
|
||||||
|
27
InvenTree/part/migrations/0020_auto_20190510_2022.py
Normal file
27
InvenTree/part/migrations/0020_auto_20190510_2022.py
Normal 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')},
|
||||||
|
),
|
||||||
|
]
|
19
InvenTree/part/migrations/0021_auto_20190510_2220.py
Normal file
19
InvenTree/part/migrations/0021_auto_20190510_2220.py
Normal 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]),
|
||||||
|
),
|
||||||
|
]
|
@ -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:
|
||||||
|
@ -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)
|
||||||
|
@ -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 %}
|
@ -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',
|
||||||
|
@ -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 %}
|
||||||
|
@ -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'>
|
||||||
|
@ -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 %}
|
@ -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 }}
|
||||||
|
@ -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>
|
||||||
|
@ -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 }}
|
||||||
|
@ -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');
|
||||||
|
@ -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>";
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
Loading…
Reference in New Issue
Block a user