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.

{% 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 @@ - + @@ -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 }}
Part name{{ part.name }}{{ part.long_name }}
Description
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 }}