Merge pull request #299 from SchrodingersGat/part-revision

Part revision
This commit is contained in:
Oliver 2019-05-10 23:26:28 +10:00 committed by GitHub
commit 9ba91a9d80
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 112 additions and 23 deletions

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

@ -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>