mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
commit
8fe7284173
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
@ -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'),
|
||||
|
18
InvenTree/part/migrations/0017_bomitem_checksum.py
Normal file
18
InvenTree/part/migrations/0017_bomitem_checksum.py
Normal 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),
|
||||
),
|
||||
]
|
@ -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.
|
||||
|
||||
|
@ -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',
|
||||
]
|
||||
|
@ -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 %}
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user