Merge pull request #471 from SchrodingersGat/bom-checksum

Bom checksum
This commit is contained in:
Oliver 2019-09-05 19:42:11 +10:00 committed by GitHub
commit 8fe7284173
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 178 additions and 16 deletions

View File

@ -81,6 +81,15 @@
max-width: 250px;
}
.bomrowvalid {
color: #050;
}
.bomrowinvalid {
color: #A00;
font-style: italic;
}
/* Part image icons with full-display on mouse hover */
.hover-img-thumb {

View File

@ -113,14 +113,19 @@ function loadBomTable(table, options) {
];
if (options.editable) {
/*
// TODO - Enable multi-select functionality
cols.push({
checkbox: true,
title: 'Select',
searchable: false,
sortable: false,
});
*/
}
// Part column
cols.push(
{
@ -230,10 +235,27 @@ function loadBomTable(table, options) {
if (options.editable) {
cols.push({
formatter: function(value, row, index, field) {
var bValidate = "<button title='Validate BOM Item' class='bom-validate-button btn btn-default btn-glyph' type='button' pk='" + row.pk + "'><span class='glyphicon glyphicon-check'/></button>";
var bValid = "<span class='glyphicon glyphicon-ok'/>";
var bEdit = "<button title='Edit BOM Item' class='bom-edit-button btn btn-default btn-glyph' type='button' url='/part/bom/" + row.pk + "/edit'><span class='glyphicon glyphicon-edit'/></button>";
var bDelt = "<button title='Delete BOM Item' class='bom-delete-button btn btn-default btn-glyph' type='button' url='/part/bom/" + row.pk + "/delete'><span class='glyphicon glyphicon-trash'/></button>";
return "<div class='btn-group' role='group'>" + bEdit + bDelt + "</div>";
var html = "<div class='btn-group' role='group'>";
html += bEdit;
html += bDelt;
if (!row.validated) {
html += bValidate;
} else {
html += bValid;
}
html += "</div>";
return html;
}
});
}
@ -256,6 +278,13 @@ function loadBomTable(table, options) {
table.bootstrapTable({
sortable: true,
search: true,
rowStyle: function(row, index) {
if (row.validated) {
return {classes: 'bomrowvalid'};
} else {
return {classes: 'bomrowinvalid'};
}
},
formatNoMatches: function() { return "No BOM items found"; },
clickToSelect: true,
showFooter: true,
@ -288,5 +317,22 @@ function loadBomTable(table, options) {
}
});
});
table.on('click', '.bom-validate-button', function() {
var button = $(this);
var url = '/api/bom/' + button.attr('pk') + '/validate/';
inventreePut(
url,
{
valid: true
},
{
method: 'PATCH',
reloadOnSuccess: true
}
);
});
}
}

View File

@ -12,7 +12,7 @@ from django.db.models import Sum
from rest_framework import status
from rest_framework.response import Response
from rest_framework import filters
from rest_framework import filters, serializers
from rest_framework import generics, permissions
from django.conf.urls import url, include
@ -303,7 +303,7 @@ class BomList(generics.ListCreateAPIView):
filter_fields = [
'part',
'sub_part'
'sub_part',
]
@ -318,6 +318,35 @@ class BomDetail(generics.RetrieveUpdateDestroyAPIView):
]
class BomItemValidate(generics.UpdateAPIView):
""" API endpoint for validating a BomItem """
# Very simple serializers
class BomItemValidationSerializer(serializers.Serializer):
valid = serializers.BooleanField(default=False)
queryset = BomItem.objects.all()
serializer_class = BomItemValidationSerializer
def update(self, request, *args, **kwargs):
""" Perform update request """
partial = kwargs.pop('partial', False)
valid = request.data.get('valid', False)
instance = self.get_object()
serializer = self.get_serializer(instance, data=request.data, partial=partial)
serializer.is_valid(raise_exception=True)
if type(instance) == BomItem:
instance.validate_hash(valid)
return Response(serializer.data)
cat_api_urls = [
url(r'^(?P<pk>\d+)/?', CategoryDetail.as_view(), name='api-part-category-detail'),
@ -345,10 +374,16 @@ part_api_urls = [
url(r'^.*$', PartList.as_view(), name='api-part-list'),
]
bom_item_urls = [
url(r'^validate/?', BomItemValidate.as_view(), name='api-bom-item-validate'),
url(r'^.*$', BomDetail.as_view(), name='api-bom-item-detail'),
]
bom_api_urls = [
# BOM Item Detail
url(r'^(?P<pk>\d+)/?', BomDetail.as_view(), name='api-bom-detail'),
url(r'^(?P<pk>\d+)/', include(bom_item_urls)),
# Catch-all
url(r'^.*$', BomList.as_view(), name='api-bom-list'),

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.4 on 2019-09-05 02:57
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('part', '0016_auto_20190820_0257'),
]
operations = [
migrations.AddField(
model_name='bomitem',
name='checksum',
field=models.CharField(blank=True, help_text='BOM line checksum', max_length=128),
),
]

View File

@ -631,24 +631,15 @@ class Part(models.Model):
""" Return a checksum hash for the BOM for this part.
Used to determine if the BOM has changed (and needs to be signed off!)
For hash is calculated from the following fields of each BOM item:
The hash is calculated by hashing each line item in the BOM.
- Part.full_name (if the part name changes, the BOM checksum is invalidated)
- Quantity
- Reference field
- Note field
returns a string representation of a hash object which can be compared with a stored value
"""
hash = hashlib.md5(str(self.id).encode())
for item in self.bom_items.all().prefetch_related('sub_part'):
hash.update(str(item.sub_part.id).encode())
hash.update(str(item.sub_part.full_name).encode())
hash.update(str(item.quantity).encode())
hash.update(str(item.note).encode())
hash.update(str(item.reference).encode())
hash.update(str(item.get_item_hash()).encode())
return str(hash.digest())
@ -667,6 +658,10 @@ class Part(models.Model):
- Saves the current date and the checking user
"""
# Validate each line item too
for item in self.bom_items.all():
item.validate_hash()
self.bom_checksum = self.get_bom_hash()
self.bom_checked_by = user
self.bom_checked_date = datetime.now().date()
@ -1121,6 +1116,7 @@ class BomItem(models.Model):
reference: BOM reference field (e.g. part designators)
overage: Estimated losses for a Build. Can be expressed as absolute value (e.g. '7') or a percentage (e.g. '2%')
note: Note field for this BOM item
checksum: Validation checksum for the particular BOM line item
"""
def get_absolute_url(self):
@ -1154,6 +1150,56 @@ class BomItem(models.Model):
# Note attached to this BOM line item
note = models.CharField(max_length=500, blank=True, help_text='BOM item notes')
checksum = models.CharField(max_length=128, blank=True, help_text='BOM line checksum')
def get_item_hash(self):
""" Calculate the checksum hash of this BOM line item:
The hash is calculated from the following fields:
- Part.full_name (if the part name changes, the BOM checksum is invalidated)
- Quantity
- Reference field
- Note field
"""
# Seed the hash with the ID of this BOM item
hash = hashlib.md5(str(self.id).encode())
# Update the hash based on line information
hash.update(str(self.sub_part.id).encode())
hash.update(str(self.sub_part.full_name).encode())
hash.update(str(self.quantity).encode())
hash.update(str(self.note).encode())
hash.update(str(self.reference).encode())
return str(hash.digest())
def validate_hash(self, valid=True):
""" Mark this item as 'valid' (store the checksum hash).
Args:
valid: If true, validate the hash, otherwise invalidate it (default = True)
"""
if valid:
self.checksum = str(self.get_item_hash())
else:
self.checksum = ''
self.save()
@property
def is_line_valid(self):
""" Check if this line item has been validated by the user """
# Ensure an empty checksum returns False
if len(self.checksum) == 0:
return False
return self.get_item_hash() == self.checksum
def clean(self):
""" Check validity of the BomItem model.

View File

@ -131,6 +131,7 @@ class BomItemSerializer(InvenTreeModelSerializer):
part_detail = PartBriefSerializer(source='part', many=False, read_only=True)
sub_part_detail = PartBriefSerializer(source='sub_part', many=False, read_only=True)
price_range = serializers.CharField(read_only=True)
validated = serializers.BooleanField(read_only=True, source='is_line_valid')
def __init__(self, *args, **kwargs):
# part_detail and sub_part_detail serializers are only included if requested.
@ -171,4 +172,5 @@ class BomItemSerializer(InvenTreeModelSerializer):
'price_range',
'overage',
'note',
'validated',
]

View File

@ -2,4 +2,9 @@
{% block pre_form_content %}
Confirm that the Bill of Materials (BOM) is valid for:<br><i>{{ part.full_name }}</i>
<div class='alert alert-warning alert-block'>
This will validate each line in the BOM.
</div>
{% endblock %}

View File

@ -120,7 +120,7 @@ class PartAPITest(APITestCase):
def test_get_bom_detail(self):
# Get the detail for a single BomItem
url = reverse('api-bom-detail', kwargs={'pk': 3})
url = reverse('api-bom-item-detail', kwargs={'pk': 3})
response = self.client.get(url, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['quantity'], 25)

View File

@ -18,6 +18,7 @@ migrate:
python3 InvenTree/manage.py migrate
python3 InvenTree/manage.py migrate --run-syncdb
python3 InvenTree/manage.py check
python3 InvenTree/manage.py collectstatic
# Install all required packages
install: