mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-11-03 22:55:43 +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:
		@@ -19,7 +19,7 @@ Relevant PRs:
 | 
			
		||||
from decimal import Decimal
 | 
			
		||||
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.db.models import OuterRef, Q
 | 
			
		||||
from django.db.models import F, FloatField, Func, OuterRef, Q, Subquery
 | 
			
		||||
from django.db.models.functions import Coalesce
 | 
			
		||||
 | 
			
		||||
from sql_util.utils import SubquerySum
 | 
			
		||||
@@ -139,3 +139,22 @@ def variant_stock_query(reference: str = '', filter: Q = stock.models.StockItem.
 | 
			
		||||
        part__lft__gt=OuterRef(f'{reference}lft'),
 | 
			
		||||
        part__rght__lt=OuterRef(f'{reference}rght'),
 | 
			
		||||
    ).filter(filter)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def annotate_variant_quantity(subquery: Q, reference: str = 'quantity'):
 | 
			
		||||
    """Create a subquery annotation for all variant part stock items on the given parent query
 | 
			
		||||
 | 
			
		||||
    Args:
 | 
			
		||||
        subquery: A 'variant_stock_query' Q object
 | 
			
		||||
        reference: The relationship reference of the variant stock items from the current queryset
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    return Coalesce(
 | 
			
		||||
        Subquery(
 | 
			
		||||
            subquery.annotate(
 | 
			
		||||
                total=Func(F(reference), function='SUM', output_field=FloatField())
 | 
			
		||||
            ).values('total')
 | 
			
		||||
        ),
 | 
			
		||||
        0,
 | 
			
		||||
        output_field=FloatField(),
 | 
			
		||||
    )
 | 
			
		||||
 
 | 
			
		||||
@@ -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"""
 | 
			
		||||
 
 | 
			
		||||
@@ -4,8 +4,7 @@ import imghdr
 | 
			
		||||
from decimal import Decimal
 | 
			
		||||
 | 
			
		||||
from django.db import models, transaction
 | 
			
		||||
from django.db.models import (ExpressionWrapper, F, FloatField, Func, Q,
 | 
			
		||||
                              Subquery)
 | 
			
		||||
from django.db.models import ExpressionWrapper, F, FloatField, Q
 | 
			
		||||
from django.db.models.functions import Coalesce
 | 
			
		||||
from django.urls import reverse_lazy
 | 
			
		||||
from django.utils.translation import gettext_lazy as _
 | 
			
		||||
@@ -251,8 +250,6 @@ class PartBriefSerializer(InvenTreeModelSerializer):
 | 
			
		||||
 | 
			
		||||
    thumbnail = serializers.CharField(source='get_thumbnail_url', read_only=True)
 | 
			
		||||
 | 
			
		||||
    stock = serializers.FloatField(source='total_stock')
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        """Metaclass defining serializer fields"""
 | 
			
		||||
        model = Part
 | 
			
		||||
@@ -270,7 +267,6 @@ class PartBriefSerializer(InvenTreeModelSerializer):
 | 
			
		||||
            'is_template',
 | 
			
		||||
            'purchaseable',
 | 
			
		||||
            'salable',
 | 
			
		||||
            'stock',
 | 
			
		||||
            'trackable',
 | 
			
		||||
            'virtual',
 | 
			
		||||
            'units',
 | 
			
		||||
@@ -322,14 +318,7 @@ class PartSerializer(InvenTreeModelSerializer):
 | 
			
		||||
        variant_query = part.filters.variant_stock_query()
 | 
			
		||||
 | 
			
		||||
        queryset = queryset.annotate(
 | 
			
		||||
            variant_stock=Coalesce(
 | 
			
		||||
                Subquery(
 | 
			
		||||
                    variant_query.annotate(
 | 
			
		||||
                        total=Func(F('quantity'), function='SUM', output_field=FloatField())
 | 
			
		||||
                    ).values('total')),
 | 
			
		||||
                0,
 | 
			
		||||
                output_field=FloatField(),
 | 
			
		||||
            )
 | 
			
		||||
            variant_stock=part.filters.annotate_variant_quantity(variant_query, reference='quantity'),
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # Filter to limit builds to "active"
 | 
			
		||||
@@ -642,35 +631,14 @@ class BomItemSerializer(InvenTreeModelSerializer):
 | 
			
		||||
        variant_stock_query = part.filters.variant_stock_query(reference='sub_part__')
 | 
			
		||||
 | 
			
		||||
        queryset = queryset.alias(
 | 
			
		||||
            variant_stock_total=Coalesce(
 | 
			
		||||
                Subquery(
 | 
			
		||||
                    variant_stock_query.annotate(
 | 
			
		||||
                        total=Func(F('quantity'), function='SUM', output_field=FloatField())
 | 
			
		||||
                    ).values('total')),
 | 
			
		||||
                0,
 | 
			
		||||
                output_field=FloatField()
 | 
			
		||||
            ),
 | 
			
		||||
            variant_stock_build_order_allocations=Coalesce(
 | 
			
		||||
                Subquery(
 | 
			
		||||
                    variant_stock_query.annotate(
 | 
			
		||||
                        total=Func(F('sales_order_allocations__quantity'), function='SUM', output_field=FloatField()),
 | 
			
		||||
                    ).values('total')),
 | 
			
		||||
                0,
 | 
			
		||||
                output_field=FloatField(),
 | 
			
		||||
            ),
 | 
			
		||||
            variant_stock_sales_order_allocations=Coalesce(
 | 
			
		||||
                Subquery(
 | 
			
		||||
                    variant_stock_query.annotate(
 | 
			
		||||
                        total=Func(F('allocations__quantity'), function='SUM', output_field=FloatField()),
 | 
			
		||||
                    ).values('total')),
 | 
			
		||||
                0,
 | 
			
		||||
                output_field=FloatField(),
 | 
			
		||||
            )
 | 
			
		||||
            variant_stock_total=part.filters.annotate_variant_quantity(variant_stock_query, reference='quantity'),
 | 
			
		||||
            variant_bo_allocations=part.filters.annotate_variant_quantity(variant_stock_query, reference='sales_order_allocations__quantity'),
 | 
			
		||||
            variant_so_allocations=part.filters.annotate_variant_quantity(variant_stock_query, reference='allocations__quantity'),
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        queryset = queryset.annotate(
 | 
			
		||||
            available_variant_stock=ExpressionWrapper(
 | 
			
		||||
                F('variant_stock_total') - F('variant_stock_build_order_allocations') - F('variant_stock_sales_order_allocations'),
 | 
			
		||||
                F('variant_stock_total') - F('variant_bo_allocations') - F('variant_so_allocations'),
 | 
			
		||||
                output_field=FloatField(),
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
 
 | 
			
		||||
@@ -690,17 +690,6 @@
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Load the BOM table data in the pricing view
 | 
			
		||||
    {% if part.has_bom and roles.sales_order.view %}
 | 
			
		||||
    loadBomTable($("#bom-pricing-table"), {
 | 
			
		||||
        editable: false,
 | 
			
		||||
        bom_url: "{% url 'api-bom-list' %}",
 | 
			
		||||
        part_url: "{% url 'api-part-list' %}",
 | 
			
		||||
        parent_id: {{ part.id }} ,
 | 
			
		||||
        sub_part_detail: true,
 | 
			
		||||
    });
 | 
			
		||||
    {% endif %}
 | 
			
		||||
 | 
			
		||||
    onPanelLoad("purchase-orders", function() {
 | 
			
		||||
        loadPartPurchaseOrderTable(
 | 
			
		||||
            "#purchase-order-table",
 | 
			
		||||
@@ -885,152 +874,164 @@
 | 
			
		||||
        );
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    onPanelLoad('pricing', function() {
 | 
			
		||||
        {% default_currency as currency %}
 | 
			
		||||
 | 
			
		||||
    {% default_currency as currency %}
 | 
			
		||||
        // Load the BOM table data in the pricing view
 | 
			
		||||
        {% if part.has_bom and roles.sales_order.view %}
 | 
			
		||||
        loadBomTable($("#bom-pricing-table"), {
 | 
			
		||||
            editable: false,
 | 
			
		||||
            bom_url: "{% url 'api-bom-list' %}",
 | 
			
		||||
            part_url: "{% url 'api-part-list' %}",
 | 
			
		||||
            parent_id: {{ part.id }} ,
 | 
			
		||||
            sub_part_detail: true,
 | 
			
		||||
        });
 | 
			
		||||
        {% endif %}
 | 
			
		||||
 | 
			
		||||
    // history graphs
 | 
			
		||||
    {% if price_history %}
 | 
			
		||||
        var purchasepricedata = {
 | 
			
		||||
                labels: [
 | 
			
		||||
                    {% for line in price_history %}'{% render_date line.date %}',{% endfor %}
 | 
			
		||||
                ],
 | 
			
		||||
                datasets: [{
 | 
			
		||||
                    label: '{% blocktrans %}Purchase Unit Price - {{currency}}{% endblocktrans %}',
 | 
			
		||||
                    backgroundColor: 'rgba(255, 99, 132, 0.2)',
 | 
			
		||||
                    borderColor: 'rgb(255, 99, 132)',
 | 
			
		||||
                    yAxisID: 'y',
 | 
			
		||||
                    data: [
 | 
			
		||||
                        {% for line in price_history %}{{ line.price|stringformat:".2f" }},{% endfor %}
 | 
			
		||||
                    ],
 | 
			
		||||
                    borderWidth: 1,
 | 
			
		||||
                    type: 'line'
 | 
			
		||||
                },
 | 
			
		||||
                {% if 'price_diff' in price_history.0 %}
 | 
			
		||||
                {
 | 
			
		||||
                    label: '{% blocktrans %}Unit Price-Cost Difference - {{currency}}{% endblocktrans %}',
 | 
			
		||||
                    backgroundColor: 'rgba(68, 157, 68, 0.2)',
 | 
			
		||||
                    borderColor: 'rgb(68, 157, 68)',
 | 
			
		||||
                    yAxisID: 'y2',
 | 
			
		||||
                    data: [
 | 
			
		||||
                        {% for line in price_history %}{{ line.price_diff|stringformat:".2f" }},{% endfor %}
 | 
			
		||||
                    ],
 | 
			
		||||
                    borderWidth: 1,
 | 
			
		||||
                    type: 'line',
 | 
			
		||||
                    hidden: true,
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    label: '{% blocktrans %}Supplier Unit Cost - {{currency}}{% endblocktrans %}',
 | 
			
		||||
                    backgroundColor: 'rgba(70, 127, 155, 0.2)',
 | 
			
		||||
                    borderColor: 'rgb(70, 127, 155)',
 | 
			
		||||
                    yAxisID: 'y',
 | 
			
		||||
                    data: [
 | 
			
		||||
                        {% for line in price_history %}{{ line.price_part|stringformat:".2f" }},{% endfor %}
 | 
			
		||||
                    ],
 | 
			
		||||
                    borderWidth: 1,
 | 
			
		||||
                    type: 'line',
 | 
			
		||||
                    hidden: true,
 | 
			
		||||
                },
 | 
			
		||||
                {% endif %}
 | 
			
		||||
                {
 | 
			
		||||
                    label: '{% trans "Quantity" %}',
 | 
			
		||||
                    backgroundColor: 'rgba(255, 206, 86, 0.2)',
 | 
			
		||||
                    borderColor: 'rgb(255, 206, 86)',
 | 
			
		||||
                    yAxisID: 'y1',
 | 
			
		||||
                    data: [
 | 
			
		||||
                        {% for line in price_history %}{{ line.qty|stringformat:"f" }},{% endfor %}
 | 
			
		||||
                    ],
 | 
			
		||||
                    borderWidth: 1
 | 
			
		||||
                }]
 | 
			
		||||
            }
 | 
			
		||||
        var StockPriceChart = loadStockPricingChart($('#StockPriceChart'), purchasepricedata)
 | 
			
		||||
    {% endif %}
 | 
			
		||||
 | 
			
		||||
    {% if bom_parts %}
 | 
			
		||||
        var bom_colors =  randomColor({hue: 'green', count: {{ bom_parts|length }} })
 | 
			
		||||
        var bomdata = {
 | 
			
		||||
            labels: [{% for line in bom_parts %}'{{ line.name }}',{% endfor %}],
 | 
			
		||||
            datasets:  [
 | 
			
		||||
            {
 | 
			
		||||
              label: 'Price',
 | 
			
		||||
              data: [{% for line in bom_parts %}{{ line.min_price }},{% endfor %}],
 | 
			
		||||
                backgroundColor: bom_colors,
 | 
			
		||||
            },
 | 
			
		||||
            {% if bom_pie_max %}
 | 
			
		||||
            {
 | 
			
		||||
                label: 'Max Price',
 | 
			
		||||
                data: [{% for line in bom_parts %}{{ line.max_price }},{% endfor %}],
 | 
			
		||||
                backgroundColor: bom_colors,
 | 
			
		||||
              },
 | 
			
		||||
            {% endif %}
 | 
			
		||||
            ]
 | 
			
		||||
        };
 | 
			
		||||
        var BomChart = loadBomChart(document.getElementById('BomChart'), bomdata)
 | 
			
		||||
    {% endif %}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    // Internal pricebreaks
 | 
			
		||||
    {% settings_value "PART_INTERNAL_PRICE" as show_internal_price %}
 | 
			
		||||
    {% if show_internal_price and roles.sales_order.view %}
 | 
			
		||||
        initPriceBreakSet(
 | 
			
		||||
            $('#internal-price-break-table'),
 | 
			
		||||
            {
 | 
			
		||||
                part_id: {{part.id}},
 | 
			
		||||
                pb_human_name: 'internal price break',
 | 
			
		||||
                pb_url_slug: 'internal-price',
 | 
			
		||||
                pb_url: '{% url 'api-part-internal-price-list' %}',
 | 
			
		||||
                pb_new_btn: $('#new-internal-price-break'),
 | 
			
		||||
                pb_new_url: '{% url 'api-part-internal-price-list' %}',
 | 
			
		||||
                linkedGraph: $('#InternalPriceBreakChart'),
 | 
			
		||||
            },
 | 
			
		||||
        );
 | 
			
		||||
    {% endif %}
 | 
			
		||||
 | 
			
		||||
    // Sales pricebreaks
 | 
			
		||||
    {% if part.salable and roles.sales_order.view %}
 | 
			
		||||
        initPriceBreakSet(
 | 
			
		||||
            $('#price-break-table'),
 | 
			
		||||
            {
 | 
			
		||||
                part_id: {{part.id}},
 | 
			
		||||
                pb_human_name: 'sale price break',
 | 
			
		||||
                pb_url_slug: 'sale-price',
 | 
			
		||||
                pb_url: "{% url 'api-part-sale-price-list' %}",
 | 
			
		||||
                pb_new_btn: $('#new-price-break'),
 | 
			
		||||
                pb_new_url: '{% url 'api-part-sale-price-list' %}',
 | 
			
		||||
                linkedGraph: $('#SalePriceBreakChart'),
 | 
			
		||||
            },
 | 
			
		||||
        );
 | 
			
		||||
    {% endif %}
 | 
			
		||||
 | 
			
		||||
    // Sale price history
 | 
			
		||||
    {% if sale_history %}
 | 
			
		||||
            var salepricedata = {
 | 
			
		||||
        // history graphs
 | 
			
		||||
        {% if price_history %}
 | 
			
		||||
            var purchasepricedata = {
 | 
			
		||||
                    labels: [
 | 
			
		||||
                        {% for line in sale_history %}'{% render_date line.date %}',{% endfor %}
 | 
			
		||||
                        {% for line in price_history %}'{% render_date line.date %}',{% endfor %}
 | 
			
		||||
                    ],
 | 
			
		||||
                    datasets: [{
 | 
			
		||||
                        label: '{% blocktrans %}Unit Price - {{currency}}{% endblocktrans %}',
 | 
			
		||||
                        label: '{% blocktrans %}Purchase Unit Price - {{currency}}{% endblocktrans %}',
 | 
			
		||||
                        backgroundColor: 'rgba(255, 99, 132, 0.2)',
 | 
			
		||||
                        borderColor: 'rgb(255, 99, 132)',
 | 
			
		||||
                        yAxisID: 'y',
 | 
			
		||||
                        data: [
 | 
			
		||||
                            {% for line in sale_history %}{{ line.price|stringformat:".2f" }},{% endfor %}
 | 
			
		||||
                            {% for line in price_history %}{{ line.price|stringformat:".2f" }},{% endfor %}
 | 
			
		||||
                        ],
 | 
			
		||||
                        borderWidth: 1,
 | 
			
		||||
                        type: 'line'
 | 
			
		||||
                    },
 | 
			
		||||
                    {% if 'price_diff' in price_history.0 %}
 | 
			
		||||
                    {
 | 
			
		||||
                        label: '{% blocktrans %}Unit Price-Cost Difference - {{currency}}{% endblocktrans %}',
 | 
			
		||||
                        backgroundColor: 'rgba(68, 157, 68, 0.2)',
 | 
			
		||||
                        borderColor: 'rgb(68, 157, 68)',
 | 
			
		||||
                        yAxisID: 'y2',
 | 
			
		||||
                        data: [
 | 
			
		||||
                            {% for line in price_history %}{{ line.price_diff|stringformat:".2f" }},{% endfor %}
 | 
			
		||||
                        ],
 | 
			
		||||
                        borderWidth: 1,
 | 
			
		||||
                        type: 'line',
 | 
			
		||||
                        hidden: true,
 | 
			
		||||
                    },
 | 
			
		||||
                    {
 | 
			
		||||
                        label: '{% blocktrans %}Supplier Unit Cost - {{currency}}{% endblocktrans %}',
 | 
			
		||||
                        backgroundColor: 'rgba(70, 127, 155, 0.2)',
 | 
			
		||||
                        borderColor: 'rgb(70, 127, 155)',
 | 
			
		||||
                        yAxisID: 'y',
 | 
			
		||||
                        data: [
 | 
			
		||||
                            {% for line in price_history %}{{ line.price_part|stringformat:".2f" }},{% endfor %}
 | 
			
		||||
                        ],
 | 
			
		||||
                        borderWidth: 1,
 | 
			
		||||
                        type: 'line',
 | 
			
		||||
                        hidden: true,
 | 
			
		||||
                    },
 | 
			
		||||
                    {% endif %}
 | 
			
		||||
                    {
 | 
			
		||||
                        label: '{% trans "Quantity" %}',
 | 
			
		||||
                        backgroundColor: 'rgba(255, 206, 86, 0.2)',
 | 
			
		||||
                        borderColor: 'rgb(255, 206, 86)',
 | 
			
		||||
                        yAxisID: 'y1',
 | 
			
		||||
                        data: [
 | 
			
		||||
                            {% for line in sale_history %}{{ line.qty|stringformat:"f" }},{% endfor %}
 | 
			
		||||
                            {% for line in price_history %}{{ line.qty|stringformat:"f" }},{% endfor %}
 | 
			
		||||
                        ],
 | 
			
		||||
                        borderWidth: 1,
 | 
			
		||||
                        type: 'bar',
 | 
			
		||||
                        borderWidth: 1
 | 
			
		||||
                    }]
 | 
			
		||||
                }
 | 
			
		||||
            var SalePriceChart = loadSellPricingChart($('#SalePriceChart'), salepricedata)
 | 
			
		||||
    {% endif %}
 | 
			
		||||
            var StockPriceChart = loadStockPricingChart($('#StockPriceChart'), purchasepricedata)
 | 
			
		||||
        {% endif %}
 | 
			
		||||
 | 
			
		||||
        {% if bom_parts %}
 | 
			
		||||
            var bom_colors =  randomColor({hue: 'green', count: {{ bom_parts|length }} })
 | 
			
		||||
            var bomdata = {
 | 
			
		||||
                labels: [{% for line in bom_parts %}'{{ line.name }}',{% endfor %}],
 | 
			
		||||
                datasets:  [
 | 
			
		||||
                {
 | 
			
		||||
                label: 'Price',
 | 
			
		||||
                data: [{% for line in bom_parts %}{{ line.min_price }},{% endfor %}],
 | 
			
		||||
                    backgroundColor: bom_colors,
 | 
			
		||||
                },
 | 
			
		||||
                {% if bom_pie_max %}
 | 
			
		||||
                {
 | 
			
		||||
                    label: 'Max Price',
 | 
			
		||||
                    data: [{% for line in bom_parts %}{{ line.max_price }},{% endfor %}],
 | 
			
		||||
                    backgroundColor: bom_colors,
 | 
			
		||||
                },
 | 
			
		||||
                {% endif %}
 | 
			
		||||
                ]
 | 
			
		||||
            };
 | 
			
		||||
            var BomChart = loadBomChart(document.getElementById('BomChart'), bomdata)
 | 
			
		||||
        {% endif %}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        // Internal pricebreaks
 | 
			
		||||
        {% settings_value "PART_INTERNAL_PRICE" as show_internal_price %}
 | 
			
		||||
        {% if show_internal_price and roles.sales_order.view %}
 | 
			
		||||
            initPriceBreakSet(
 | 
			
		||||
                $('#internal-price-break-table'),
 | 
			
		||||
                {
 | 
			
		||||
                    part_id: {{part.id}},
 | 
			
		||||
                    pb_human_name: 'internal price break',
 | 
			
		||||
                    pb_url_slug: 'internal-price',
 | 
			
		||||
                    pb_url: '{% url 'api-part-internal-price-list' %}',
 | 
			
		||||
                    pb_new_btn: $('#new-internal-price-break'),
 | 
			
		||||
                    pb_new_url: '{% url 'api-part-internal-price-list' %}',
 | 
			
		||||
                    linkedGraph: $('#InternalPriceBreakChart'),
 | 
			
		||||
                },
 | 
			
		||||
            );
 | 
			
		||||
        {% endif %}
 | 
			
		||||
 | 
			
		||||
        // Sales pricebreaks
 | 
			
		||||
        {% if part.salable and roles.sales_order.view %}
 | 
			
		||||
            initPriceBreakSet(
 | 
			
		||||
                $('#price-break-table'),
 | 
			
		||||
                {
 | 
			
		||||
                    part_id: {{part.id}},
 | 
			
		||||
                    pb_human_name: 'sale price break',
 | 
			
		||||
                    pb_url_slug: 'sale-price',
 | 
			
		||||
                    pb_url: "{% url 'api-part-sale-price-list' %}",
 | 
			
		||||
                    pb_new_btn: $('#new-price-break'),
 | 
			
		||||
                    pb_new_url: '{% url 'api-part-sale-price-list' %}',
 | 
			
		||||
                    linkedGraph: $('#SalePriceBreakChart'),
 | 
			
		||||
                },
 | 
			
		||||
            );
 | 
			
		||||
        {% endif %}
 | 
			
		||||
 | 
			
		||||
        // Sale price history
 | 
			
		||||
        {% if sale_history %}
 | 
			
		||||
                var salepricedata = {
 | 
			
		||||
                        labels: [
 | 
			
		||||
                            {% for line in sale_history %}'{% render_date line.date %}',{% endfor %}
 | 
			
		||||
                        ],
 | 
			
		||||
                        datasets: [{
 | 
			
		||||
                            label: '{% blocktrans %}Unit Price - {{currency}}{% endblocktrans %}',
 | 
			
		||||
                            backgroundColor: 'rgba(255, 99, 132, 0.2)',
 | 
			
		||||
                            borderColor: 'rgb(255, 99, 132)',
 | 
			
		||||
                            yAxisID: 'y',
 | 
			
		||||
                            data: [
 | 
			
		||||
                                {% for line in sale_history %}{{ line.price|stringformat:".2f" }},{% endfor %}
 | 
			
		||||
                            ],
 | 
			
		||||
                            borderWidth: 1,
 | 
			
		||||
                        },
 | 
			
		||||
                        {
 | 
			
		||||
                            label: '{% trans "Quantity" %}',
 | 
			
		||||
                            backgroundColor: 'rgba(255, 206, 86, 0.2)',
 | 
			
		||||
                            borderColor: 'rgb(255, 206, 86)',
 | 
			
		||||
                            yAxisID: 'y1',
 | 
			
		||||
                            data: [
 | 
			
		||||
                                {% for line in sale_history %}{{ line.qty|stringformat:"f" }},{% endfor %}
 | 
			
		||||
                            ],
 | 
			
		||||
                            borderWidth: 1,
 | 
			
		||||
                            type: 'bar',
 | 
			
		||||
                        }]
 | 
			
		||||
                    }
 | 
			
		||||
                var SalePriceChart = loadSellPricingChart($('#SalePriceChart'), salepricedata)
 | 
			
		||||
        {% endif %}
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    enableSidebar('part');
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -18,7 +18,7 @@
 | 
			
		||||
    {% endif %}
 | 
			
		||||
    <img class="part-thumb" id='part-image'
 | 
			
		||||
        {% if part.image %}
 | 
			
		||||
        src="{{ part.image.url }}"
 | 
			
		||||
        src="{{ part.image.preview.url }}"
 | 
			
		||||
        {% else %}
 | 
			
		||||
        src="{% static 'img/blank_image.png' %}"
 | 
			
		||||
        {% endif %}/>
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user