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:
@ -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"""
|
||||
|
Reference in New Issue
Block a user