mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Update data migration file (#4596)
* Update data migration file - Do not call "save()" on the ORM model as it has some other hooks - Manually calculate checksum to determine which line items are "valid" - Update BomItem checksum calculation * Update BomItem hashing function - Ensure it remains the same after saving - Must use normalize(quantity) otherwise weird issues
This commit is contained in:
parent
f70bde02d2
commit
27892f7652
@ -1,21 +1,94 @@
|
||||
# Generated by Django 3.2.18 on 2023-03-14 01:12
|
||||
|
||||
import hashlib
|
||||
import logging
|
||||
|
||||
from django.db import migrations
|
||||
from jinja2 import Template
|
||||
|
||||
from InvenTree.helpers import normalize
|
||||
|
||||
|
||||
logger = logging.getLogger('inventree')
|
||||
|
||||
|
||||
def update_bom_item(apps, schema_editor):
|
||||
"""Update all existing BomItem instances"""
|
||||
"""Update all existing BomItem instances, and cache the 'validated' field.
|
||||
|
||||
from part.models import BomItem
|
||||
The 'validated' field denotes whether this individual BomItem has been validated,
|
||||
which previously was calculated on the fly (which was very expensive).
|
||||
"""
|
||||
|
||||
if n := BomItem.objects.count():
|
||||
BomItem = apps.get_model('part', 'bomitem')
|
||||
InvenTreeSetting = apps.get_model('common', 'inventreesetting')
|
||||
|
||||
n = BomItem.objects.count()
|
||||
|
||||
if n > 0:
|
||||
|
||||
for item in BomItem.objects.all():
|
||||
"""For each item, we need to re-calculate the "checksum", based on the *old* routine.
|
||||
Note that as we cannot access the ORM models, we have to do this "by hand"
|
||||
"""
|
||||
|
||||
# Construct the 'full_name' for the sub_part (this is no longer required, but *was* required at point of migration)
|
||||
try:
|
||||
setting = InvenTreeSetting.objects.get(key='PART_NAME_FORMAT')
|
||||
full_name_pattern = str(setting.value)
|
||||
except Exception:
|
||||
full_name_pattern = "{{ part.IPN if part.IPN }}{{ ' | ' if part.IPN }}{{ part.name }}{{ ' | ' if part.revision }}{{ part.revision if part.revision }}"
|
||||
|
||||
template = Template(full_name_pattern)
|
||||
|
||||
full_name = template.render({'part': item.sub_part})
|
||||
|
||||
# Calculate the OLD checksum manually for this BomItem
|
||||
old_hash = hashlib.md5(str(item.pk).encode())
|
||||
old_hash.update(str(item.sub_part.pk).encode())
|
||||
old_hash.update(str(full_name).encode())
|
||||
old_hash.update(str(item.quantity).encode())
|
||||
old_hash.update(str(item.note).encode())
|
||||
old_hash.update(str(item.reference).encode())
|
||||
old_hash.update(str(item.optional).encode())
|
||||
old_hash.update(str(item.inherited).encode())
|
||||
|
||||
if item.consumable:
|
||||
old_hash.update(str(item.consumable).encode())
|
||||
|
||||
if item.allow_variants:
|
||||
old_hash.update(str(item.allow_variants).encode())
|
||||
|
||||
checksum = str(old_hash.digest())
|
||||
|
||||
# Now, update the 'validated' field based on whether the checksum is 'valid' or not
|
||||
item.validated = item.checksum == checksum
|
||||
|
||||
"""Next, we need to update the item with a "new" hash, with the following differences:
|
||||
- Uses the PK of the 'part', not the BomItem itself,
|
||||
- Does not use the 'full_name' of the linked 'sub_part'
|
||||
- Does not use the 'note' field
|
||||
"""
|
||||
|
||||
if item.validated:
|
||||
|
||||
new_hash = hashlib.md5(''.encode())
|
||||
|
||||
components = [
|
||||
item.part.pk,
|
||||
item.sub_part.pk,
|
||||
normalize(item.quantity),
|
||||
item.reference,
|
||||
item.optional,
|
||||
item.inherited,
|
||||
item.consumable,
|
||||
item.allow_variants
|
||||
]
|
||||
|
||||
for component in components:
|
||||
new_hash.update(str(component).encode())
|
||||
|
||||
item.checksum = str(new_hash.digest())
|
||||
|
||||
item.save()
|
||||
|
||||
logger.info(f"Updated 'validated' flag for {n} BomItem objects")
|
||||
|
@ -3631,32 +3631,32 @@ class BomItem(DataImportMixin, MetadataMixin, models.Model):
|
||||
"""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
|
||||
- Optional field
|
||||
- Inherited field
|
||||
- part.pk
|
||||
- sub_part.pk
|
||||
- quantity
|
||||
- reference
|
||||
- optional
|
||||
- inherited
|
||||
- consumable
|
||||
- allow_variants
|
||||
"""
|
||||
# Seed the hash with the ID of this BOM item
|
||||
result_hash = hashlib.md5(str(self.id).encode())
|
||||
result_hash = hashlib.md5(''.encode())
|
||||
|
||||
# Update the hash based on line information
|
||||
result_hash.update(str(self.sub_part.id).encode())
|
||||
result_hash.update(str(self.sub_part.full_name).encode())
|
||||
result_hash.update(str(self.quantity).encode())
|
||||
result_hash.update(str(self.note).encode())
|
||||
result_hash.update(str(self.reference).encode())
|
||||
result_hash.update(str(self.optional).encode())
|
||||
result_hash.update(str(self.inherited).encode())
|
||||
# The following components are used to calculate the checksum
|
||||
components = [
|
||||
self.part.pk,
|
||||
self.sub_part.pk,
|
||||
normalize(self.quantity),
|
||||
self.reference,
|
||||
self.optional,
|
||||
self.inherited,
|
||||
self.consumable,
|
||||
self.allow_variants
|
||||
]
|
||||
|
||||
# Optionally encoded for backwards compatibility
|
||||
if self.consumable:
|
||||
result_hash.update(str(self.consumable).encode())
|
||||
|
||||
# Optionally encoded for backwards compatibility
|
||||
if self.allow_variants:
|
||||
result_hash.update(str(self.allow_variants).encode())
|
||||
for component in components:
|
||||
result_hash.update(str(component).encode())
|
||||
|
||||
return str(result_hash.digest())
|
||||
|
||||
@ -3667,7 +3667,7 @@ class BomItem(DataImportMixin, MetadataMixin, models.Model):
|
||||
valid: If true, validate the hash, otherwise invalidate it (default = True)
|
||||
"""
|
||||
if valid:
|
||||
self.checksum = str(self.get_item_hash())
|
||||
self.checksum = self.get_item_hash()
|
||||
else:
|
||||
self.checksum = ''
|
||||
|
||||
|
@ -46,3 +46,39 @@ class TestForwardMigrations(MigratorTestCase):
|
||||
for name in ['A', 'C', 'E']:
|
||||
part = Part.objects.get(name=name)
|
||||
self.assertEqual(part.description, f"My part {name}")
|
||||
|
||||
|
||||
class TestBomItemMigrations(MigratorTestCase):
|
||||
"""Tests for BomItem migrations"""
|
||||
|
||||
migrate_from = ('part', '0002_auto_20190520_2204')
|
||||
migrate_to = ('part', helpers.getNewestMigrationFile('part'))
|
||||
|
||||
def prepare(self):
|
||||
"""Create intial dataset"""
|
||||
|
||||
Part = self.old_state.apps.get_model('part', 'part')
|
||||
BomItem = self.old_state.apps.get_model('part', 'bomitem')
|
||||
|
||||
a = Part.objects.create(name='Part A', description='My part A')
|
||||
b = Part.objects.create(name='Part B', description='My part B')
|
||||
c = Part.objects.create(name='Part C', description='My part C')
|
||||
|
||||
BomItem.objects.create(part=a, sub_part=b, quantity=1)
|
||||
BomItem.objects.create(part=a, sub_part=c, quantity=1)
|
||||
|
||||
self.assertEqual(BomItem.objects.count(), 2)
|
||||
|
||||
# Initially we don't have the 'validated' field
|
||||
with self.assertRaises(AttributeError):
|
||||
print(b.validated)
|
||||
|
||||
def test_validated_field(self):
|
||||
"""Test that the 'validated' field is added to the BomItem objects"""
|
||||
|
||||
BomItem = self.new_state.apps.get_model('part', 'bomitem')
|
||||
|
||||
self.assertEqual(BomItem.objects.count(), 2)
|
||||
|
||||
for bom_item in BomItem.objects.all():
|
||||
self.assertFalse(bom_item.validated)
|
||||
|
Loading…
Reference in New Issue
Block a user