diff --git a/InvenTree/part/templates/part/navbar.html b/InvenTree/part/templates/part/navbar.html
index df49841e31..a2c8104486 100644
--- a/InvenTree/part/templates/part/navbar.html
+++ b/InvenTree/part/templates/part/navbar.html
@@ -69,6 +69,12 @@
{% endif %}
{% if part.purchaseable and roles.purchase_order.view %}
+
+
+
+ {% trans "Order Price" %}
+
+
diff --git a/InvenTree/part/templates/part/order_prices.html b/InvenTree/part/templates/part/order_prices.html
new file mode 100644
index 0000000000..5bbd5d22ef
--- /dev/null
+++ b/InvenTree/part/templates/part/order_prices.html
@@ -0,0 +1,219 @@
+{% extends "part/part_base.html" %}
+{% load static %}
+{% load i18n %}
+{% load inventree_extras %}
+
+{% block menubar %}
+{% include 'part/navbar.html' with tab='order-prices' %}
+{% endblock %}
+
+{% block heading %}
+{% trans "Order Price Information" %}
+{% endblock %}
+
+{% block details %}
+
+
+
+
+{% if part.supplier_count > 0 %}
+ {% trans 'Supplier Pricing' %}
+
+ {% if min_total_buy_price %}
+
+ {% trans 'Unit Cost' %} |
+ Min: {% include "price.html" with price=min_unit_buy_price %} |
+ Max: {% include "price.html" with price=max_unit_buy_price %} |
+
+ {% if quantity > 1 %}
+
+ {% trans 'Total Cost' %} |
+ Min: {% include "price.html" with price=min_total_buy_price %} |
+ Max: {% include "price.html" with price=max_total_buy_price %} |
+
+ {% endif %}
+ {% else %}
+
+
+ {% trans 'No supplier pricing available' %}
+ |
+
+ {% endif %}
+
+{% endif %}
+
+{% if part.bom_count > 0 %}
+ {% trans 'BOM Pricing' %}
+
+ {% if min_total_bom_price %}
+
+ {% trans 'Unit Cost' %} |
+ Min: {% include "price.html" with price=min_unit_bom_price %} |
+ Max: {% include "price.html" with price=max_unit_bom_price %} |
+
+ {% if quantity > 1 %}
+
+ {% trans 'Total Cost' %} |
+ Min: {% include "price.html" with price=min_total_bom_price %} |
+ Max: {% include "price.html" with price=max_total_bom_price %} |
+
+ {% endif %}
+ {% if part.has_complete_bom_pricing == False %}
+
+
+ {% trans 'Note: BOM pricing is incomplete for this part' %}
+ |
+
+ {% endif %}
+ {% else %}
+
+
+ {% trans 'No BOM pricing available' %}
+ |
+
+ {% endif %}
+
+{% endif %}
+
+{% if total_part_price %}
+ {% trans 'Sale Price' %}
+
+
+ {% trans 'Unit Cost' %} |
+ {% include "price.html" with price=unit_part_price %} |
+
+
+ {% trans 'Total Cost' %} |
+ {% include "price.html" with price=total_part_price %} |
+
+
+{% endif %}
+
+ {% if min_unit_buy_price or min_unit_bom_price %}
+ {% else %}
+
+ {% trans 'No pricing information is available for this part.' %}
+
+ {% endif %}
+
+
+{% if price_history %}
+ {% trans 'Stock Pricing' %}
+ {% if price_history|length > 1 %}
+
+
+
+ {% else %}
+
+ {% trans 'No stock pricing history is available for this part.' %}
+
+ {% endif %}
+{% endif %}
+{% endblock %}
+
+
+
+
+{% block js_ready %}
+ {{ block.super }}
+
+ {% settings_value "INVENTREE_DEFAULT_CURRENCY" as currency %}
+
+ {% if price_history %}
+ var pricedata = {
+ labels: [
+ {% for line in price_history %}'{{ line.date }}',{% endfor %}
+ ],
+ datasets: [{
+ label: '{% trans "Single Price" %}',
+ 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: '{% trans "Single Price Difference" %}',
+ 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'
+ },
+ {
+ label: '{% trans "Part Single Price" %}',
+ 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'
+ },
+ {% 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 ctx = document.getElementById('StockPriceChart');
+ var StockPriceChart = new Chart(ctx, {
+ type: 'bar',
+ data: pricedata,
+ options: {
+ responsive: true,
+ maintainAspectRatio: false,
+ plugins: {legend: {position: 'bottom'}},
+ scales: {
+ y: {
+ type: 'linear',
+ position: 'left',
+ grid: {display: false},
+ title: {
+ display: true,
+ text: '{% blocktrans %}Single Price - {{currency}}{% endblocktrans %}'
+ }
+ },
+ y1: {
+ type: 'linear',
+ position: 'right',
+ grid: {display: false},
+ titel: {
+ display: true,
+ text: '{% trans "Quantity" %}',
+ position: 'right'
+ }
+ },
+ y2: {
+ type: 'linear',
+ position: 'left',
+ grid: {display: false},
+ title: {
+ display: true,
+ text: '{% blocktrans %}Single Price Difference- {{currency}}{% endblocktrans %}'
+ }
+ }
+ },
+ }
+ });
+ {% endif %}
+
+{% endblock %}
diff --git a/InvenTree/part/urls.py b/InvenTree/part/urls.py
index c734b7f610..e53ce54782 100644
--- a/InvenTree/part/urls.py
+++ b/InvenTree/part/urls.py
@@ -59,6 +59,7 @@ part_detail_urls = [
url(r'^bom/?', views.PartDetail.as_view(template_name='part/bom.html'), name='part-bom'),
url(r'^build/?', views.PartDetail.as_view(template_name='part/build.html'), name='part-build'),
url(r'^used/?', views.PartDetail.as_view(template_name='part/used_in.html'), name='part-used-in'),
+ url(r'^order-prices/', views.PartPricingView.as_view(template_name='part/order_prices.html'), name='part-order-prices'),
url(r'^manufacturers/?', views.PartDetail.as_view(template_name='part/manufacturer.html'), name='part-manufacturers'),
url(r'^suppliers/?', views.PartDetail.as_view(template_name='part/supplier.html'), name='part-suppliers'),
url(r'^orders/?', views.PartDetail.as_view(template_name='part/orders.html'), name='part-orders'),
diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py
index a2986fe869..63352c613b 100644
--- a/InvenTree/part/views.py
+++ b/InvenTree/part/views.py
@@ -784,6 +784,81 @@ class PartDetail(InvenTreeRoleMixin, DetailView):
return context
+class PartPricingView(PartDetail):
+ """ Detail view for Part object
+ """
+ context_object_name = 'part'
+ template_name = 'part/order_prices.html'
+ form_class = part_forms.PartPriceForm
+
+ # Add in some extra context information based on query params
+ def get_context_data(self, **kwargs):
+ """ Provide extra context data to template """
+ context = super().get_context_data(**kwargs)
+
+ ctx = self.get_pricing(self.get_quantity())
+ ctx['form'] = self.form_class(initial=self.get_initials())
+
+ context.update(ctx)
+ return context
+
+ def get_quantity(self):
+ """ Return set quantity in decimal format """
+ return Decimal(self.request.POST.get('quantity', 1))
+
+ def get_part(self):
+ return self.get_object()
+
+ def get_pricing(self, quantity=1, currency=None):
+ """ returns context with pricing information """
+ ctx = PartPricing.get_pricing(self, quantity, currency)
+ part = self.get_part()
+ # Stock history
+ if part.total_stock > 1:
+ ret = []
+ stock = part.stock_entries(include_variants=False, in_stock=True) # .order_by('purchase_order__date')
+ stock = stock.prefetch_related('purchase_order', 'supplier_part')
+
+ for stock_item in stock:
+ if None in [stock_item.purchase_price, stock_item.quantity]:
+ continue
+
+ # convert purchase price to current currency - only one currency in the graph
+ price = convert_money(stock_item.purchase_price, inventree_settings.currency_code_default())
+ line = {
+ 'price': price.amount,
+ 'qty': stock_item.quantity
+ }
+ # Supplier Part Name # TODO use in graph
+ if stock_item.supplier_part:
+ line['name'] = stock_item.supplier_part.pretty_name
+
+ if stock_item.supplier_part.unit_pricing and price:
+ line['price_diff'] = price.amount - stock_item.supplier_part.unit_pricing
+ line['price_part'] = stock_item.supplier_part.unit_pricing
+
+ # set date for graph labels
+ if stock_item.purchase_order:
+ line['date'] = stock_item.purchase_order.issue_date.strftime('%d.%m.%Y')
+ else:
+ line['date'] = stock_item.tracking_info.first().date.strftime('%d.%m.%Y')
+ ret.append(line)
+
+ ctx['price_history'] = ret
+
+ return ctx
+
+ def get_initials(self):
+ """ returns initials for form """
+ return {'quantity': self.get_quantity()}
+
+ def post(self, request, *args, **kwargs):
+ self.object = self.get_object()
+ kwargs['object'] = self.object
+ ctx=self.get_context_data(**kwargs)
+ return self.get(request, context=ctx)
+
+
class PartDetailFromIPN(PartDetail):
slug_field = 'IPN'
slug_url_kwarg = 'slug'
@@ -2040,38 +2115,6 @@ class PartPricing(AjaxView):
ctx['max_total_bom_price'] = max_bom_price
ctx['max_unit_bom_price'] = max_unit_bom_price
- # Stock history
- if part_settings.part_show_graph and part.total_stock > 1:
- ret = []
- stock = part.stock_entries(include_variants=False, in_stock=True) # .order_by('purchase_order__date')
- stock = stock.prefetch_related('purchase_order', 'supplier_part')
-
- for stock_item in stock:
- if None in [stock_item.purchase_price, stock_item.quantity]:
- continue
-
- # convert purchase price to current currency - only one currency in the graph
- price = convert_money(stock_item.purchase_price, inventree_settings.currency_code_default())
- line = {
- 'price': price.amount,
- 'qty': stock_item.quantity
- }
- # Supplier Part Name # TODO use in graph
- if stock_item.supplier_part:
- line['name'] = stock_item.supplier_part.pretty_name
-
- if stock_item.supplier_part.unit_pricing and price:
- line['price_diff'] = price.amount - stock_item.supplier_part.unit_pricing
- line['price_part'] = stock_item.supplier_part.unit_pricing
-
- # set date for graph labels
- if stock_item.purchase_order:
- line['date'] = stock_item.purchase_order.issue_date.strftime('%d.%m.%Y')
- else:
- line['date'] = stock_item.tracking_info.first().date.strftime('%d.%m.%Y')
- ret.append(line)
-
- ctx['price_history'] = ret
# part pricing information
part_price = part.get_price(quantity)
if part_price is not None: