From 7671eb2b22b50a9428fe1ee97250a6991edf265d Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 5 Sep 2019 12:58:11 +1000 Subject: [PATCH 1/6] Add a checksum field to the bom line item --- .../part/migrations/0017_bomitem_checksum.py | 18 ++++++++++++++++++ InvenTree/part/models.py | 3 +++ 2 files changed, 21 insertions(+) create mode 100644 InvenTree/part/migrations/0017_bomitem_checksum.py diff --git a/InvenTree/part/migrations/0017_bomitem_checksum.py b/InvenTree/part/migrations/0017_bomitem_checksum.py new file mode 100644 index 0000000000..3dfaae7a09 --- /dev/null +++ b/InvenTree/part/migrations/0017_bomitem_checksum.py @@ -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), + ), + ] diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 4a4c5fb606..2eb8f9f842 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -1121,6 +1121,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 +1155,8 @@ 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 clean(self): """ Check validity of the BomItem model. From 0508c2dcaf1d763890b0852b91821a5d28b57e7b Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 5 Sep 2019 13:10:26 +1000 Subject: [PATCH 2/6] Use the hash for each line item to calculate the total BOM hash --- InvenTree/part/models.py | 53 +++++++++++++++++++++++++++-------- InvenTree/part/serializers.py | 2 ++ 2 files changed, 44 insertions(+), 11 deletions(-) diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 2eb8f9f842..5627375a48 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -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() @@ -1157,6 +1152,42 @@ class BomItem(models.Model): 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): + """ Mark this item as 'valid' (store the checksum hash) """ + + self.checksum = str(self.get_item_hash()) + self.save() + + @property + def is_line_valid(self): + """ Check if this line item has been validated by the user """ + + return self.get_item_hash() == self.checksum + def clean(self): """ Check validity of the BomItem model. diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index 47b34b292f..a8d0df5954 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -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', ] From 1ea7bdf843567abce044ed47e86afd8477727726 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 5 Sep 2019 13:12:49 +1000 Subject: [PATCH 3/6] Add a note --- InvenTree/part/templates/part/bom_validate.html | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/InvenTree/part/templates/part/bom_validate.html b/InvenTree/part/templates/part/bom_validate.html index f2c159349f..763d946bf2 100644 --- a/InvenTree/part/templates/part/bom_validate.html +++ b/InvenTree/part/templates/part/bom_validate.html @@ -2,4 +2,9 @@ {% block pre_form_content %} Confirm that the Bill of Materials (BOM) is valid for:
{{ part.full_name }} + +
+ This will validate each line in the BOM. +
+ {% endblock %} \ No newline at end of file From 81f5714cb144a01ffbb9799a9e8e8147e42d9c34 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 5 Sep 2019 14:15:58 +1000 Subject: [PATCH 4/6] BOM table now displays which lines have been marked as valid --- InvenTree/InvenTree/static/css/inventree.css | 9 ++++++ .../InvenTree/static/script/inventree/bom.js | 31 ++++++++++++++++++- Makefile | 1 + 3 files changed, 40 insertions(+), 1 deletion(-) diff --git a/InvenTree/InvenTree/static/css/inventree.css b/InvenTree/InvenTree/static/css/inventree.css index 3f64633062..d9515dbff9 100644 --- a/InvenTree/InvenTree/static/css/inventree.css +++ b/InvenTree/InvenTree/static/css/inventree.css @@ -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 { diff --git a/InvenTree/InvenTree/static/script/inventree/bom.js b/InvenTree/InvenTree/static/script/inventree/bom.js index 450fdeb503..2da80c6138 100644 --- a/InvenTree/InvenTree/static/script/inventree/bom.js +++ b/InvenTree/InvenTree/static/script/inventree/bom.js @@ -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 = ""; + var bValid = ""; + var bEdit = ""; var bDelt = ""; - return "
" + bEdit + bDelt + "
"; + var html = "
"; + + html += bEdit; + html += bDelt; + + if (!row.validated) { + html += bValidate; + } else { + html += bValid; + } + + html += "
"; + + 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, diff --git a/Makefile b/Makefile index 0b3379a724..aaf795144e 100644 --- a/Makefile +++ b/Makefile @@ -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: From 37d9c59a0ea63fe08836a271658804df6f89d38f Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 5 Sep 2019 19:29:51 +1000 Subject: [PATCH 5/6] Add API endpoint for validating a BOM item --- InvenTree/part/api.py | 41 +++++++++++++++++++++++++++++++++++--- InvenTree/part/models.py | 18 ++++++++++++++--- InvenTree/part/test_api.py | 2 +- 3 files changed, 54 insertions(+), 7 deletions(-) diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index 1c6678f2d3..e9c6d32f0d 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -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\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\d+)/?', BomDetail.as_view(), name='api-bom-detail'), + url(r'^(?P\d+)/', include(bom_item_urls)), # Catch-all url(r'^.*$', BomList.as_view(), name='api-bom-list'), diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 5627375a48..b13b6632c1 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -1176,16 +1176,28 @@ class BomItem(models.Model): return str(hash.digest()) - def validate_hash(self): - """ Mark this item as 'valid' (store the checksum hash) """ + 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.checksum = str(self.get_item_hash()) 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): diff --git a/InvenTree/part/test_api.py b/InvenTree/part/test_api.py index b0859e5c1e..1b1ef3bc07 100644 --- a/InvenTree/part/test_api.py +++ b/InvenTree/part/test_api.py @@ -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) From 7659f2de7bfe2b05fb56147240561c2fdcf4d019 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 5 Sep 2019 19:34:58 +1000 Subject: [PATCH 6/6] Click button to validate BOM item --- .../InvenTree/static/script/inventree/bom.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/InvenTree/InvenTree/static/script/inventree/bom.js b/InvenTree/InvenTree/static/script/inventree/bom.js index 2da80c6138..1e66af5c7e 100644 --- a/InvenTree/InvenTree/static/script/inventree/bom.js +++ b/InvenTree/InvenTree/static/script/inventree/bom.js @@ -317,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 + } + ); + }); } } \ No newline at end of file