Refactor part pricing page

This commit is contained in:
Oliver 2021-07-15 23:06:37 +10:00
parent 0fc558068f
commit c61fc7c1df
9 changed files with 366 additions and 435 deletions

View File

@ -7,9 +7,8 @@ from __future__ import unicode_literals
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.views.generic import DetailView, ListView, UpdateView from django.views.generic import DetailView, ListView
from django.forms import HiddenInput from django.forms import HiddenInput
from django.urls import reverse
from part.models import Part from part.models import Part
from .models import Build, BuildItem from .models import Build, BuildItem

View File

@ -1,6 +1,8 @@
{% extends "part/part_base.html" %} {% extends "part/part_base.html" %}
{% load static %} {% load static %}
{% load i18n %} {% load i18n %}
{% load inventree_extras %}
{% load crispy_forms_tags %}
{% load markdownify %} {% load markdownify %}
{% block menubar %} {% block menubar %}
@ -92,6 +94,10 @@
</div> </div>
</div> </div>
<div class='panel panel-default panel-inventree panel-hidden' id='panel-pricing'>
{% include "part/prices.html" %}
</div>
<div class='panel panel-default panel-inventree panel-hidden' id='panel-part-notes'> <div class='panel panel-default panel-inventree panel-hidden' id='panel-part-notes'>
<div class='panel-heading'> <div class='panel-heading'>
<div class='row'> <div class='row'>
@ -893,6 +899,152 @@
}); });
}); });
{% default_currency as currency %}
// history graphs
{% if price_history %}
var purchasepricedata = {
labels: [
{% for line in price_history %}'{{ line.date }}',{% endfor %}
],
datasets: [{
label: '{% blocktrans %}Single 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 %}Single Price 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 %}Part Single Price - {{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 'internal-price-break-create' %}',
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 'sale-price-break-create' %}',
linkedGraph: $('#SalePriceBreakChart'),
},
);
{% endif %}
// Sale price history
{% if sale_history %}
var salepricedata = {
labels: [
{% for line in sale_history %}'{{ 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 %}
attachNavCallbacks({ attachNavCallbacks({
name: 'part', name: 'part',
default: 'part-stock' default: 'part-stock'

View File

@ -56,7 +56,7 @@
</li> </li>
{% endif %} {% endif %}
<li class='list-group-item' title='{% trans "Pricing Information" %}'> <li class='list-group-item' title='{% trans "Pricing Information" %}'>
<a href='#' id='select-part-pricing' class='nav-toggle'> <a href='#' id='select-pricing' class='nav-toggle'>
<span class='menu-tab-icon fas fa-dollar-sign sidebar-icon'></span> <span class='menu-tab-icon fas fa-dollar-sign sidebar-icon'></span>
{% trans "Prices" %} {% trans "Prices" %}
</a> </a>

View File

@ -1,20 +1,14 @@
{% extends "part/part_base.html" %}
{% load static %} {% load static %}
{% load i18n %} {% load i18n %}
{% load crispy_forms_tags %} {% load crispy_forms_tags %}
{% load inventree_extras %} {% load inventree_extras %}
{% block menubar %} <div class='panel-heading'>
{% include 'part/navbar.html' with tab='prices' %} <h4>{% trans "Pricing Information" %}</h4>
{% endblock %} </div>
{% block heading %}
{% trans "General Price Information" %}
{% endblock %}
{% block details %}
{% default_currency as currency %} {% default_currency as currency %}
<div class='panel-content'>
<div class="row"> <div class="row">
<a class="anchor" id="overview"></a> <a class="anchor" id="overview"></a>
@ -132,15 +126,11 @@
</form> </form>
</div> </div>
</div> </div>
{% endblock %} </div>
{% block post_content_panel %}
{% default_currency as currency %}
{% settings_value "PART_INTERNAL_PRICE" as show_internal_price %} {% settings_value "PART_INTERNAL_PRICE" as show_internal_price %}
{% if part.purchaseable and roles.purchase_order.view %} {% if part.purchaseable and roles.purchase_order.view %}
<div class='panel panel-default panel-inventree'>
<a class="anchor" id="supplier-cost"></a> <a class="anchor" id="supplier-cost"></a>
<div class='panel-heading'> <div class='panel-heading'>
<h4>{% trans "Supplier Cost" %} <h4>{% trans "Supplier Cost" %}
@ -148,7 +138,8 @@
</h4> </h4>
</div> </div>
<div class='panel-content'><div class="row"> <div class='panel-content'>
<div class="row">
<div class="col col-md-6"> <div class="col col-md-6">
<h4>{% trans "Suppliers" %}</h4> <h4>{% trans "Suppliers" %}</h4>
<table class="table table-striped table-condensed" id='supplier-table' data-toolbar='#button-toolbar'></table> <table class="table table-striped table-condensed" id='supplier-table' data-toolbar='#button-toolbar'></table>
@ -157,20 +148,21 @@
<h4>{% trans "Manufacturers" %}</h4> <h4>{% trans "Manufacturers" %}</h4>
<table class="table table-striped table-condensed" id='manufacturer-table' data-toolbar='#button-toolbar'></table> <table class="table table-striped table-condensed" id='manufacturer-table' data-toolbar='#button-toolbar'></table>
</div> </div>
</div></div> </div>
</div> </div>
<div class='panel panel-default panel-inventree'> {% if price_history %}
<a class="anchor" id="purchase-price"></a> <a class="anchor" id="purchase-price"></a>
<div class='panel-heading'> <div class='panel-heading'>
<h4>{% trans "Purchase Price" %} <h4>{% trans "Purchase Price" %}
<a href="#overview" title='{% trans "Jump to overview" %}'><span class="fas fa-level-up-alt"></span></a> <a href="#overview" title='{% trans "Jump to overview" %}'><span class="fas fa-level-up-alt"></span></a>
</h4> </h4>
</div> </div>
<div class='panel-content'>
{% if price_history %} <h4>{% trans 'Stock Pricing' %}
<h4>{% trans 'Stock Pricing' %}<i class="fas fa-info-circle" title="Shows the purchase prices of stock for this part. <i class="fas fa-info-circle" title="Shows the purchase prices of stock for this part.
The part single price is the current purchase price for that supplier part."></i></h4> The part single price is the current purchase price for that supplier part."></i>
</h4>
{% if price_history|length > 0 %} {% if price_history|length > 0 %}
<div style="max-width: 99%; min-height: 300px"> <div style="max-width: 99%; min-height: 300px">
<canvas id="StockPriceChart"></canvas> <canvas id="StockPriceChart"></canvas>
@ -180,13 +172,11 @@
{% trans 'No stock pricing history is available for this part.' %} {% trans 'No stock pricing history is available for this part.' %}
</div> </div>
{% endif %} {% endif %}
{% endif %}
</div> </div>
{% endif %} {% endif %}
{% endif %}
{% if show_internal_price and roles.sales_order.view %} {% if show_internal_price and roles.sales_order.view %}
<div class='panel panel-default panel-inventree'>
<a class="anchor" id="internal-cost"></a> <a class="anchor" id="internal-cost"></a>
<div class='panel-heading'> <div class='panel-heading'>
<h4>{% trans "Internal Cost" %} <h4>{% trans "Internal Cost" %}
@ -194,7 +184,8 @@
</h4> </h4>
</div> </div>
<div class='panel-content'><div class="row full-height"> <div class='panel-content'>
<div class="row full-height">
<div class="col col-md-8"> <div class="col col-md-8">
<div style="max-width: 99%; height: 100%;"> <div style="max-width: 99%; height: 100%;">
<canvas id="InternalPriceBreakChart"></canvas> <canvas id="InternalPriceBreakChart"></canvas>
@ -211,13 +202,11 @@
data-sort-name="quantity" data-sort-order="asc"> data-sort-name="quantity" data-sort-order="asc">
</table> </table>
</div> </div>
</div></div> </div>
</div> </div>
{% endif %} {% endif %}
{% if part.has_bom and roles.sales_order.view %} {% if part.has_bom and roles.sales_order.view %}
<div class='panel panel-default panel-inventree'>
<a class="anchor" id="bom-cost"></a> <a class="anchor" id="bom-cost"></a>
<div class='panel-heading'> <div class='panel-heading'>
<h4>{% trans "BOM Cost" %} <h4>{% trans "BOM Cost" %}
@ -225,7 +214,8 @@
</h4> </h4>
</div> </div>
<div class='panel-content'><div class="row"> <div class='panel-content'>
<div class="row">
<div class="col col-md-6"> <div class="col col-md-6">
<table class='table table-bom table-condensed' data-toolbar="#button-toolbar" id='bom-table'></table> <table class='table table-bom table-condensed' data-toolbar="#button-toolbar" id='bom-table'></table>
</div> </div>
@ -238,13 +228,11 @@
</div> </div>
</div> </div>
{% endif %} {% endif %}
</div></div> </div>
</div> </div>
{% endif %} {% endif %}
{% if part.salable and roles.sales_order.view %} {% if part.salable and roles.sales_order.view %}
<div class='panel panel-default panel-inventree'>
<a class="anchor" id="sale-cost"></a> <a class="anchor" id="sale-cost"></a>
<div class='panel-heading'> <div class='panel-heading'>
<h4>{% trans "Sale Cost" %} <h4>{% trans "Sale Cost" %}
@ -252,7 +240,8 @@
</h4> </h4>
</div> </div>
<div class='panel-content'><div class="row full-height"> <div class='panel-content'>
<div class="row full-height">
<div class="col col-md-8"> <div class="col col-md-8">
<div style="max-width: 99%; height: 100%;"> <div style="max-width: 99%; height: 100%;">
<canvas id="SalePriceBreakChart"></canvas> <canvas id="SalePriceBreakChart"></canvas>
@ -269,10 +258,9 @@
data-sort-name="quantity" data-sort-order="asc"> data-sort-name="quantity" data-sort-order="asc">
</table> </table>
</div> </div>
</div></div> </div>
</div> </div>
<div class='panel panel-default panel-inventree'>
<a class="anchor" id="sale-price"></a> <a class="anchor" id="sale-price"></a>
<div class='panel-heading'> <div class='panel-heading'>
<h4>{% trans "Sale Price" %} <h4>{% trans "Sale Price" %}
@ -291,198 +279,4 @@
</div> </div>
{% endif %} {% endif %}
</div> </div>
</div>
{% endif %} {% endif %}
{% endblock %}
{% block js_ready %}
{{ block.super }}
{% default_currency as currency %}
loadSupplierPartTable(
"#supplier-table",
"{% url 'api-supplier-part-list' %}",
{
params: {
part: {{ part.id }},
part_detail: false,
supplier_detail: true,
manufacturer_detail: true,
},
}
);
loadManufacturerPartTable(
"#manufacturer-table",
"{% url 'api-manufacturer-part-list' %}",
{
params: {
part: {{ part.id }},
part_detail: false,
manufacturer_detail: true,
},
}
);
// history graphs
{% if price_history %}
var purchasepricedata = {
labels: [
{% for line in price_history %}'{{ line.date }}',{% endfor %}
],
datasets: [{
label: '{% blocktrans %}Single 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 %}Single Price 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 %}Part Single Price - {{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 'internal-price-break-create' %}',
linkedGraph: $('#InternalPriceBreakChart'),
},
);
{% endif %}
// Load the BOM table data
loadBomTable($("#bom-table"), {
editable: {{ editing_enabled }},
bom_url: "{% url 'api-bom-list' %}",
part_url: "{% url 'api-part-list' %}",
parent_id: {{ part.id }} ,
sub_part_detail: true,
});
// 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 'sale-price-break-create' %}',
linkedGraph: $('#SalePriceBreakChart'),
},
);
{% endif %}
// Sale price history
{% if sale_history %}
var salepricedata = {
labels: [
{% for line in sale_history %}'{{ 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 %}
{% endblock %}

View File

@ -47,8 +47,6 @@ part_detail_urls = [
url(r'^bom-upload/?', views.BomUpload.as_view(), name='upload-bom'), url(r'^bom-upload/?', views.BomUpload.as_view(), name='upload-bom'),
url(r'^bom-duplicate/?', views.BomDuplicate.as_view(), name='duplicate-bom'), url(r'^bom-duplicate/?', views.BomDuplicate.as_view(), name='duplicate-bom'),
url(r'^prices/', views.PartPricingView.as_view(template_name='part/prices.html'), name='part-prices'),
url(r'^qr_code/?', views.PartQRCode.as_view(), name='part-qr'), url(r'^qr_code/?', views.PartQRCode.as_view(), name='part-qr'),
# Normal thumbnail with form # Normal thumbnail with form

View File

@ -754,6 +754,7 @@ class PartDetail(InvenTreeRoleMixin, DetailView):
context_object_name = 'part' context_object_name = 'part'
queryset = Part.objects.all().select_related('category') queryset = Part.objects.all().select_related('category')
template_name = 'part/detail.html' template_name = 'part/detail.html'
form_class = part_forms.PartPriceForm
# Add in some extra context information based on query params # Add in some extra context information based on query params
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
@ -774,25 +775,12 @@ class PartDetail(InvenTreeRoleMixin, DetailView):
ctx = part.get_context_data(self.request) ctx = part.get_context_data(self.request)
context.update(**ctx) context.update(**ctx)
return context # Pricing information
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 = self.get_pricing(self.get_quantity())
ctx['form'] = self.form_class(initial=self.get_initials()) ctx['form'] = self.form_class(initial=self.get_initials())
context.update(ctx) context.update(ctx)
return context return context
def get_quantity(self): def get_quantity(self):

View File

@ -921,7 +921,7 @@ function loadBuildTable(table, options) {
} }
else else
{ {
return '{% trans "No user information" %}'; return `<i>{% trans "No user information" %}</i>`;
} }
} }
}, },

View File

@ -714,7 +714,7 @@ function loadPartTable(table, url, options={}) {
var html = ''; var html = '';
html = `<div class='row'>`; html = `<div class='row full-height'>`;
data.forEach(function(row, index) { data.forEach(function(row, index) {

View File

@ -1495,7 +1495,7 @@ function loadStockTrackingTable(table, options) {
} }
else else
{ {
return '{% trans "No user information" %}'; return `<i>{% trans "No user information" %}</i>`;
} }
} }
}); });