mirror of
https://github.com/inventree/InvenTree.git
synced 2025-08-09 21:30:54 +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