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 %} + +
    + {% csrf_token %} + {% load crispy_forms_tags %} + {% crispy form %} +
    + + +{% if part.supplier_count > 0 %} +

    {% trans 'Supplier Pricing' %}

    + + {% if min_total_buy_price %} + + + + + + {% if quantity > 1 %} + + + + + + {% endif %} + {% else %} + + + + {% endif %} +
    {% trans 'Unit Cost' %}Min: {% include "price.html" with price=min_unit_buy_price %}Max: {% include "price.html" with price=max_unit_buy_price %}
    {% trans 'Total Cost' %}Min: {% include "price.html" with price=min_total_buy_price %}Max: {% include "price.html" with price=max_total_buy_price %}
    + {% trans 'No supplier pricing available' %} +
    +{% endif %} + +{% if part.bom_count > 0 %} +

    {% trans 'BOM Pricing' %}

    + + {% if min_total_bom_price %} + + + + + + {% if quantity > 1 %} + + + + + + {% endif %} + {% if part.has_complete_bom_pricing == False %} + + + + {% endif %} + {% else %} + + + + {% endif %} +
    {% trans 'Unit Cost' %}Min: {% include "price.html" with price=min_unit_bom_price %}Max: {% include "price.html" with price=max_unit_bom_price %}
    {% trans 'Total Cost' %}Min: {% include "price.html" with price=min_total_bom_price %}Max: {% include "price.html" with price=max_total_bom_price %}
    + {% trans 'Note: BOM pricing is incomplete for this part' %} +
    + {% trans 'No BOM pricing available' %} +
    +{% 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: