mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-10-30 20:55:42 +00:00 
			
		
		
		
	Merge pull request #2695 from SchrodingersGat/scheduling
[WIP] Scheduling
This commit is contained in:
		
							
								
								
									
										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', | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user