diff --git a/InvenTree/InvenTree/validators.py b/InvenTree/InvenTree/validators.py
new file mode 100644
index 0000000000..88da882e10
--- /dev/null
+++ b/InvenTree/InvenTree/validators.py
@@ -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')
+ )
diff --git a/InvenTree/part/forms.py b/InvenTree/part/forms.py
index 1b2814ba52..1a3f8b6125 100644
--- a/InvenTree/part/forms.py
+++ b/InvenTree/part/forms.py
@@ -65,6 +65,7 @@ class EditPartForm(HelperForm):
fields = [
'category',
'name',
+ 'variant',
'description',
'IPN',
'URL',
diff --git a/InvenTree/part/migrations/0020_auto_20190510_2022.py b/InvenTree/part/migrations/0020_auto_20190510_2022.py
new file mode 100644
index 0000000000..8bf6db8f18
--- /dev/null
+++ b/InvenTree/part/migrations/0020_auto_20190510_2022.py
@@ -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')},
+ ),
+ ]
diff --git a/InvenTree/part/migrations/0021_auto_20190510_2220.py b/InvenTree/part/migrations/0021_auto_20190510_2220.py
new file mode 100644
index 0000000000..294bd112ae
--- /dev/null
+++ b/InvenTree/part/migrations/0021_auto_20190510_2220.py
@@ -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]),
+ ),
+ ]
diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py
index 5a318c85b5..0318c11f57 100644
--- a/InvenTree/part/models.py
+++ b/InvenTree/part/models.py
@@ -25,6 +25,7 @@ from django.db.models.signals import pre_delete
from django.dispatch import receiver
from InvenTree import helpers
+from InvenTree import validators
from InvenTree.models import InvenTreeTree
from company.models import Company
@@ -124,6 +125,7 @@ class Part(models.Model):
Attributes:
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
category: The PartCategory to which this part belongs
IPN: Internal part number (optional)
@@ -142,6 +144,23 @@ class Part(models.Model):
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):
""" Return the web URL for viewing this part """
return reverse('part-detail', kwargs={'pk': self.id})
@@ -154,7 +173,11 @@ class Part(models.Model):
else:
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')
@@ -228,9 +251,6 @@ class Part(models.Model):
notes = models.TextField(blank=True)
- def __str__(self):
- return "{n} - {d}".format(n=self.name, d=self.description)
-
def format_barcode(self):
""" 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
def category_path(self):
if self.category:
diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py
index 97588650cb..8cc56daabb 100644
--- a/InvenTree/part/serializers.py
+++ b/InvenTree/part/serializers.py
@@ -41,9 +41,10 @@ class PartBriefSerializer(serializers.ModelSerializer):
'pk',
'url',
'name',
- 'image_url',
+ 'variant',
'description',
'available_stock',
+ 'image_url',
]
@@ -63,6 +64,7 @@ class PartSerializer(serializers.ModelSerializer):
'pk',
'url', # Link to the part detail page
'name',
+ 'variant',
'image_url',
'IPN',
'URL', # Link to an external URL (optional)
diff --git a/InvenTree/part/templates/part/category_delete.html b/InvenTree/part/templates/part/category_delete.html
index 8b6d5618f3..c603637765 100644
--- a/InvenTree/part/templates/part/category_delete.html
+++ b/InvenTree/part/templates/part/category_delete.html
@@ -27,7 +27,7 @@ the top level 'Parts' category.
{% for part in category.parts.all %}
- - {{ part.name }} - {{ part.description }}
+ - {{ part.long_name }} - {{ part.description }}
{% endfor %}
{% endif %}
\ No newline at end of file
diff --git a/InvenTree/part/templates/part/detail.html b/InvenTree/part/templates/part/detail.html
index 419409a95d..3242316136 100644
--- a/InvenTree/part/templates/part/detail.html
+++ b/InvenTree/part/templates/part/detail.html
@@ -34,7 +34,7 @@
Part name |
- {{ part.name }} |
+ {{ part.long_name }} |
Description |
@@ -147,7 +147,7 @@
$('#activate-part').click(function() {
showQuestionDialog(
'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: function() {
@@ -169,7 +169,7 @@
$('#deactivate-part').click(function() {
showQuestionDialog(
'Deactivate Part?',
- `Are you sure you wish to deactivate {{ part.name }}?
+ `Are you sure you wish to deactivate {{ part.long_name }}?
`,
{
accept_text: 'Deactivate',
diff --git a/InvenTree/part/templates/part/part_app_base.html b/InvenTree/part/templates/part/part_app_base.html
index cb1fbdef90..ed3ecfb1b9 100644
--- a/InvenTree/part/templates/part/part_app_base.html
+++ b/InvenTree/part/templates/part/part_app_base.html
@@ -4,7 +4,7 @@
{% block page_title %}
{% if part %}
-InvenTree | Part - {{ part.name }}
+InvenTree | Part - {{ part.long_name }}
{% elif category %}
InvenTree | Part Category - {{ category }}
{% else %}
diff --git a/InvenTree/part/templates/part/part_base.html b/InvenTree/part/templates/part/part_base.html
index c73d8d5db7..d13495bc52 100644
--- a/InvenTree/part/templates/part/part_base.html
+++ b/InvenTree/part/templates/part/part_base.html
@@ -7,7 +7,7 @@
{% if part.active == False %}
- This part ({{ part.name }}) is not active:
+ This part ({{ part.long_name }}) is not active:
{% endif %}
@@ -24,8 +24,11 @@
- {{ part.name }}
+ {{ part.long_name }}
+ {% if part.variant %}
+
Variant: {{ part.variant }}
+ {% endif %}
{{ part.description }}
diff --git a/InvenTree/part/templates/part/partial_delete.html b/InvenTree/part/templates/part/partial_delete.html
index 8d60cfff70..e30be8444a 100644
--- a/InvenTree/part/templates/part/partial_delete.html
+++ b/InvenTree/part/templates/part/partial_delete.html
@@ -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 %}
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 %}
{% if part.serials.all|length > 0 %}
-
There are {{ part.serials.all|length }} unique parts tracked for '{{ part.name }}'. Deleting this part will permanently remove this tracking information.
+
There are {{ part.serials.all|length }} unique parts tracked for '{{ part.long_name }}'. Deleting this part will permanently remove this tracking information.
{% endif %}
\ No newline at end of file
diff --git a/InvenTree/part/templates/part/supplier.html b/InvenTree/part/templates/part/supplier.html
index 4999694651..5112dad913 100644
--- a/InvenTree/part/templates/part/supplier.html
+++ b/InvenTree/part/templates/part/supplier.html
@@ -48,7 +48,7 @@
$("#supplier-table").bootstrapTable({
sortable: 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) {
return {
part: {{ part.id }}
diff --git a/InvenTree/part/templates/part/track.html b/InvenTree/part/templates/part/track.html
index 01e174eef0..05f9d366d7 100644
--- a/InvenTree/part/templates/part/track.html
+++ b/InvenTree/part/templates/part/track.html
@@ -4,7 +4,7 @@
{% include 'part/tabs.html' with tab='track' %}
-Part tracking for {{ part.name }}
+Part tracking for {{ part.long_name }}
diff --git a/InvenTree/part/templates/part/used_in.html b/InvenTree/part/templates/part/used_in.html
index f1a055695e..f7272dfea6 100644
--- a/InvenTree/part/templates/part/used_in.html
+++ b/InvenTree/part/templates/part/used_in.html
@@ -27,7 +27,7 @@
$("#used-table").bootstrapTable({
sortable: 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) {
return {
sub_part: {{ part.id }}
diff --git a/InvenTree/static/script/inventree/part.js b/InvenTree/static/script/inventree/part.js
index a991cb232c..82757b47bc 100644
--- a/InvenTree/static/script/inventree/part.js
+++ b/InvenTree/static/script/inventree/part.js
@@ -123,7 +123,13 @@ function loadPartTable(table, url, options={}) {
title: 'Part',
sortable: true,
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) {
display = display + "INACTIVE";
}
diff --git a/InvenTree/templates/required_part_table.html b/InvenTree/templates/required_part_table.html
index 4ae1441f11..5d362e8f77 100644
--- a/InvenTree/templates/required_part_table.html
+++ b/InvenTree/templates/required_part_table.html
@@ -8,7 +8,7 @@
{% for part in parts %}
- {{ part.name }} |
+ {{ part.long_name }} |
{{ part.description }} |
{{ part.total_stock }} |
{{ part.allocation_count }} |