Merge pull request #1712 from matmair/one-pricing-view

One pricing view
This commit is contained in:
Oliver 2021-07-03 12:55:34 +10:00 committed by GitHub
commit 2b32f04af2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 733 additions and 489 deletions

View File

@ -961,3 +961,20 @@ input[type="date"].form-control, input[type="time"].form-control, input[type="da
.sidebar-icon {
min-width: 19px;
}
.row.full-height {
display: flex;
flex-wrap: wrap;
}
.row.full-height > [class*='col-'] {
display: flex;
flex-direction: column;
}
a.anchor {
display: block;
position: relative;
top: -60px;
visibility: hidden;
}

View File

@ -1,122 +0,0 @@
{% extends "part/part_base.html" %}
{% load static %}
{% load i18n %}
{% load inventree_extras %}
{% block menubar %}
{% include 'part/navbar.html' with tab='internal-prices' %}
{% endblock %}
{% block heading %}
{% trans "Internal Price Information" %}
{% endblock %}
{% block details %}
{% settings_value "PART_INTERNAL_PRICE" as show_internal_price %}
{% if show_internal_price and roles.sales_order.view %}
<div id='internal-price-break-toolbar' class='btn-group'>
<button class='btn btn-primary' id='new-internal-price-break' type='button'>
<span class='fas fa-plus-circle'></span> {% trans "Add Internal Price Break" %}
</button>
</div>
<table class='table table-striped table-condensed' id='internal-price-break-table' data-toolbar='#internal-price-break-toolbar'>
</table>
{% else %}
<div class='container-fluid'>
<h3>{% trans "Permission Denied" %}</h3>
<div class='alert alert-danger alert-block'>
{% trans "You do not have permission to view this page." %}
</div>
</div>
{% endif %}
{% endblock %}
{% block js_ready %}
{{ block.super }}
{% settings_value "PART_INTERNAL_PRICE" as show_internal_price %}
{% if show_internal_price and roles.sales_order.view %}
function reloadPriceBreaks() {
$("#internal-price-break-table").bootstrapTable("refresh");
}
$('#new-internal-price-break').click(function() {
launchModalForm("{% url 'internal-price-break-create' %}",
{
success: reloadPriceBreaks,
data: {
part: {{ part.id }},
}
}
);
});
$('#internal-price-break-table').inventreeTable({
name: 'internalprice',
formatNoMatches: function() { return "{% trans 'No internal price break information found' %}"; },
queryParams: {
part: {{ part.id }},
},
url: "{% url 'api-part-internal-price-list' %}",
onPostBody: function() {
var table = $('#internal-price-break-table');
table.find('.button-internal-price-break-delete').click(function() {
var pk = $(this).attr('pk');
launchModalForm(
`/part/internal-price/${pk}/delete/`,
{
success: reloadPriceBreaks
}
);
});
table.find('.button-internal-price-break-edit').click(function() {
var pk = $(this).attr('pk');
launchModalForm(
`/part/internal-price/${pk}/edit/`,
{
success: reloadPriceBreaks
}
);
});
},
columns: [
{
field: 'pk',
title: 'ID',
visible: false,
switchable: false,
},
{
field: 'quantity',
title: '{% trans "Quantity" %}',
sortable: true,
},
{
field: 'price',
title: '{% trans "Price" %}',
sortable: true,
formatter: function(value, row, index) {
var html = value;
html += `<div class='btn-group float-right' role='group'>`
html += makeIconButton('fa-edit icon-blue', 'button-internal-price-break-edit', row.pk, '{% trans "Edit internal price break" %}');
html += makeIconButton('fa-trash-alt icon-red', 'button-internal-price-break-delete', row.pk, '{% trans "Delete internal price break" %}');
html += `</div>`;
return html;
}
},
]
})
{% endif %}
{% endblock %}

View File

@ -71,13 +71,13 @@
</a>
</li>
{% endif %}
{% if part.purchaseable and roles.purchase_order.view %}
<li class='list-group-item {% if tab == "order-prices" %}active{% endif %}' title='{% trans "Order Price Information" %}'>
<a href='{% url "part-order-prices" part.id %}'>
<li class='list-group-item {% if tab == "prices" %}active{% endif %}' title='{% trans "Pricing Information" %}'>
<a href='{% url "part-prices" part.id %}'>
<span class='menu-tab-icon fas fa-dollar-sign sidebar-icon'></span>
{% trans "Order Price" %}
{% trans "Prices" %}
</a>
</li>
{% if part.purchaseable and roles.purchase_order.view %}
<li class='list-group-item {% if tab == "manufacturers" %}active{% endif %}' title='{% trans "Manufacturers" %}'>
<a href='{% url "part-manufacturers" part.id %}'>
<span class='menu-tab-icon fas fa-industry sidebar-icon'></span>
@ -97,19 +97,7 @@
</a>
</li>
{% endif %}
{% if show_internal_price and roles.sales_order.view %}
<li class='list-group-item {% if tab == "internal-prices" %}active{% endif %}' title='{% trans "Internal Price Information" %}'>
<a href='{% url "part-internal-prices" part.id %}'>
<span class='menu-tab-icon fas fa-dollar-sign' style='width: 20px;'></span>
{% trans "Internal Price" %}
</a>
</li>
<li class='list-group-item {% if tab == "sales-prices" %}active{% endif %}' title='{% trans "Sales Price Information" %}'>
<a href='{% url "part-sale-prices" part.id %}'>
<span class='menu-tab-icon fas fa-dollar-sign sidebar-icon'></span>
{% trans "Sale Price" %}
</a>
</li>
{% if roles.sales_order.view %}
<li class='list-group-item {% if tab == "sales-orders" %}active{% endif %}' title='{% trans "Sales Orders" %}'>
<a href='{% url "part-sales-orders" part.id %}'>
<span class='menu-tab-icon fas fa-truck sidebar-icon'></span>

View File

@ -1,236 +0,0 @@
{% extends "part/part_base.html" %}
{% load static %}
{% load i18n %}
{% load crispy_forms_tags %}
{% load inventree_extras %}
{% block menubar %}
{% include 'part/navbar.html' with tab='order-prices' %}
{% endblock %}
{% block heading %}
{% trans "Order Price Information" %}
{% endblock %}
{% block details %}
{% default_currency as currency %}
{% settings_value "PART_INTERNAL_PRICE" as show_internal_price %}
<form method="post" class="form-horizontal">
{% csrf_token %}
<div class="row">
<div class="col-sm-9">{{ form|crispy }}</div>
<div class="col-sm-3">
<input type="submit" value="{% trans 'Calculate' %}" class="btn btn-primary btn-block">
</div>
</div>
</form>
<hr>
<div class="row"><div class="col col-md-6">
<h4>{% trans "Pricing ranges" %}</h4>
<table class='table table-striped table-condensed'>
{% if part.supplier_count > 0 %}
{% if min_total_buy_price %}
<tr>
<td><b>{% trans 'Supplier Pricing' %}</b></td>
<td>{% trans 'Unit Cost' %}</td>
<td>Min: {% include "price.html" with price=min_unit_buy_price %}</td>
<td>Max: {% include "price.html" with price=max_unit_buy_price %}</td>
</tr>
{% if quantity > 1 %}
<tr>
<td></td>
<td>{% trans 'Total Cost' %}</td>
<td>Min: {% include "price.html" with price=min_total_buy_price %}</td>
<td>Max: {% include "price.html" with price=max_total_buy_price %}</td>
</tr>
{% endif %}
{% else %}
<tr>
<td colspan='4'>
<span class='warning-msg'><i>{% trans 'No supplier pricing available' %}</i></span>
</td>
</tr>
{% endif %}
{% endif %}
{% if part.bom_count > 0 %}
{% if min_total_bom_price %}
<tr>
<td><b>{% trans 'BOM Pricing' %}</b></td>
<td>{% trans 'Unit Cost' %}</td>
<td>Min: {% include "price.html" with price=min_unit_bom_price %}</td>
<td>Max: {% include "price.html" with price=max_unit_bom_price %}</td>
</tr>
{% if quantity > 1 %}
<tr>
<td></td>
<td>{% trans 'Total Cost' %}</td>
<td>Min: {% include "price.html" with price=min_total_bom_price %}</td>
<td>Max: {% include "price.html" with price=max_total_bom_price %}</td>
</tr>
{% endif %}
{% if part.has_complete_bom_pricing == False %}
<tr>
<td colspan='4'>
<span class='warning-msg'><i>{% trans 'Note: BOM pricing is incomplete for this part' %}</i></span>
</td>
</tr>
{% endif %}
{% else %}
<tr>
<td colspan='4'>
<span class='warning-msg'><i>{% trans 'No BOM pricing available' %}</i></span>
</td>
</tr>
{% endif %}
{% endif %}
{% if show_internal_price and roles.sales_order.view %}
{% if total_internal_part_price %}
<tr>
<td><b>{% trans 'Internal Price' %}</b></td>
<td>{% trans 'Unit Cost' %}</td>
<td colspan='2'>{% include "price.html" with price=unit_internal_part_price %}</td>
</tr>
<tr>
<td></td>
<td>{% trans 'Total Cost' %}</td>
<td colspan='2'>{% include "price.html" with price=total_internal_part_price %}</td>
</tr>
{% endif %}
{% endif %}
{% if total_part_price %}
<tr>
<td><b>{% trans 'Sale Price' %}</b></td>
<td>{% trans 'Unit Cost' %}</td>
<td colspan='2'>{% include "price.html" with price=unit_part_price %}</td>
</tr>
<tr>
<td></td>
<td>{% trans 'Total Cost' %}</td>
<td colspan='2'>{% include "price.html" with price=total_part_price %}</td>
</tr>
{% endif %}
</table>
{% if min_unit_buy_price or min_unit_bom_price %}
{% else %}
<div class='alert alert-danger alert-block'>
{% trans 'No pricing information is available for this part.' %}
</div>
{% endif %}
</div>
{% if part.bom_count > 0 %}
<div class="col col-md-6">
<h4>{% trans 'BOM Pricing' %}</h4>
<div style="max-width: 99%;">
<canvas id="BomChart"></canvas>
</div>
</div>
{% endif %}
</div>
{% if price_history %}
<hr>
<h4>{% trans 'Stock Pricing' %}<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>
{% if price_history|length > 0 %}
<div style="max-width: 99%; min-height: 300px">
<canvas id="StockPriceChart"></canvas>
</div>
{% else %}
<div class='alert alert-danger alert-block'>
{% trans 'No stock pricing history is available for this part.' %}
</div>
{% endif %}
{% endif %}
{% endblock %}
{% block js_ready %}
{{ block.super }}
{% default_currency as currency %}
{% if price_history %}
var pricedata = {
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(document.getElementById('StockPriceChart'), pricedata)
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 %}
{% endblock %}

View File

@ -0,0 +1,488 @@
{% extends "part/part_base.html" %}
{% load static %}
{% load i18n %}
{% load crispy_forms_tags %}
{% load inventree_extras %}
{% block menubar %}
{% include 'part/navbar.html' with tab='prices' %}
{% endblock %}
{% block heading %}
{% trans "General Price Information" %}
{% endblock %}
{% block details %}
{% default_currency as currency %}
<div class="row">
<a class="anchor" id="overview"></a>
<div class="col col-md-6">
<h4>{% trans "Pricing ranges" %}</h4>
<table class='table table-striped table-condensed'>
{% if part.supplier_count > 0 %}
{% if min_total_buy_price %}
<tr>
<td><b>{% trans 'Supplier Pricing' %}</b>
<a href="#supplier-cost" title='{% trans "Show supplier cost" %}'><span class="fas fa-search-dollar"></span></a>
<a href="#purchase-price" title='{% trans "Show purchase price" %}'><span class="fas fa-chart-bar"></span></a>
</td>
<td>{% trans 'Unit Cost' %}</td>
<td>Min: {% include "price.html" with price=min_unit_buy_price %}</td>
<td>Max: {% include "price.html" with price=max_unit_buy_price %}</td>
</tr>
{% if quantity > 1 %}
<tr>
<td></td>
<td>{% trans 'Total Cost' %}</td>
<td>Min: {% include "price.html" with price=min_total_buy_price %}</td>
<td>Max: {% include "price.html" with price=max_total_buy_price %}</td>
</tr>
{% endif %}
{% else %}
<tr>
<td colspan='4'>
<span class='warning-msg'><i>{% trans 'No supplier pricing available' %}</i></span>
</td>
</tr>
{% endif %}
{% endif %}
{% if part.bom_count > 0 %}
{% if min_total_bom_price %}
<tr>
<td><b>{% trans 'BOM Pricing' %}</b>
<a href="#bom-cost" title='{% trans "Show BOM cost" %}'><span class="fas fa-search-dollar"></span></a>
</td>
<td>{% trans 'Unit Cost' %}</td>
<td>Min: {% include "price.html" with price=min_unit_bom_price %}</td>
<td>Max: {% include "price.html" with price=max_unit_bom_price %}</td>
</tr>
{% if quantity > 1 %}
<tr>
<td></td>
<td>{% trans 'Total Cost' %}</td>
<td>Min: {% include "price.html" with price=min_total_bom_price %}</td>
<td>Max: {% include "price.html" with price=max_total_bom_price %}</td>
</tr>
{% endif %}
{% if part.has_complete_bom_pricing == False %}
<tr>
<td colspan='4'>
<span class='warning-msg'><i>{% trans 'Note: BOM pricing is incomplete for this part' %}</i></span>
</td>
</tr>
{% endif %}
{% else %}
<tr>
<td colspan='4'>
<span class='warning-msg'><i>{% trans 'No BOM pricing available' %}</i></span>
</td>
</tr>
{% endif %}
{% endif %}
{% if show_internal_price and roles.sales_order.view %}
{% if total_internal_part_price %}
<tr>
<td><b>{% trans 'Internal Price' %}</b></td>
<td>{% trans 'Unit Cost' %}</td>
<td colspan='2'>{% include "price.html" with price=unit_internal_part_price %}</td>
</tr>
<tr>
<td></td>
<td>{% trans 'Total Cost' %}</td>
<td colspan='2'>{% include "price.html" with price=total_internal_part_price %}</td>
</tr>
{% endif %}
{% endif %}
{% if total_part_price %}
<tr>
<td><b>{% trans 'Sale Price' %}</b>
<a href="#sale-cost" title='{% trans "Show sale cost" %}'><span class="fas fa-search-dollar"></span></a>
<a href="#sale-price" title='{% trans "Show sale price" %}'><span class="fas fa-chart-bar"></span></a>
</td>
<td>{% trans 'Unit Cost' %}</td>
<td colspan='2'>{% include "price.html" with price=unit_part_price %}</td>
</tr>
<tr>
<td></td>
<td>{% trans 'Total Cost' %}</td>
<td colspan='2'>{% include "price.html" with price=total_part_price %}</td>
</tr>
{% endif %}
</table>
{% if min_unit_buy_price or min_unit_bom_price %}
{% else %}
<div class='alert alert-danger alert-block'>
{% trans 'No pricing information is available for this part.' %}
</div>
{% endif %}
</div>
<div class="col col-md-6">
<h4>{% trans "Calculation parameters" %}</h4>
<form method="post">
{% csrf_token %}
{{ form|crispy }}
<input type="submit" value="{% trans 'Calculate' %}" class="btn btn-primary btn-block">
</form>
</div>
</div>
{% endblock %}
{% block post_content_panel %}
{% default_currency as currency %}
{% settings_value "PART_INTERNAL_PRICE" as show_internal_price %}
{% if part.purchaseable and roles.purchase_order.view %}
<div class='panel panel-default panel-inventree'>
<a class="anchor" id="supplier-cost"></a>
<div class='panel-heading'>
<h4>{% trans "Supplier Cost" %}
<a href="#overview" title='{% trans "Jump to overview" %}'><span class="fas fa-level-up-alt"></span></a>
</h4>
</div>
<div class='panel-content'><div class="row">
<div class="col col-md-6">
<h4>{% trans "Suppliers" %}</h4>
<table class="table table-striped table-condensed" id='supplier-table' data-toolbar='#button-toolbar'></table>
</div>
<div class="col col-md-6">
<h4>{% trans "Manufacturers" %}</h4>
<table class="table table-striped table-condensed" id='manufacturer-table' data-toolbar='#button-toolbar'></table>
</div>
</div></div>
</div>
<div class='panel panel-default panel-inventree'>
<a class="anchor" id="purchase-price"></a>
<div class='panel-heading'>
<h4>{% trans "Purchase Price" %}
<a href="#overview" title='{% trans "Jump to overview" %}'><span class="fas fa-level-up-alt"></span></a>
</h4>
</div>
{% if price_history %}
<h4>{% trans 'Stock Pricing' %}<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>
{% if price_history|length > 0 %}
<div style="max-width: 99%; min-height: 300px">
<canvas id="StockPriceChart"></canvas>
</div>
{% else %}
<div class='alert alert-danger alert-block'>
{% trans 'No stock pricing history is available for this part.' %}
</div>
{% endif %}
{% endif %}
</div>
{% endif %}
{% if show_internal_price and roles.sales_order.view %}
<div class='panel panel-default panel-inventree'>
<a class="anchor" id="internal-cost"></a>
<div class='panel-heading'>
<h4>{% trans "Internal Cost" %}
<a href="#overview" title='{% trans "Jump to overview" %}'><span class="fas fa-level-up-alt"></span></a>
</h4>
</div>
<div class='panel-content'><div class="row full-height">
<div class="col col-md-8">
<div style="max-width: 99%; height: 100%;">
<canvas id="InternalPriceBreakChart"></canvas>
</div>
</div>
<div class="col col-md-4">
<div id='internal-price-break-toolbar' class='btn-group'>
<button class='btn btn-primary' id='new-internal-price-break' type='button'>
<span class='fas fa-plus-circle'></span> {% trans "Add Internal Price Break" %}
</button>
</div>
<table class='table table-striped table-condensed' id='internal-price-break-table' data-toolbar='#internal-price-break-toolbar'
data-sort-name="quantity" data-sort-order="asc">
</table>
</div>
</div></div>
</div>
{% endif %}
{% if part.has_bom and roles.sales_order.view %}
<div class='panel panel-default panel-inventree'>
<a class="anchor" id="bom-cost"></a>
<div class='panel-heading'>
<h4>{% trans "BOM Cost" %}
<a href="#overview" title='{% trans "Jump to overview" %}'><span class="fas fa-level-up-alt"></span></a>
</h4>
</div>
<div class='panel-content'><div class="row">
<div class="col col-md-6">
<table class='table table-bom table-condensed' data-toolbar="#button-toolbar" id='bom-table'></table>
</div>
{% if part.bom_count > 0 %}
<div class="col col-md-6">
<h4>{% trans 'BOM Pricing' %}</h4>
<div style="max-width: 99%;">
<canvas id="BomChart"></canvas>
</div>
</div>
{% endif %}
</div></div>
</div>
{% endif %}
{% if part.salable and roles.sales_order.view %}
<div class='panel panel-default panel-inventree'>
<a class="anchor" id="sale-cost"></a>
<div class='panel-heading'>
<h4>{% trans "Sale Cost" %}
<a href="#overview" title='{% trans "Jump to overview" %}'><span class="fas fa-level-up-alt"></span></a>
</h4>
</div>
<div class='panel-content'><div class="row full-height">
<div class="col col-md-8">
<div style="max-width: 99%; height: 100%;">
<canvas id="SalePriceBreakChart"></canvas>
</div>
</div>
<div class="col col-md-4">
<div id='price-break-toolbar' class='btn-group'>
<button class='btn btn-primary' id='new-price-break' type='button'>
<span class='fas fa-plus-circle'></span> {% trans "Add Price Break" %}
</button>
</div>
<table class='table table-striped table-condensed' id='price-break-table' data-toolbar='#price-break-toolbar'
data-sort-name="quantity" data-sort-order="asc">
</table>
</div>
</div></div>
</div>
<div class='panel panel-default panel-inventree'>
<a class="anchor" id="sale-price"></a>
<div class='panel-heading'>
<h4>{% trans "Sale Price" %}
<a href="#overview" title='{% trans "Jump to overview" %}'><span class="fas fa-level-up-alt"></span></a>
</h4>
</div>
<div class='panel-content'>
{% if sale_history|length > 0 %}
<div style="max-width: 99%; min-height: 300px">
<canvas id="SalePriceChart"></canvas>
</div>
{% else %}
<div class='alert alert-danger alert-block'>
{% trans 'No sale pice history available for this part.' %}
</div>
{% endif %}
</div>
</div>
{% 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

@ -1,108 +0,0 @@
{% extends "part/part_base.html" %}
{% load static %}
{% load i18n %}
{% block menubar %}
{% include 'part/navbar.html' with tab='sales-prices' %}
{% endblock %}
{% block heading %}
{% trans "Sell Price Information" %}
{% endblock %}
{% block details %}
<div id='price-break-toolbar' class='btn-group'>
<button class='btn btn-primary' id='new-price-break' type='button'>
<span class='fas fa-plus-circle'></span> {% trans "Add Price Break" %}
</button>
</div>
<table class='table table-striped table-condensed' id='price-break-table' data-toolbar='#price-break-toolbar'>
</table>
{% endblock %}
{% block js_ready %}
{{ block.super }}
function reloadPriceBreaks() {
$("#price-break-table").bootstrapTable("refresh");
}
$('#new-price-break').click(function() {
launchModalForm("{% url 'sale-price-break-create' %}",
{
success: reloadPriceBreaks,
data: {
part: {{ part.id }},
}
}
);
});
$('#price-break-table').inventreeTable({
name: 'saleprice',
formatNoMatches: function() { return "{% trans 'No price break information found' %}"; },
queryParams: {
part: {{ part.id }},
},
url: "{% url 'api-part-sale-price-list' %}",
onPostBody: function() {
var table = $('#price-break-table');
table.find('.button-price-break-delete').click(function() {
var pk = $(this).attr('pk');
launchModalForm(
`/part/sale-price/${pk}/delete/`,
{
success: reloadPriceBreaks
}
);
});
table.find('.button-price-break-edit').click(function() {
var pk = $(this).attr('pk');
launchModalForm(
`/part/sale-price/${pk}/edit/`,
{
success: reloadPriceBreaks
}
);
});
},
columns: [
{
field: 'pk',
title: 'ID',
visible: false,
switchable: false,
},
{
field: 'quantity',
title: '{% trans "Quantity" %}',
sortable: true,
},
{
field: 'price',
title: '{% trans "Price" %}',
sortable: true,
formatter: function(value, row, index) {
var html = value;
html += `<div class='btn-group float-right' role='group'>`
html += makeIconButton('fa-edit icon-blue', 'button-price-break-edit', row.pk, '{% trans "Edit price break" %}');
html += makeIconButton('fa-trash-alt icon-red', 'button-price-break-delete', row.pk, '{% trans "Delete price break" %}');
html += `</div>`;
return html;
}
},
]
})
{% endblock %}

View File

@ -65,13 +65,11 @@ 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'^prices/', views.PartPricingView.as_view(template_name='part/prices.html'), name='part-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'),
url(r'^sales-orders/', views.PartDetail.as_view(template_name='part/sales_orders.html'), name='part-sales-orders'),
url(r'^sale-prices/', views.PartDetail.as_view(template_name='part/sale_prices.html'), name='part-sale-prices'),
url(r'^internal-prices/', views.PartDetail.as_view(template_name='part/internal_prices.html'), name='part-internal-prices'),
url(r'^tests/', views.PartDetail.as_view(template_name='part/part_tests.html'), name='part-test-templates'),
url(r'^track/?', views.PartDetail.as_view(template_name='part/track.html'), name='part-track'),
url(r'^related-parts/?', views.PartDetail.as_view(template_name='part/related.html'), name='part-related'),

View File

@ -50,6 +50,7 @@ import common.settings as inventree_settings
from . import forms as part_forms
from .bom import MakeBomTemplate, BomUploadManager, ExportBom, IsValidBOMFormat
from order.models import PurchaseOrderLineItem
from .admin import PartResource
@ -979,18 +980,20 @@ class PartPricingView(PartDetail):
""" returns context with pricing information """
ctx = PartPricing.get_pricing(self, quantity, currency)
part = self.get_part()
default_currency = inventree_settings.currency_code_default()
# Stock history
if part.total_stock > 1:
price_history = []
stock = part.stock_entries(include_variants=False, in_stock=True) # .order_by('purchase_order__date')
stock = stock.prefetch_related('purchase_order', 'supplier_part')
stock = part.stock_entries(include_variants=False, in_stock=True).\
order_by('purchase_order__issue_date').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())
price = convert_money(stock_item.purchase_price, default_currency)
line = {
'price': price.amount,
'qty': stock_item.quantity
@ -1036,6 +1039,36 @@ class PartPricingView(PartDetail):
# add to global context
ctx['bom_parts'] = ctx_bom_parts
# Sale price history
sale_items = PurchaseOrderLineItem.objects.filter(part__part=part).order_by('order__issue_date').\
prefetch_related('order', ).all()
if sale_items:
sale_history = []
for sale_item in sale_items:
# check for not fully defined elements
if None in [sale_item.purchase_price, sale_item.quantity]:
continue
price = convert_money(sale_item.purchase_price, default_currency)
line = {
'price': price.amount if price else 0,
'qty': sale_item.quantity,
}
# set date for graph labels
if sale_item.order.issue_date:
line['date'] = sale_item.order.issue_date.strftime('%d.%m.%Y')
elif sale_item.order.creation_date:
line['date'] = sale_item.order.creation_date.strftime('%d.%m.%Y')
else:
line['date'] = _('None')
sale_history.append(line)
ctx['sale_history'] = sale_history
return ctx
def get_initials(self):

View File

@ -769,6 +769,159 @@ function loadPartTestTemplateTable(table, options) {
}
function loadPriceBreakTable(table, options) {
/*
* Load PriceBreak table.
*/
var name = options.name || 'pricebreak';
var human_name = options.human_name || 'price break';
var linkedGraph = options.linkedGraph || null;
var chart = null;
table.inventreeTable({
name: name,
method: 'get',
formatNoMatches: function() {
return `{% trans "No ${human_name} information found" %}`;
},
url: options.url,
onLoadSuccess: function(tableData) {
if (linkedGraph) {
// sort array
tableData = tableData.sort((a,b)=>a.quantity-b.quantity);
// split up for graph definition
var graphLabels = Array.from(tableData, x => x.quantity);
var graphData = Array.from(tableData, x => parseFloat(x.price));
// destroy chart if exists
if (chart){
chart.destroy();
}
chart = loadLineChart(linkedGraph,
{
labels: graphLabels,
datasets: [
{
label: '{% trans "Unit Price" %}',
data: graphData,
backgroundColor: 'rgba(255, 206, 86, 0.2)',
borderColor: 'rgb(255, 206, 86)',
stepped: true,
fill: true,
},]
}
);
}
},
columns: [
{
field: 'pk',
title: 'ID',
visible: false,
switchable: false,
},
{
field: 'quantity',
title: '{% trans "Quantity" %}',
sortable: true,
},
{
field: 'price',
title: '{% trans "Price" %}',
sortable: true,
formatter: function(value, row, index) {
var html = value;
html += `<div class='btn-group float-right' role='group'>`
html += makeIconButton('fa-edit icon-blue', `button-${name}-edit`, row.pk, `{% trans "Edit ${human_name}" %}`);
html += makeIconButton('fa-trash-alt icon-red', `button-${name}-delete`, row.pk, `{% trans "Delete ${human_name}" %}`);
html += `</div>`;
return html;
}
},
]
});
}
function loadLineChart(context, data) {
return new Chart(context, {
type: 'line',
data: data,
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {position: 'bottom'},
}
}
});
}
function initPriceBreakSet(table, options) {
var part_id = options.part_id;
var pb_human_name = options.pb_human_name;
var pb_url_slug = options.pb_url_slug;
var pb_url = options.pb_url;
var pb_new_btn = options.pb_new_btn;
var pb_new_url = options.pb_new_url;
var linkedGraph = options.linkedGraph || null;
loadPriceBreakTable(
table,
{
name: pb_url_slug,
human_name: pb_human_name,
url: pb_url,
linkedGraph: linkedGraph,
}
);
function reloadPriceBreakTable(){
table.bootstrapTable("refresh");
}
pb_new_btn.click(function() {
launchModalForm(pb_new_url,
{
success: reloadPriceBreakTable,
data: {
part: part_id,
}
}
);
});
table.on('click', `.button-${pb_url_slug}-delete`, function() {
var pk = $(this).attr('pk');
launchModalForm(
`/part/${pb_url_slug}/${pk}/delete/`,
{
success: reloadPriceBreakTable
}
);
});
table.on('click', `.button-${pb_url_slug}-edit`, function() {
var pk = $(this).attr('pk');
launchModalForm(
`/part/${pb_url_slug}/${pk}/edit/`,
{
success: reloadPriceBreakTable
}
);
});
}
function loadStockPricingChart(context, data) {
return new Chart(context, {
type: 'bar',
@ -824,3 +977,36 @@ function loadBomChart(context, data) {
}
});
}
function loadSellPricingChart(context, data) {
return new Chart(context, {
type: 'line',
data: data,
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {legend: {position: 'bottom'}},
scales: {
y: {
type: 'linear',
position: 'left',
grid: {display: false},
title: {
display: true,
text: '{% trans "Unit Price" %}'
}
},
y1: {
type: 'linear',
position: 'right',
grid: {display: false},
titel: {
display: true,
text: '{% trans "Quantity" %}',
position: 'right'
}
},
},
}
});
}