mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge pull request #1482 from matmair/price-history
Stock price history
This commit is contained in:
commit
7f58d95254
13
InvenTree/InvenTree/static/script/chart.min.js
vendored
Normal file
13
InvenTree/InvenTree/static/script/chart.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
InvenTree/InvenTree/static/script/randomColor.min.js
vendored
Normal file
1
InvenTree/InvenTree/static/script/randomColor.min.js
vendored
Normal file
@ -0,0 +1 @@
|
||||
!function(r,e){var n;"object"==typeof exports?(n=e(),"object"==typeof module&&module&&module.exports&&(exports=module.exports=n),exports.randomColor=n):"function"==typeof define&&define.amd?define([],e):r.randomColor=e()}(this,function(){var o=null,s={};r("monochrome",null,[[0,0],[100,0]]),r("red",[-26,18],[[20,100],[30,92],[40,89],[50,85],[60,78],[70,70],[80,60],[90,55],[100,50]]),r("orange",[18,46],[[20,100],[30,93],[40,88],[50,86],[60,85],[70,70],[100,70]]),r("yellow",[46,62],[[25,100],[40,94],[50,89],[60,86],[70,84],[80,82],[90,80],[100,75]]),r("green",[62,178],[[30,100],[40,90],[50,85],[60,81],[70,74],[80,64],[90,50],[100,40]]),r("blue",[178,257],[[20,100],[30,86],[40,80],[50,74],[60,60],[70,52],[80,44],[90,39],[100,35]]),r("purple",[257,282],[[20,100],[30,87],[40,79],[50,70],[60,65],[70,59],[80,52],[90,45],[100,42]]),r("pink",[282,334],[[20,100],[30,90],[40,86],[60,84],[80,80],[90,75],[100,73]]);var i=[],f=function(r){if(void 0!==(r=r||{}).seed&&null!==r.seed&&r.seed===parseInt(r.seed,10))o=r.seed;else if("string"==typeof r.seed)o=function(r){for(var e=0,n=0;n!==r.length&&!(e>=Number.MAX_SAFE_INTEGER);n++)e+=r.charCodeAt(n);return e}(r.seed);else{if(void 0!==r.seed&&null!==r.seed)throw new TypeError("The seed value must be an integer or string");o=null}var e,n;if(null===r.count||void 0===r.count)return function(r,e){switch(e.format){case"hsvArray":return r;case"hslArray":return d(r);case"hsl":var n=d(r);return"hsl("+n[0]+", "+n[1]+"%, "+n[2]+"%)";case"hsla":var t=d(r),a=e.alpha||Math.random();return"hsla("+t[0]+", "+t[1]+"%, "+t[2]+"%, "+a+")";case"rgbArray":return h(r);case"rgb":return"rgb("+h(r).join(", ")+")";case"rgba":var u=h(r),a=e.alpha||Math.random();return"rgba("+u.join(", ")+", "+a+")";default:return function(r){var e=h(r);function n(r){var e=r.toString(16);return 1==e.length?"0"+e:e}return"#"+n(e[0])+n(e[1])+n(e[2])}(r)}}([e=function(r){{if(0<i.length){var e=c(o=function(r){if(isNaN(r)){if("string"==typeof r)if(s[r]){var e=s[r];if(e.hueRange)return e.hueRange}else if(r.match(/^#?([0-9A-F]{3}|[0-9A-F]{6})$/i)){return l(g(r)[0]).hueRange}}else{var n=parseInt(r);if(n<360&&0<n)return l(r).hueRange}return[0,360]}(r.hue)),n=(o[1]-o[0])/i.length,t=parseInt((e-o[0])/n);!0===i[t]?t=(t+2)%i.length:i[t]=!0;var a=(o[0]+t*n)%359,u=(o[0]+(t+1)*n)%359;return(e=c(o=[a,u]))<0&&(e=360+e),e}var o=function(r){if("number"==typeof parseInt(r)){var e=parseInt(r);if(e<360&&0<e)return[e,e]}if("string"==typeof r)if(s[r]){var n=s[r];if(n.hueRange)return n.hueRange}else if(r.match(/^#?([0-9A-F]{3}|[0-9A-F]{6})$/i)){var t=g(r)[0];return[t,t]}return[0,360]}(r.hue);return(e=c(o))<0&&(e=360+e),e}}(r),n=function(r,e){if("monochrome"===e.hue)return 0;if("random"===e.luminosity)return c([0,100]);var n=function(r){return l(r).saturationRange}(r),t=n[0],a=n[1];switch(e.luminosity){case"bright":t=55;break;case"dark":t=a-10;break;case"light":a=55}return c([t,a])}(e,r),function(r,e,n){var t=function(r,e){for(var n=l(r).lowerBounds,t=0;t<n.length-1;t++){var a=n[t][0],u=n[t][1],o=n[t+1][0],s=n[t+1][1];if(a<=e&&e<=o){var i=(s-u)/(o-a);return i*e+(u-i*a)}}return 0}(r,e),a=100;switch(n.luminosity){case"dark":a=t+20;break;case"light":t=(a+t)/2;break;case"random":t=0,a=100}return c([t,a])}(e,n,r)],r);for(var t=r.count,a=[],u=0;u<r.count;u++)i.push(!1);for(r.count=null;t>a.length;)o&&r.seed&&(r.seed+=1),a.push(f(r));return r.count=t,a};function l(r){for(var e in 334<=r&&r<=360&&(r-=360),s){var n=s[e];if(n.hueRange&&r>=n.hueRange[0]&&r<=n.hueRange[1])return s[e]}return"Color not found"}function c(r){if(null===o){var e=Math.random();return e+=.618033988749895,e%=1,Math.floor(r[0]+e*(r[1]+1-r[0]))}var n=r[1]||1,t=r[0]||0,a=(o=(9301*o+49297)%233280)/233280;return Math.floor(t+a*(n-t))}function r(r,e,n){var t=n[0][0],a=n[n.length-1][0],u=n[n.length-1][1],o=n[0][1];s[r]={hueRange:e,lowerBounds:n,saturationRange:[t,a],brightnessRange:[u,o]}}function h(r){var e=r[0];0===e&&(e=1),360===e&&(e=359),e/=360;var n=r[1]/100,t=r[2]/100,a=Math.floor(6*e),u=6*e-a,o=t*(1-n),s=t*(1-u*n),i=t*(1-(1-u)*n),f=256,l=256,c=256;switch(a){case 0:f=t,l=i,c=o;break;case 1:f=s,l=t,c=o;break;case 2:f=o,l=t,c=i;break;case 3:f=o,l=s,c=t;break;case 4:f=i,l=o,c=t;break;case 5:f=t,l=o,c=s}return[Math.floor(255*f),Math.floor(255*l),Math.floor(255*c)]}function g(r){r=3===(r=r.replace(/^#/,"")).length?r.replace(/(.)/g,"$1$1"):r;var e=parseInt(r.substr(0,2),16)/255,n=parseInt(r.substr(2,2),16)/255,t=parseInt(r.substr(4,2),16)/255,a=Math.max(e,n,t),u=a-Math.min(e,n,t),o=a?u/a:0;switch(a){case e:return[(n-t)/u%6*60||0,o,a];case n:return[60*((t-e)/u+2)||0,o,a];case t:return[60*((e-n)/u+4)||0,o,a]}}function d(r){var e=r[0],n=r[1]/100,t=r[2]/100,a=(2-n)*t;return[e,Math.round(n*t/(a<1?a:2-a)*1e4)/100,a/2*100]}return f});
|
@ -50,6 +50,15 @@
|
||||
supplier: 3
|
||||
status: 40 # Cancelled
|
||||
|
||||
# for pricebreaks
|
||||
- model: order.purchaseorder
|
||||
pk: 7
|
||||
fields:
|
||||
reference: '0007'
|
||||
description: 'Another PO'
|
||||
supplier: 2
|
||||
status: 10 # Pending
|
||||
|
||||
# Add some line items against PO 0001
|
||||
|
||||
# 100 x ACME0001 (M2x4 LPHS)
|
||||
|
@ -60,18 +60,18 @@ class PurchaseOrderTest(OrderTest):
|
||||
def test_po_list(self):
|
||||
|
||||
# List *ALL* PO items
|
||||
self.filter({}, 6)
|
||||
self.filter({}, 7)
|
||||
|
||||
# Filter by supplier
|
||||
self.filter({'supplier': 1}, 1)
|
||||
self.filter({'supplier': 3}, 5)
|
||||
|
||||
# Filter by "outstanding"
|
||||
self.filter({'outstanding': True}, 4)
|
||||
self.filter({'outstanding': True}, 5)
|
||||
self.filter({'outstanding': False}, 2)
|
||||
|
||||
# Filter by "status"
|
||||
self.filter({'status': 10}, 2)
|
||||
self.filter({'status': 10}, 3)
|
||||
self.filter({'status': 40}, 1)
|
||||
|
||||
def test_overdue(self):
|
||||
@ -80,14 +80,14 @@ class PurchaseOrderTest(OrderTest):
|
||||
"""
|
||||
|
||||
self.filter({'overdue': True}, 0)
|
||||
self.filter({'overdue': False}, 6)
|
||||
self.filter({'overdue': False}, 7)
|
||||
|
||||
order = PurchaseOrder.objects.get(pk=1)
|
||||
order.target_date = datetime.now().date() - timedelta(days=10)
|
||||
order.save()
|
||||
|
||||
self.filter({'overdue': True}, 1)
|
||||
self.filter({'overdue': False}, 5)
|
||||
self.filter({'overdue': False}, 6)
|
||||
|
||||
def test_po_detail(self):
|
||||
|
||||
|
@ -21,6 +21,7 @@ class OrderTest(TestCase):
|
||||
fixtures = [
|
||||
'company',
|
||||
'supplier_part',
|
||||
'price_breaks',
|
||||
'category',
|
||||
'part',
|
||||
'location',
|
||||
@ -63,7 +64,7 @@ class OrderTest(TestCase):
|
||||
|
||||
next_ref = PurchaseOrder.getNextOrderNumber()
|
||||
|
||||
self.assertEqual(next_ref, '0007')
|
||||
self.assertEqual(next_ref, '0008')
|
||||
|
||||
def test_on_order(self):
|
||||
""" There should be 3 separate items on order for the M2x4 LPHS part """
|
||||
@ -117,6 +118,39 @@ class OrderTest(TestCase):
|
||||
with self.assertRaises(django_exceptions.ValidationError):
|
||||
order.add_line_item(sku, 99)
|
||||
|
||||
def test_pricing(self):
|
||||
""" Test functions for adding line items to an order including price-breaks """
|
||||
|
||||
order = PurchaseOrder.objects.get(pk=7)
|
||||
|
||||
self.assertEqual(order.status, PurchaseOrderStatus.PENDING)
|
||||
self.assertEqual(order.lines.count(), 0)
|
||||
|
||||
sku = SupplierPart.objects.get(SKU='ZERGM312')
|
||||
part = sku.part
|
||||
|
||||
# Order the part
|
||||
self.assertEqual(part.on_order, 0)
|
||||
|
||||
# Order 25 with manually set high value
|
||||
pp = sku.get_price(25)
|
||||
order.add_line_item(sku, 25, purchase_price=pp)
|
||||
self.assertEqual(part.on_order, 25)
|
||||
self.assertEqual(order.lines.count(), 1)
|
||||
self.assertEqual(order.lines.first().purchase_price.amount, 200)
|
||||
|
||||
# Add a few, now the pricebreak should adjust although wrong price given
|
||||
order.add_line_item(sku, 10, purchase_price=sku.get_price(25))
|
||||
self.assertEqual(part.on_order, 35)
|
||||
self.assertEqual(order.lines.count(), 1)
|
||||
self.assertEqual(order.lines.first().purchase_price.amount, 8)
|
||||
|
||||
# Order the same part again (it should be merged)
|
||||
order.add_line_item(sku, 100, purchase_price=sku.get_price(100))
|
||||
self.assertEqual(order.lines.count(), 1)
|
||||
self.assertEqual(part.on_order, 135)
|
||||
self.assertEqual(order.lines.first().purchase_price.amount, 1.25)
|
||||
|
||||
def test_receive(self):
|
||||
""" Test order receiving functions """
|
||||
|
||||
|
@ -58,7 +58,7 @@ def part_salable_default():
|
||||
|
||||
def part_trackable_default():
|
||||
"""
|
||||
Returns the defualt value fro the 'trackable' field for a Part object
|
||||
Returns the default value for the 'trackable' field for a Part object
|
||||
"""
|
||||
|
||||
return InvenTreeSetting.get_setting('PART_TRACKABLE')
|
||||
|
@ -69,6 +69,12 @@
|
||||
</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 %}'>
|
||||
<span class='menu-tab-icon fas fa-dollar-sign' style='width: 20px;'></span>
|
||||
{% trans "Order Price" %}
|
||||
</a>
|
||||
</li>
|
||||
<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'></span>
|
||||
|
208
InvenTree/part/templates/part/order_prices.html
Normal file
208
InvenTree/part/templates/part/order_prices.html
Normal file
@ -0,0 +1,208 @@
|
||||
{% 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 %}
|
||||
{% settings_value "INVENTREE_DEFAULT_CURRENCY" as currency %}
|
||||
|
||||
{% crispy form %}
|
||||
|
||||
<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 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 prices of stock for this part
|
||||
the part single price shown is the current price for that supplier part"></i></h4>
|
||||
{% if price_history|length > 1 %}
|
||||
<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 }}
|
||||
|
||||
{% settings_value "INVENTREE_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'
|
||||
},
|
||||
{
|
||||
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'
|
||||
},
|
||||
{% 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: [
|
||||
{% if bom_pie_min %}
|
||||
{
|
||||
label: 'Max Price',
|
||||
data: [{% for line in bom_parts %}{{ line.max_price }},{% endfor %}],
|
||||
backgroundColor: bom_colors,
|
||||
},
|
||||
{% endif %}
|
||||
{
|
||||
label: 'Price',
|
||||
data: [{% for line in bom_parts %}{% if bom_pie_min %}{{ line.min_price }}{% else %}{{ line.price }}{% endif%},{% endfor %}],
|
||||
backgroundColor: bom_colors,
|
||||
}
|
||||
]
|
||||
};
|
||||
var BomChart = loadBomChart(document.getElementById('BomChart'), bomdata)
|
||||
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
@ -3,7 +3,6 @@
|
||||
{% load i18n %}
|
||||
|
||||
{% block pre_form_content %}
|
||||
|
||||
<table class='table table-striped table-condensed table-price-two'>
|
||||
<tr>
|
||||
<td><b>{% trans 'Part' %}</b></td>
|
||||
@ -95,4 +94,4 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
<hr>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
@ -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'),
|
||||
|
@ -19,6 +19,7 @@ from django.forms import HiddenInput, CheckboxInput
|
||||
from django.conf import settings
|
||||
|
||||
from moneyed import CURRENCIES
|
||||
from djmoney.contrib.exchange.models import convert_money
|
||||
|
||||
from PIL import Image
|
||||
|
||||
@ -782,6 +783,94 @@ 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
|
||||
|
||||
# BOM Information for Pie-Chart
|
||||
bom_items = [{'name': str(a.sub_part), 'price': a.sub_part.get_price_range(quantity), 'q': a.quantity} for a in part.bom_items.all()]
|
||||
if [True for a in bom_items if len(set(a['price'])) == 2]:
|
||||
ctx['bom_parts'] = [{
|
||||
'name': a['name'],
|
||||
'min_price': str((a['price'][0] * a['q']) / quantity),
|
||||
'max_price': str((a['price'][1] * a['q']) / quantity)} for a in bom_items]
|
||||
ctx['bom_pie_min'] = True
|
||||
else:
|
||||
ctx['bom_parts'] = [{
|
||||
'name': a['name'],
|
||||
'price': str((a['price'][0] * a['q']) / quantity)} for a in bom_items]
|
||||
|
||||
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'
|
||||
|
@ -138,7 +138,9 @@
|
||||
<script type="text/javascript" src="{% static 'fullcalendar/locales-all.js' %}"></script>
|
||||
<script type="text/javascript" src="{% static 'script/select2/select2.js' %}"></script>
|
||||
<script type='text/javascript' src="{% static 'script/moment.js' %}"></script>
|
||||
<script type='text/javascript' src="{% static 'script/chart.min.js' %}"></script>
|
||||
<script type='text/javascript' src="{% static 'script/clipboard.min.js' %}"></script>
|
||||
<script type='text/javascript' src="{% static 'script/randomColor.min.js' %}"></script>
|
||||
|
||||
<!-- general InvenTree -->
|
||||
<script type='text/javascript' src="{% static 'script/inventree/inventree.js' %}"></script>
|
||||
|
@ -688,3 +688,60 @@ function loadPartTestTemplateTable(table, options) {
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function loadStockPricingChart(context, data) {
|
||||
return new Chart(context, {
|
||||
type: 'bar',
|
||||
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 "Single Price" %}'
|
||||
}
|
||||
},
|
||||
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: '{% trans "Single Price Difference" %}'
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function loadBomChart(context, data) {
|
||||
return new Chart(context, {
|
||||
type: 'doughnut',
|
||||
data: data,
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {legend: {position: 'bottom'},
|
||||
scales: {xAxes: [{beginAtZero: true, ticks: {autoSkip: false}}]}}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user