mirror of
https://github.com/inventree/InvenTree.git
synced 2025-07-18 02:36:31 +00:00
MPTT rebuild tweak (#10020)
* More efficient call * Tree save fix - Do not rebuild tree when not required - Reduce DB count for API test * Reduce query count even further * Reduce query limit for other tests * Fix for parent getter * Added condition * Remove duplicate check * Don't pre-fill tree values * perform atomic delete * Adjust unit tests
This commit is contained in:
@@ -718,6 +718,7 @@ class InvenTreeTree(MPTTModel):
|
|||||||
if parent:
|
if parent:
|
||||||
# If we have a parent, use the parent's tree_id
|
# If we have a parent, use the parent's tree_id
|
||||||
self.tree_id = parent.tree_id
|
self.tree_id = parent.tree_id
|
||||||
|
self.level = parent.level + 1
|
||||||
else:
|
else:
|
||||||
# Otherwise, we need to generate a new tree_id
|
# Otherwise, we need to generate a new tree_id
|
||||||
self.tree_id = self.getNextTreeID()
|
self.tree_id = self.getNextTreeID()
|
||||||
@@ -736,23 +737,27 @@ class InvenTreeTree(MPTTModel):
|
|||||||
|
|
||||||
trees = set()
|
trees = set()
|
||||||
|
|
||||||
|
parent = getattr(self, self.NODE_PARENT_KEY, None)
|
||||||
|
|
||||||
if db_instance:
|
if db_instance:
|
||||||
# If the tree_id or parent has changed, we need to rebuild the tree
|
# If the tree_id or parent has changed, we need to rebuild the tree
|
||||||
if getattr(db_instance, self.NODE_PARENT_KEY) != getattr(
|
if getattr(db_instance, self.NODE_PARENT_KEY) != parent:
|
||||||
self, self.NODE_PARENT_KEY
|
|
||||||
):
|
|
||||||
trees.add(db_instance.tree_id)
|
trees.add(db_instance.tree_id)
|
||||||
if db_instance.tree_id != self.tree_id:
|
if db_instance.tree_id != self.tree_id:
|
||||||
trees.add(self.tree_id)
|
trees.add(self.tree_id)
|
||||||
trees.add(db_instance.tree_id)
|
trees.add(db_instance.tree_id)
|
||||||
else:
|
elif parent:
|
||||||
# New instance, so we need to rebuild the tree
|
# New instance, so we need to rebuild the tree (if it has a parent)
|
||||||
trees.add(self.tree_id)
|
trees.add(self.tree_id)
|
||||||
|
|
||||||
for tree_id in trees:
|
for tree_id in trees:
|
||||||
if tree_id:
|
if tree_id:
|
||||||
self.partial_rebuild(tree_id)
|
self.partial_rebuild(tree_id)
|
||||||
|
|
||||||
|
if len(trees) > 0:
|
||||||
|
# A tree update was performed, so we need to refresh the instance
|
||||||
|
self.refresh_from_db()
|
||||||
|
|
||||||
def partial_rebuild(self, tree_id: int) -> bool:
|
def partial_rebuild(self, tree_id: int) -> bool:
|
||||||
"""Perform a partial rebuild of the tree structure.
|
"""Perform a partial rebuild of the tree structure.
|
||||||
|
|
||||||
|
@@ -166,6 +166,7 @@ class BuildTest(BuildAPITest):
|
|||||||
# We shall complete 4 of these outputs
|
# We shall complete 4 of these outputs
|
||||||
outputs = self.build.incomplete_outputs.all()
|
outputs = self.build.incomplete_outputs.all()
|
||||||
|
|
||||||
|
# TODO: (2025-07-15) Try to optimize this API query to reduce DB hits
|
||||||
self.post(
|
self.post(
|
||||||
self.url,
|
self.url,
|
||||||
{
|
{
|
||||||
@@ -174,7 +175,7 @@ class BuildTest(BuildAPITest):
|
|||||||
'status': StockStatus.ATTENTION.value,
|
'status': StockStatus.ATTENTION.value,
|
||||||
},
|
},
|
||||||
expected_code=201,
|
expected_code=201,
|
||||||
max_query_count=600, # TODO: Try to optimize this
|
max_query_count=400,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(self.build.incomplete_outputs.count(), 0)
|
self.assertEqual(self.build.incomplete_outputs.count(), 0)
|
||||||
@@ -976,7 +977,7 @@ class BuildOverallocationTest(BuildAPITest):
|
|||||||
self.url,
|
self.url,
|
||||||
{'accept_overallocated': 'accept'},
|
{'accept_overallocated': 'accept'},
|
||||||
expected_code=201,
|
expected_code=201,
|
||||||
max_query_count=1000, # TODO: Come back and refactor this
|
max_query_count=375,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.build.refresh_from_db()
|
self.build.refresh_from_db()
|
||||||
@@ -995,7 +996,7 @@ class BuildOverallocationTest(BuildAPITest):
|
|||||||
self.url,
|
self.url,
|
||||||
{'accept_overallocated': 'trim'},
|
{'accept_overallocated': 'trim'},
|
||||||
expected_code=201,
|
expected_code=201,
|
||||||
max_query_count=1000, # TODO: Come back and refactor this
|
max_query_count=375,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Note: Large number of queries is due to pricing recalculation for each stock item
|
# Note: Large number of queries is due to pricing recalculation for each stock item
|
||||||
|
@@ -199,6 +199,8 @@ class BuildTreeTest(InvenTreeTestCase):
|
|||||||
|
|
||||||
# Test the tree structure for each node
|
# Test the tree structure for each node
|
||||||
for idx, child in enumerate(builds):
|
for idx, child in enumerate(builds):
|
||||||
|
child.refresh_from_db()
|
||||||
|
|
||||||
# Check parent-child relationships
|
# Check parent-child relationships
|
||||||
expected_parent = builds[idx - 1] if idx > 0 else None
|
expected_parent = builds[idx - 1] if idx > 0 else None
|
||||||
self.assertEqual(child.parent, expected_parent)
|
self.assertEqual(child.parent, expected_parent)
|
||||||
@@ -278,12 +280,16 @@ class BuildTreeTest(InvenTreeTestCase):
|
|||||||
self.assertEqual(grandchild.tree_id, self.build.tree_id)
|
self.assertEqual(grandchild.tree_id, self.build.tree_id)
|
||||||
self.assertEqual(grandchild.level, 2)
|
self.assertEqual(grandchild.level, 2)
|
||||||
|
|
||||||
|
child.refresh_from_db()
|
||||||
|
|
||||||
self.assertEqual(child.get_children().count(), 3)
|
self.assertEqual(child.get_children().count(), 3)
|
||||||
self.assertEqual(child.get_descendants(include_self=False).count(), 3)
|
self.assertEqual(child.get_descendants(include_self=False).count(), 3)
|
||||||
|
|
||||||
self.assertEqual(child.level, 1)
|
self.assertEqual(child.level, 1)
|
||||||
self.assertEqual(child.tree_id, self.build.tree_id)
|
self.assertEqual(child.tree_id, self.build.tree_id)
|
||||||
|
|
||||||
|
self.build.refresh_from_db()
|
||||||
|
|
||||||
# Basic tests
|
# Basic tests
|
||||||
self.assertEqual(Build.objects.count(), 13)
|
self.assertEqual(Build.objects.count(), 13)
|
||||||
self.assertEqual(self.build.get_children().count(), 3)
|
self.assertEqual(self.build.get_children().count(), 3)
|
||||||
|
@@ -1182,8 +1182,7 @@ class PurchaseOrderReceiveTest(OrderTest):
|
|||||||
|
|
||||||
n = StockItem.objects.count()
|
n = StockItem.objects.count()
|
||||||
|
|
||||||
# TODO: 2024-12-10 - This API query needs to be refactored!
|
self.post(self.url, data, expected_code=201, max_query_count=275)
|
||||||
self.post(self.url, data, expected_code=201, max_query_count=500)
|
|
||||||
|
|
||||||
# Check that the expected number of stock items has been created
|
# Check that the expected number of stock items has been created
|
||||||
self.assertEqual(n + 11, StockItem.objects.count())
|
self.assertEqual(n + 11, StockItem.objects.count())
|
||||||
|
@@ -267,7 +267,13 @@ class CategoryTest(TestCase):
|
|||||||
and the correct ancestor tree is observed.
|
and the correct ancestor tree is observed.
|
||||||
"""
|
"""
|
||||||
# Clear out any existing parts
|
# Clear out any existing parts
|
||||||
Part.objects.all().delete()
|
for p in Part.objects.all():
|
||||||
|
if p.active:
|
||||||
|
p.refresh_from_db()
|
||||||
|
p.active = False
|
||||||
|
p.save()
|
||||||
|
|
||||||
|
p.delete()
|
||||||
|
|
||||||
# First, create a structured tree of part categories
|
# First, create a structured tree of part categories
|
||||||
A = PartCategory.objects.create(name='A', description='Top level category')
|
A = PartCategory.objects.create(name='A', description='Top level category')
|
||||||
|
@@ -73,19 +73,12 @@ def register_event(event, *args, **kwargs):
|
|||||||
registry.check_reload()
|
registry.check_reload()
|
||||||
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
for slug, plugin in registry.plugins.items():
|
for plugin in registry.with_mixin(PluginMixinEnum.EVENTS, active=True):
|
||||||
if not plugin.mixin_enabled(PluginMixinEnum.EVENTS):
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Only allow event registering for 'active' plugins
|
|
||||||
if not plugin.is_active():
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Let the plugin decide if it wants to process this event
|
# Let the plugin decide if it wants to process this event
|
||||||
if not plugin.wants_process_event(event):
|
if not plugin.wants_process_event(event):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
logger.debug("Registering callback for plugin '%s'", slug)
|
logger.debug("Registering callback for plugin '%s'", plugin.slug)
|
||||||
|
|
||||||
# This task *must* be processed by the background worker,
|
# This task *must* be processed by the background worker,
|
||||||
# unless we are running CI tests
|
# unless we are running CI tests
|
||||||
@@ -94,7 +87,7 @@ def register_event(event, *args, **kwargs):
|
|||||||
|
|
||||||
# Offload a separate task for each plugin
|
# Offload a separate task for each plugin
|
||||||
offload_task(
|
offload_task(
|
||||||
process_event, slug, event, *args, group='plugin', **kwargs
|
process_event, plugin.slug, event, *args, group='plugin', **kwargs
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@@ -811,7 +811,7 @@ class StockItem(
|
|||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
# If user information is provided, and no existing note exists, create one!
|
# If user information is provided, and no existing note exists, create one!
|
||||||
if user and self.tracking_info.count() == 0:
|
if user and add_note and self.tracking_info.count() == 0:
|
||||||
tracking_info = {'status': self.status}
|
tracking_info = {'status': self.status}
|
||||||
|
|
||||||
self.add_tracking_entry(
|
self.add_tracking_entry(
|
||||||
|
Reference in New Issue
Block a user