2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-07-05 13:10:57 +00:00

Part page loading improvements (#3185)

* Lazy load the pricing bom table when the "pricing" tab is selected

* Update django-debug-toolbar configuration

* Major refactoring for the 'can_build' function

- Use a single annotated query to the db, rather than a for loop (which is what a caveman would use)
- Query performance is greatly improved
- Also refactors existing variant-part-stock subquery code, to make it re-usable

* Use minified JS and CSS where possible

* Render a 'preview' version of each part image

- Saves load time when the image is quite large
- Adds a data migration to render out the new variation

* Adds 'preview' version of company images

* Defer loading of javascript files

Note: some cannot be deferred - jquery in particular

* Crucial bugfix for user roles context

- Previously was *not* being calculated correctly
- A non-superuser role would most likely display pages incorrectly

* Prevent loading of "about" on every page

- Load dynamically when requested
- Takes ~400ms!
- Cuts out a lot of fat

* Match displayed image size to preview image size

* Utilize caching framework for accessing user "role" information

- Reduces number of DB queries required by rendering framework

* Remove redundant query elements

* Remove 'stock' field from PartBrief serializer

- A calculated field on a serializer is a *bad idea* when that calculation requires a DB hit

* Query improvements for StockItem serializer

- Remove calculated fields
- Fix annotations

* Bug fixes

* Remove JS load test

- Loading of JS files is now deferred, so the unit test does not work as it used to

* Fix broken template for "maintenance" page

* Remove thumbnail generation migrations

- Already performed manually as part of ''invoke migrate"
- Running as a migration causes unit test problems
- Not sensible to run this as a data-migration anyway

* tweak for build table
This commit is contained in:
Oliver
2022-06-17 21:26:28 +10:00
committed by GitHub
parent 0d01ea2f2e
commit 74bec86675
51 changed files with 3592 additions and 2212 deletions

View File

@ -13,7 +13,7 @@ from django.contrib.auth.models import User
from django.core.exceptions import ValidationError
from django.core.validators import MinValueValidator
from django.db import models, transaction
from django.db.models import Q, Sum, UniqueConstraint
from django.db.models import ExpressionWrapper, F, Q, Sum, UniqueConstraint
from django.db.models.functions import Coalesce
from django.db.models.signals import post_save
from django.db.utils import IntegrityError
@ -34,6 +34,7 @@ from stdimage.models import StdImageField
import common.models
import InvenTree.ready
import InvenTree.tasks
import part.filters as part_filters
import part.settings as part_settings
from build import models as BuildModels
from common.models import InvenTreeSetting
@ -74,9 +75,9 @@ class PartCategory(MetadataMixin, InvenTreeTree):
tree_id = self.tree_id
# Update each part in this category to point to the parent category
for part in self.parts.all():
part.category = self.parent
part.save()
for p in self.parts.all():
p.category = self.parent
p.save()
# Update each child category
for child in self.children.all():
@ -221,7 +222,7 @@ class PartCategory(MetadataMixin, InvenTreeTree):
if include_parents:
queryset = PartCategoryStar.objects.filter(
category__pk__in=[cat.pk for cat in cats]
category__in=cats,
)
else:
queryset = PartCategoryStar.objects.filter(
@ -800,7 +801,10 @@ class Part(MetadataMixin, MPTTModel):
upload_to=rename_part_image,
null=True,
blank=True,
variations={'thumbnail': (128, 128)},
variations={
'thumbnail': (128, 128),
'preview': (256, 256),
},
delete_orphans=False,
verbose_name=_('Image'),
)
@ -968,13 +972,10 @@ class Part(MetadataMixin, MPTTModel):
def requiring_build_orders(self):
"""Return list of outstanding build orders which require this part."""
# List parts that this part is required for
parts = self.get_used_in().all()
part_ids = [part.pk for part in parts]
# Now, get a list of outstanding build orders which require this part
builds = BuildModels.Build.objects.filter(
part__in=part_ids,
part__in=self.get_used_in().all(),
status__in=BuildStatus.ACTIVE_CODES
)
@ -1098,7 +1099,7 @@ class Part(MetadataMixin, MPTTModel):
if include_variants:
queryset = queryset.filter(
part__pk__in=[part.pk for part in self.get_ancestors(include_self=True)]
part__in=self.get_ancestors(include_self=True),
)
else:
queryset = queryset.filter(part=self)
@ -1142,18 +1143,70 @@ class Part(MetadataMixin, MPTTModel):
total = None
bom_items = self.get_bom_items().prefetch_related('sub_part__stock_items')
# Prefetch related tables, to reduce query expense
queryset = self.get_bom_items().prefetch_related(
'sub_part__stock_items',
'sub_part__stock_items__allocations',
'sub_part__stock_items__sales_order_allocations',
'substitutes',
'substitutes__part__stock_items',
)
# Calculate the minimum number of parts that can be built using each sub-part
for item in bom_items.all():
stock = item.sub_part.available_stock
# Annotate the 'available stock' for each part in the BOM
ref = 'sub_part__'
queryset = queryset.alias(
total_stock=part_filters.annotate_total_stock(reference=ref),
so_allocations=part_filters.annotate_sales_order_allocations(reference=ref),
bo_allocations=part_filters.annotate_build_order_allocations(reference=ref),
)
# If (by some chance) we get here but the BOM item quantity is invalid,
# ignore!
if item.quantity <= 0:
continue
# Calculate the 'available stock' based on previous annotations
queryset = queryset.annotate(
available_stock=ExpressionWrapper(
F('total_stock') - F('so_allocations') - F('bo_allocations'),
output_field=models.DecimalField(),
)
)
n = int(stock / item.quantity)
# Extract similar information for any 'substitute' parts
ref = 'substitutes__part__'
queryset = queryset.alias(
sub_total_stock=part_filters.annotate_total_stock(reference=ref),
sub_so_allocations=part_filters.annotate_sales_order_allocations(reference=ref),
sub_bo_allocations=part_filters.annotate_build_order_allocations(reference=ref),
)
queryset = queryset.annotate(
substitute_stock=ExpressionWrapper(
F('sub_total_stock') - F('sub_so_allocations') - F('sub_bo_allocations'),
output_field=models.DecimalField(),
)
)
# Extract similar information for any 'variant' parts
variant_stock_query = part_filters.variant_stock_query(reference='sub_part__')
queryset = queryset.alias(
var_total_stock=part_filters.annotate_variant_quantity(variant_stock_query, reference='quantity'),
var_bo_allocations=part_filters.annotate_variant_quantity(variant_stock_query, reference='allocations__quantity'),
var_so_allocations=part_filters.annotate_variant_quantity(variant_stock_query, reference='sales_order_allocations__quantity'),
)
queryset = queryset.annotate(
variant_stock=ExpressionWrapper(
F('var_total_stock') - F('var_bo_allocations') - F('var_so_allocations'),
output_field=models.DecimalField(),
)
)
for item in queryset.all():
# Iterate through each item in the queryset, work out the limiting quantity
quantity = item.available_stock + item.substitute_stock
if item.allow_variants:
quantity += item.variant_stock
n = int(quantity / item.quantity)
if total is None or n < total:
total = n
@ -1336,11 +1389,10 @@ class Part(MetadataMixin, MPTTModel):
parents = self.get_ancestors(include_self=False)
# There are parents available
if parents.count() > 0:
parent_ids = [p.pk for p in parents]
if parents.exists():
parent_filter = Q(
part__id__in=parent_ids,
part__in=parents,
inherited=True
)
@ -1425,7 +1477,7 @@ class Part(MetadataMixin, MPTTModel):
@property
def has_bom(self):
"""Return True if this Part instance has any BOM items"""
return self.get_bom_items().count() > 0
return self.get_bom_items().exists()
def get_trackable_parts(self):
"""Return a queryset of all trackable parts in the BOM for this part."""
@ -1440,7 +1492,7 @@ class Part(MetadataMixin, MPTTModel):
This is important when building the part.
"""
return self.get_trackable_parts().count() > 0
return self.get_trackable_parts().exists()
@property
def bom_count(self):
@ -1482,7 +1534,7 @@ class Part(MetadataMixin, MPTTModel):
# Validate each line item, ignoring inherited ones
bom_items = self.get_bom_items(include_inherited=False)
for item in bom_items.all():
for item in bom_items:
item.validate_hash()
self.bom_checksum = self.get_bom_hash()
@ -1509,7 +1561,7 @@ class Part(MetadataMixin, MPTTModel):
if parts is None:
parts = set()
bom_items = self.get_bom_items().all()
bom_items = self.get_bom_items()
for bom_item in bom_items:
@ -1533,7 +1585,7 @@ class Part(MetadataMixin, MPTTModel):
def has_complete_bom_pricing(self):
"""Return true if there is pricing information for each item in the BOM."""
use_internal = common.models.InvenTreeSetting.get_setting('PART_BOM_USE_INTERNAL_PRICE', False)
for item in self.get_bom_items().all().select_related('sub_part'):
for item in self.get_bom_items().select_related('sub_part'):
if item.sub_part.get_price_range(internal=use_internal) is None:
return False
@ -1609,7 +1661,7 @@ class Part(MetadataMixin, MPTTModel):
min_price = None
max_price = None
for item in self.get_bom_items().all().select_related('sub_part'):
for item in self.get_bom_items().select_related('sub_part'):
if item.sub_part.pk == self.pk:
logger.warning(f"WARNING: BomItem ID {item.pk} contains itself in BOM")
@ -1689,7 +1741,7 @@ class Part(MetadataMixin, MPTTModel):
@property
def has_price_breaks(self):
"""Return True if this part has sale price breaks"""
return self.price_breaks.count() > 0
return self.price_breaks.exists()
@property
def price_breaks(self):
@ -1725,7 +1777,7 @@ class Part(MetadataMixin, MPTTModel):
@property
def has_internal_price_breaks(self):
"""Return True if this Part has internal pricing information"""
return self.internal_price_breaks.count() > 0
return self.internal_price_breaks.exists()
@property
def internal_price_breaks(self):
@ -1978,7 +2030,7 @@ class Part(MetadataMixin, MPTTModel):
@property
def has_variants(self):
"""Check if this Part object has variants underneath it."""
return self.get_all_variants().count() > 0
return self.get_all_variants().exists()
def get_all_variants(self):
"""Return all Part object which exist as a variant under this part."""
@ -1993,7 +2045,7 @@ class Part(MetadataMixin, MPTTModel):
b) It has non-virtual template parts above it
c) It has non-virtual sibling variants
"""
return self.get_conversion_options().count() > 0
return self.get_conversion_options().exists()
def get_conversion_options(self):
"""Return options for converting this part to a "variant" within the same tree.
@ -2520,7 +2572,7 @@ class BomItem(DataImportMixin, models.Model):
- Allow stock from all directly specified substitute parts
- If allow_variants is True, allow all part variants
"""
return Q(part__in=[part.pk for part in self.get_valid_parts_for_allocation()])
return Q(part__in=self.get_valid_parts_for_allocation())
def save(self, *args, **kwargs):
"""Enforce 'clean' operation when saving a BomItem instance"""