mirror of
https://github.com/inventree/InvenTree.git
synced 2025-04-29 20:16:44 +00:00
Merge pull request #2695 from SchrodingersGat/scheduling
[WIP] Scheduling
This commit is contained in:
commit
fda556c289
13
InvenTree/InvenTree/static/script/chart.js
Normal file
13
InvenTree/InvenTree/static/script/chart.js
Normal file
File diff suppressed because one or more lines are too long
13
InvenTree/InvenTree/static/script/chart.min.js
vendored
13
InvenTree/InvenTree/static/script/chart.min.js
vendored
File diff suppressed because one or more lines are too long
@ -0,0 +1,8 @@
|
|||||||
|
/*!
|
||||||
|
* chartjs-adapter-moment v1.0.0
|
||||||
|
* https://www.chartjs.org
|
||||||
|
* (c) 2021 chartjs-adapter-moment Contributors
|
||||||
|
* Released under the MIT license
|
||||||
|
*/
|
||||||
|
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(require("moment"),require("chart.js")):"function"==typeof define&&define.amd?define(["moment","chart.js"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).moment,e.Chart)}(this,(function(e,t){"use strict";function n(e){return e&&"object"==typeof e&&"default"in e?e:{default:e}}var f=n(e);const a={datetime:"MMM D, YYYY, h:mm:ss a",millisecond:"h:mm:ss.SSS a",second:"h:mm:ss a",minute:"h:mm a",hour:"hA",day:"MMM D",week:"ll",month:"MMM YYYY",quarter:"[Q]Q - YYYY",year:"YYYY"};t._adapters._date.override("function"==typeof f.default?{_id:"moment",formats:function(){return a},parse:function(e,t){return"string"==typeof e&&"string"==typeof t?e=f.default(e,t):e instanceof f.default||(e=f.default(e)),e.isValid()?e.valueOf():null},format:function(e,t){return f.default(e).format(t)},add:function(e,t,n){return f.default(e).add(t,n).valueOf()},diff:function(e,t,n){return f.default(e).diff(f.default(t),n)},startOf:function(e,t,n){return e=f.default(e),"isoWeek"===t?(n=Math.trunc(Math.min(Math.max(0,n),6)),e.isoWeekday(n).startOf("day").valueOf()):e.startOf(t).valueOf()},endOf:function(e,t){return f.default(e).endOf(t).valueOf()}}:{})}));
|
||||||
|
//# sourceMappingURL=chartjs-adapter-moment.min.js.map
|
File diff suppressed because one or more lines are too long
@ -72,7 +72,7 @@ class ViewTests(TestCase):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# Change this number as more javascript files are added to the index page
|
# Change this number as more javascript files are added to the index page
|
||||||
N_SCRIPT_FILES = 35
|
N_SCRIPT_FILES = 36
|
||||||
|
|
||||||
content = self.get_index_page()
|
content = self.get_index_page()
|
||||||
|
|
||||||
|
@ -12,11 +12,14 @@ import common.models
|
|||||||
INVENTREE_SW_VERSION = "0.7.0 dev"
|
INVENTREE_SW_VERSION = "0.7.0 dev"
|
||||||
|
|
||||||
# InvenTree API version
|
# InvenTree API version
|
||||||
INVENTREE_API_VERSION = 28
|
INVENTREE_API_VERSION = 29
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Increment this API version number whenever there is a significant change to the API that any clients need to know about
|
Increment this API version number whenever there is a significant change to the API that any clients need to know about
|
||||||
|
|
||||||
|
v29 -> 2022-03-08
|
||||||
|
- Adds "scheduling" endpoint for predicted stock scheduling information
|
||||||
|
|
||||||
v28 -> 2022-03-04
|
v28 -> 2022-03-04
|
||||||
- Adds an API endpoint for auto allocation of stock items against a build order
|
- Adds an API endpoint for auto allocation of stock items against a build order
|
||||||
- Ref: https://github.com/inventree/InvenTree/pull/2713
|
- Ref: https://github.com/inventree/InvenTree/pull/2713
|
||||||
|
@ -1253,7 +1253,14 @@ class InvenTreeUserSetting(BaseInvenTreeSetting):
|
|||||||
('MM/DD/YYYY', '02/22/2022'),
|
('MM/DD/YYYY', '02/22/2022'),
|
||||||
('MMM DD YYYY', 'Feb 22 2022'),
|
('MMM DD YYYY', 'Feb 22 2022'),
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
|
|
||||||
|
'DISPLAY_SCHEDULE_TAB': {
|
||||||
|
'name': _('Part Scheduling'),
|
||||||
|
'description': _('Display part scheduling information'),
|
||||||
|
'default': True,
|
||||||
|
'validator': bool,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -5,6 +5,8 @@ Provides a JSON API for the Part app
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
|
||||||
from django.conf.urls import url, include
|
from django.conf.urls import url, include
|
||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
from django.db.models import Q, F, Count, Min, Max, Avg
|
from django.db.models import Q, F, Count, Min, Max, Avg
|
||||||
@ -40,7 +42,8 @@ from company.models import Company, ManufacturerPart, SupplierPart
|
|||||||
from stock.models import StockItem, StockLocation
|
from stock.models import StockItem, StockLocation
|
||||||
|
|
||||||
from common.models import InvenTreeSetting
|
from common.models import InvenTreeSetting
|
||||||
from build.models import Build
|
from build.models import Build, BuildItem
|
||||||
|
import order.models
|
||||||
|
|
||||||
from . import serializers as part_serializers
|
from . import serializers as part_serializers
|
||||||
|
|
||||||
@ -48,7 +51,7 @@ from InvenTree.helpers import str2bool, isNull, increment
|
|||||||
from InvenTree.helpers import DownloadFile
|
from InvenTree.helpers import DownloadFile
|
||||||
from InvenTree.api import AttachmentMixin
|
from InvenTree.api import AttachmentMixin
|
||||||
|
|
||||||
from InvenTree.status_codes import BuildStatus
|
from InvenTree.status_codes import BuildStatus, PurchaseOrderStatus, SalesOrderStatus
|
||||||
|
|
||||||
|
|
||||||
class CategoryList(generics.ListCreateAPIView):
|
class CategoryList(generics.ListCreateAPIView):
|
||||||
@ -430,6 +433,142 @@ class PartThumbsUpdate(generics.RetrieveUpdateAPIView):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class PartScheduling(generics.RetrieveAPIView):
|
||||||
|
"""
|
||||||
|
API endpoint for delivering "scheduling" information about a given part via the API.
|
||||||
|
|
||||||
|
Returns a chronologically ordered list about future "scheduled" events,
|
||||||
|
concerning stock levels for the part:
|
||||||
|
|
||||||
|
- Purchase Orders (incoming stock)
|
||||||
|
- Sales Orders (outgoing stock)
|
||||||
|
- Build Orders (incoming completed stock)
|
||||||
|
- Build Orders (outgoing allocated stock)
|
||||||
|
"""
|
||||||
|
|
||||||
|
queryset = Part.objects.all()
|
||||||
|
|
||||||
|
def retrieve(self, request, *args, **kwargs):
|
||||||
|
|
||||||
|
today = datetime.datetime.now().date()
|
||||||
|
|
||||||
|
part = self.get_object()
|
||||||
|
|
||||||
|
schedule = []
|
||||||
|
|
||||||
|
def add_schedule_entry(date, quantity, title, label, url):
|
||||||
|
"""
|
||||||
|
Check if a scheduled entry should be added:
|
||||||
|
- date must be non-null
|
||||||
|
- date cannot be in the "past"
|
||||||
|
- quantity must not be zero
|
||||||
|
"""
|
||||||
|
|
||||||
|
if date and date >= today and quantity != 0:
|
||||||
|
schedule.append({
|
||||||
|
'date': date,
|
||||||
|
'quantity': quantity,
|
||||||
|
'title': title,
|
||||||
|
'label': label,
|
||||||
|
'url': url,
|
||||||
|
})
|
||||||
|
|
||||||
|
# Add purchase order (incoming stock) information
|
||||||
|
po_lines = order.models.PurchaseOrderLineItem.objects.filter(
|
||||||
|
part__part=part,
|
||||||
|
order__status__in=PurchaseOrderStatus.OPEN,
|
||||||
|
)
|
||||||
|
|
||||||
|
for line in po_lines:
|
||||||
|
|
||||||
|
target_date = line.target_date or line.order.target_date
|
||||||
|
|
||||||
|
quantity = max(line.quantity - line.received, 0)
|
||||||
|
|
||||||
|
add_schedule_entry(
|
||||||
|
target_date,
|
||||||
|
quantity,
|
||||||
|
_('Incoming Purchase Order'),
|
||||||
|
str(line.order),
|
||||||
|
line.order.get_absolute_url()
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add sales order (outgoing stock) information
|
||||||
|
so_lines = order.models.SalesOrderLineItem.objects.filter(
|
||||||
|
part=part,
|
||||||
|
order__status__in=SalesOrderStatus.OPEN,
|
||||||
|
)
|
||||||
|
|
||||||
|
for line in so_lines:
|
||||||
|
|
||||||
|
target_date = line.target_date or line.order.target_date
|
||||||
|
|
||||||
|
quantity = max(line.quantity - line.shipped, 0)
|
||||||
|
|
||||||
|
add_schedule_entry(
|
||||||
|
target_date,
|
||||||
|
-quantity,
|
||||||
|
_('Outgoing Sales Order'),
|
||||||
|
str(line.order),
|
||||||
|
line.order.get_absolute_url(),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add build orders (incoming stock) information
|
||||||
|
build_orders = Build.objects.filter(
|
||||||
|
part=part,
|
||||||
|
status__in=BuildStatus.ACTIVE_CODES
|
||||||
|
)
|
||||||
|
|
||||||
|
for build in build_orders:
|
||||||
|
|
||||||
|
quantity = max(build.quantity - build.completed, 0)
|
||||||
|
|
||||||
|
add_schedule_entry(
|
||||||
|
build.target_date,
|
||||||
|
quantity,
|
||||||
|
_('Stock produced by Build Order'),
|
||||||
|
str(build),
|
||||||
|
build.get_absolute_url(),
|
||||||
|
)
|
||||||
|
|
||||||
|
"""
|
||||||
|
Add build order allocation (outgoing stock) information.
|
||||||
|
|
||||||
|
Here we need some careful consideration:
|
||||||
|
|
||||||
|
- 'Tracked' stock items are removed from stock when the individual Build Output is completed
|
||||||
|
- 'Untracked' stock items are removed from stock when the Build Order is completed
|
||||||
|
|
||||||
|
The 'simplest' approach here is to look at existing BuildItem allocations which reference this part,
|
||||||
|
and "schedule" them for removal at the time of build order completion.
|
||||||
|
|
||||||
|
This assumes that the user is responsible for correctly allocating parts.
|
||||||
|
|
||||||
|
However, it has the added benefit of side-stepping the various BOM substition options,
|
||||||
|
and just looking at what stock items the user has actually allocated against the Build.
|
||||||
|
"""
|
||||||
|
|
||||||
|
build_allocations = BuildItem.objects.filter(
|
||||||
|
stock_item__part=part,
|
||||||
|
build__status__in=BuildStatus.ACTIVE_CODES,
|
||||||
|
)
|
||||||
|
|
||||||
|
for allocation in build_allocations:
|
||||||
|
|
||||||
|
add_schedule_entry(
|
||||||
|
allocation.build.target_date,
|
||||||
|
-allocation.quantity,
|
||||||
|
_('Stock required for Build Order'),
|
||||||
|
str(allocation.build),
|
||||||
|
allocation.build.get_absolute_url(),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Sort by incrementing date values
|
||||||
|
schedule = sorted(schedule, key=lambda entry: entry['date'])
|
||||||
|
|
||||||
|
return Response(schedule)
|
||||||
|
|
||||||
|
|
||||||
class PartSerialNumberDetail(generics.RetrieveAPIView):
|
class PartSerialNumberDetail(generics.RetrieveAPIView):
|
||||||
"""
|
"""
|
||||||
API endpoint for returning extra serial number information about a particular part
|
API endpoint for returning extra serial number information about a particular part
|
||||||
@ -1734,6 +1873,9 @@ part_api_urls = [
|
|||||||
# Endpoint for extra serial number information
|
# Endpoint for extra serial number information
|
||||||
url(r'^serial-numbers/', PartSerialNumberDetail.as_view(), name='api-part-serial-number-detail'),
|
url(r'^serial-numbers/', PartSerialNumberDetail.as_view(), name='api-part-serial-number-detail'),
|
||||||
|
|
||||||
|
# Endpoint for future scheduling information
|
||||||
|
url(r'^scheduling/', PartScheduling.as_view(), name='api-part-scheduling'),
|
||||||
|
|
||||||
# Endpoint for duplicating a BOM for the specific Part
|
# Endpoint for duplicating a BOM for the specific Part
|
||||||
url(r'^bom-copy/', PartCopyBOM.as_view(), name='api-part-bom-copy'),
|
url(r'^bom-copy/', PartCopyBOM.as_view(), name='api-part-bom-copy'),
|
||||||
|
|
||||||
|
@ -32,6 +32,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% settings_value 'DISPLAY_SCHEDULE_TAB' user=request.user as show_scheduling %}
|
||||||
|
{% if show_scheduling %}
|
||||||
|
<div class='panel panel-hidden' id='panel-scheduling'>
|
||||||
|
<div class='panel-heading'>
|
||||||
|
<div class='d-flex flex-wrap'>
|
||||||
|
<h4>{% trans "Part Scheduling" %}</h4>
|
||||||
|
{% include "spacer.html" %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class='panel-content'>
|
||||||
|
{% include "part/part_scheduling.html" %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<div class='panel panel-hidden' id='panel-allocations'>
|
<div class='panel panel-hidden' id='panel-allocations'>
|
||||||
<div class='panel-heading'>
|
<div class='panel-heading'>
|
||||||
<div class='d-flex flex-wrap'>
|
<div class='d-flex flex-wrap'>
|
||||||
@ -417,6 +432,11 @@
|
|||||||
{% block js_ready %}
|
{% block js_ready %}
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
|
|
||||||
|
// Load the "scheduling" tab
|
||||||
|
onPanelLoad('scheduling', function() {
|
||||||
|
loadPartSchedulingChart('part-schedule-chart', {{ part.pk }});
|
||||||
|
});
|
||||||
|
|
||||||
// Load the "suppliers" tab
|
// Load the "suppliers" tab
|
||||||
onPanelLoad('suppliers', function() {
|
onPanelLoad('suppliers', function() {
|
||||||
|
|
||||||
|
6
InvenTree/part/templates/part/part_scheduling.html
Normal file
6
InvenTree/part/templates/part/part_scheduling.html
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{% load i18n %}
|
||||||
|
{% load inventree_extras %}
|
||||||
|
|
||||||
|
<div id='part-schedule' style='max-height: 300px;'>
|
||||||
|
<canvas id='part-schedule-chart' width='100%' style='max-height: 300px;'></canvas>
|
||||||
|
</div>
|
@ -44,6 +44,11 @@
|
|||||||
{% trans "Sales Orders" as text %}
|
{% trans "Sales Orders" as text %}
|
||||||
{% include "sidebar_item.html" with label="sales-orders" text=text icon="fa-truck" %}
|
{% include "sidebar_item.html" with label="sales-orders" text=text icon="fa-truck" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% settings_value 'DISPLAY_SCHEDULE_TAB' user=request.user as show_scheduling %}
|
||||||
|
{% if show_scheduling %}
|
||||||
|
{% trans "Scheduling" as text %}
|
||||||
|
{% include "sidebar_item.html" with label="scheduling" text=text icon="fa-calendar-alt" %}
|
||||||
|
{% endif %}
|
||||||
{% if part.trackable %}
|
{% if part.trackable %}
|
||||||
{% trans "Test Templates" as text %}
|
{% trans "Test Templates" as text %}
|
||||||
{% include "sidebar_item.html" with label="test-templates" text=text icon="fa-vial" %}
|
{% include "sidebar_item.html" with label="test-templates" text=text icon="fa-vial" %}
|
||||||
|
@ -18,6 +18,7 @@
|
|||||||
{% include "InvenTree/settings/setting.html" with key="DATE_DISPLAY_FORMAT" icon="fa-calendar-alt" user_setting=True %}
|
{% include "InvenTree/settings/setting.html" with key="DATE_DISPLAY_FORMAT" icon="fa-calendar-alt" user_setting=True %}
|
||||||
{% include "InvenTree/settings/setting.html" with key="FORMS_CLOSE_USING_ESCAPE" icon="fa-window-close" user_setting=True %}
|
{% include "InvenTree/settings/setting.html" with key="FORMS_CLOSE_USING_ESCAPE" icon="fa-window-close" user_setting=True %}
|
||||||
{% include "InvenTree/settings/setting.html" with key="PART_SHOW_QUANTITY_IN_FORMS" icon="fa-hashtag" user_setting=True %}
|
{% include "InvenTree/settings/setting.html" with key="PART_SHOW_QUANTITY_IN_FORMS" icon="fa-hashtag" user_setting=True %}
|
||||||
|
{% include "InvenTree/settings/setting.html" with key="DISPLAY_SCHEDULE_TAB" icon="fa-calendar-alt" user_setting=True %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
@ -153,8 +153,10 @@
|
|||||||
<script type="text/javascript" src="{% static 'fullcalendar/main.js' %}"></script>
|
<script type="text/javascript" src="{% static 'fullcalendar/main.js' %}"></script>
|
||||||
<script type="text/javascript" src="{% static 'fullcalendar/locales-all.js' %}"></script>
|
<script type="text/javascript" src="{% static 'fullcalendar/locales-all.js' %}"></script>
|
||||||
<script type="text/javascript" src="{% static 'select2/js/select2.full.js' %}"></script>
|
<script type="text/javascript" src="{% static 'select2/js/select2.full.js' %}"></script>
|
||||||
|
<script type='text/javascript' src="{% static 'script/chart.js' %}"></script>
|
||||||
<script type='text/javascript' src="{% static 'script/moment.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/chartjs-adapter-moment.js' %}"></script>
|
||||||
|
|
||||||
<script type='text/javascript' src="{% static 'script/clipboard.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>
|
<script type='text/javascript' src="{% static 'script/randomColor.min.js' %}"></script>
|
||||||
|
|
||||||
|
@ -33,6 +33,7 @@
|
|||||||
loadPartPurchaseOrderTable,
|
loadPartPurchaseOrderTable,
|
||||||
loadPartTable,
|
loadPartTable,
|
||||||
loadPartTestTemplateTable,
|
loadPartTestTemplateTable,
|
||||||
|
loadPartSchedulingChart,
|
||||||
loadPartVariantTable,
|
loadPartVariantTable,
|
||||||
loadRelatedPartsTable,
|
loadRelatedPartsTable,
|
||||||
loadSellPricingChart,
|
loadSellPricingChart,
|
||||||
@ -1981,6 +1982,123 @@ function initPriceBreakSet(table, options) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function loadPartSchedulingChart(canvas_id, part_id) {
|
||||||
|
|
||||||
|
var part_info = null;
|
||||||
|
|
||||||
|
// First, grab updated data for the particular part
|
||||||
|
inventreeGet(`/api/part/${part_id}/`, {}, {
|
||||||
|
async: false,
|
||||||
|
success: function(response) {
|
||||||
|
part_info = response;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var today = moment();
|
||||||
|
|
||||||
|
// Create an initial entry, using the available quantity
|
||||||
|
var stock_schedule = [
|
||||||
|
{
|
||||||
|
date: today,
|
||||||
|
delta: 0,
|
||||||
|
label: '{% trans "Current Stock" %}',
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
/* Request scheduling information for the part.
|
||||||
|
* Note that this information has already been 'curated' by the server,
|
||||||
|
* and arranged in increasing chronological order
|
||||||
|
*/
|
||||||
|
inventreeGet(
|
||||||
|
`/api/part/${part_id}/scheduling/`,
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
async: false,
|
||||||
|
success: function(response) {
|
||||||
|
response.forEach(function(entry) {
|
||||||
|
stock_schedule.push({
|
||||||
|
date: moment(entry.date),
|
||||||
|
delta: entry.quantity,
|
||||||
|
title: entry.title,
|
||||||
|
label: entry.label,
|
||||||
|
url: entry.url,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Iterate through future "events" to calculate expected quantity
|
||||||
|
|
||||||
|
var quantity = part_info.in_stock;
|
||||||
|
|
||||||
|
for (var idx = 0; idx < stock_schedule.length; idx++) {
|
||||||
|
|
||||||
|
quantity += stock_schedule[idx].delta;
|
||||||
|
|
||||||
|
stock_schedule[idx].x = stock_schedule[idx].date.format('YYYY-MM-DD');
|
||||||
|
stock_schedule[idx].y = quantity;
|
||||||
|
}
|
||||||
|
|
||||||
|
var context = document.getElementById(canvas_id);
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
datasets: [{
|
||||||
|
label: '{% trans "Scheduled Stock Quantities" %}',
|
||||||
|
data: stock_schedule,
|
||||||
|
backgroundColor: 'rgb(220, 160, 80)',
|
||||||
|
borderWidth: 2,
|
||||||
|
borderColor: 'rgb(90, 130, 150)'
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
|
||||||
|
return new Chart(context, {
|
||||||
|
type: 'scatter',
|
||||||
|
data: data,
|
||||||
|
options: {
|
||||||
|
showLine: true,
|
||||||
|
stepped: true,
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
type: 'time',
|
||||||
|
min: today.format(),
|
||||||
|
position: 'bottom',
|
||||||
|
time: {
|
||||||
|
unit: 'day',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
tooltip: {
|
||||||
|
callbacks: {
|
||||||
|
label: function(item) {
|
||||||
|
return item.raw.label;
|
||||||
|
},
|
||||||
|
beforeLabel: function(item) {
|
||||||
|
return item.raw.title;
|
||||||
|
},
|
||||||
|
afterLabel: function(item) {
|
||||||
|
var delta = item.raw.delta;
|
||||||
|
|
||||||
|
if (delta == 0) {
|
||||||
|
delta = '';
|
||||||
|
} else {
|
||||||
|
delta = ` (${item.raw.delta > 0 ? '+' : ''}${item.raw.delta})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `{% trans "Quantity" %}: ${item.raw.y}${delta}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function loadStockPricingChart(context, data) {
|
function loadStockPricingChart(context, data) {
|
||||||
return new Chart(context, {
|
return new Chart(context, {
|
||||||
type: 'bar',
|
type: 'bar',
|
||||||
|
Loading…
x
Reference in New Issue
Block a user