2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-06-20 05:46:34 +00:00

Merge branch 'master' of https://github.com/inventree/InvenTree into homepage-settings

This commit is contained in:
2021-07-23 00:03:21 +02:00
50 changed files with 2692 additions and 177 deletions

View File

@ -105,6 +105,20 @@ class CategoryList(generics.ListCreateAPIView):
except (ValueError, PartCategory.DoesNotExist):
pass
# Exclude PartCategory tree
exclude_tree = params.get('exclude_tree', None)
if exclude_tree is not None:
try:
cat = PartCategory.objects.get(pk=exclude_tree)
queryset = queryset.exclude(
pk__in=[c.pk for c in cat.get_descendants(include_self=True)]
)
except (ValueError, PartCategory.DoesNotExist):
pass
return queryset
filter_backends = [
@ -361,7 +375,6 @@ class PartDetail(generics.RetrieveUpdateDestroyAPIView):
def get_queryset(self, *args, **kwargs):
queryset = super().get_queryset(*args, **kwargs)
queryset = part_serializers.PartSerializer.prefetch_queryset(queryset)
queryset = part_serializers.PartSerializer.annotate_queryset(queryset)
return queryset
@ -619,8 +632,6 @@ class PartList(generics.ListCreateAPIView):
def get_queryset(self, *args, **kwargs):
queryset = super().get_queryset(*args, **kwargs)
queryset = part_serializers.PartSerializer.prefetch_queryset(queryset)
queryset = part_serializers.PartSerializer.annotate_queryset(queryset)
return queryset
@ -633,10 +644,6 @@ class PartList(generics.ListCreateAPIView):
params = self.request.query_params
# Annotate calculated data to the queryset
# (This will be used for further filtering)
queryset = part_serializers.PartSerializer.annotate_queryset(queryset)
queryset = super().filter_queryset(queryset)
# Filter by "uses" query - Limit to parts which use the provided part
@ -651,6 +658,20 @@ class PartList(generics.ListCreateAPIView):
except (ValueError, Part.DoesNotExist):
pass
# Exclude part variant tree?
exclude_tree = params.get('exclude_tree', None)
if exclude_tree is not None:
try:
top_level_part = Part.objects.get(pk=exclude_tree)
queryset = queryset.exclude(
pk__in=[prt.pk for prt in top_level_part.get_descendants(include_self=True)]
)
except (ValueError, Part.DoesNotExist):
pass
# Filter by 'ancestor'?
ancestor = params.get('ancestor', None)

View File

@ -27,6 +27,8 @@ from markdownx.models import MarkdownxField
from django_cleanup import cleanup
from mptt.models import TreeForeignKey, MPTTModel
from mptt.exceptions import InvalidMove
from mptt.managers import TreeManager
from stdimage.models import StdImageField
@ -284,6 +286,24 @@ def match_part_names(match, threshold=80, reverse=True, compare_length=False):
return matches
class PartManager(TreeManager):
"""
Defines a custom object manager for the Part model.
The main purpose of this manager is to reduce the number of database hits,
as the Part model has a large number of ForeignKey fields!
"""
def get_queryset(self):
return super().get_queryset().prefetch_related(
'category',
'category__parent',
'stock_items',
'builds',
)
@cleanup.ignore
class Part(MPTTModel):
""" The Part object represents an abstract part, the 'concept' of an actual entity.
@ -321,6 +341,8 @@ class Part(MPTTModel):
responsible: User who is responsible for this part (optional)
"""
objects = PartManager()
class Meta:
verbose_name = _("Part")
verbose_name_plural = _("Parts")
@ -338,6 +360,17 @@ class Part(MPTTModel):
return reverse('api-part-list')
def api_instance_filters(self):
"""
Return API query filters for limiting field results against this instance
"""
return {
'variant_of': {
'exclude_tree': self.pk,
}
}
def get_context_data(self, request, **kwargs):
"""
Return some useful context data about this part for template rendering
@ -393,7 +426,12 @@ class Part(MPTTModel):
self.full_clean()
super().save(*args, **kwargs)
try:
super().save(*args, **kwargs)
except InvalidMove:
raise ValidationError({
'variant_of': _('Invalid choice for parent part'),
})
if add_category_templates:
# Get part category
@ -1473,16 +1511,16 @@ class Part(MPTTModel):
return self.supplier_parts.count()
@property
def has_pricing_info(self):
def has_pricing_info(self, internal=False):
""" Return true if there is pricing information for this part """
return self.get_price_range() is not None
return self.get_price_range(internal=internal) is not None
@property
def has_complete_bom_pricing(self):
""" Return true if there is pricing information for each item in the BOM. """
use_internal = common.models.get_setting('PART_BOM_USE_INTERNAL_PRICE', False)
for item in self.get_bom_items().all().select_related('sub_part'):
if not item.sub_part.has_pricing_info:
if not item.sub_part.has_pricing_info(use_internal):
return False
return True
@ -1866,6 +1904,23 @@ class Part(MPTTModel):
return self.parameters.order_by('template__name')
def parameters_map(self):
"""
Return a map (dict) of parameter values assocaited with this Part instance,
of the form:
{
"name_1": "value_1",
"name_2": "value_2",
}
"""
params = {}
for parameter in self.parameters.all():
params[parameter.template.name] = parameter.data
return params
@property
def has_variants(self):
""" Check if this Part object has variants underneath it. """

View File

@ -215,25 +215,6 @@ class PartSerializer(InvenTreeModelSerializer):
if category_detail is not True:
self.fields.pop('category_detail')
@staticmethod
def prefetch_queryset(queryset):
"""
Prefetch related database tables,
to reduce database hits.
"""
return queryset.prefetch_related(
'category',
'category__parts',
'category__parent',
'stock_items',
'bom_items',
'builds',
'supplier_parts',
'supplier_parts__purchase_order_line_items',
'supplier_parts__purchase_order_line_items__order',
)
@staticmethod
def annotate_queryset(queryset):
"""

View File

@ -99,7 +99,7 @@ class CategoryTest(TestCase):
""" Test that the Category parameters are correctly fetched """
# Check number of SQL queries to iterate other parameters
with self.assertNumQueries(3):
with self.assertNumQueries(7):
# Prefetch: 3 queries (parts, parameters and parameters_template)
fasteners = self.fasteners.prefetch_parts_parameters()
# Iterate through all parts and parameters