2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-06-18 04:55:44 +00:00

Scheduling improvements (#3564)

* Handle case where initial API call fails

* Error if scheduling fails to retrieve

* Visual improvements for scheduling graph:

- Fixes for vertical scales
- Add "minimum stock level" line

* Refactor / improve query for list of BOM items a part can exist in

* Remove stock column from "substitute part" dialog

- Stock quantity no longer available in this serailizer

* Add a button to reload part scheduling information

* Add extra information to part scheduling API

- Include "speculative" quantity drawdown for build orders

* Add table of scheduling data

* Improved chart display

- Adds "minimum" and "maximum" expected values
- Adds table of scheduling information

* Bump API version

* Improve table rendering

* Improve axis scaling

* Add ability to dynamically refresh schedling data

* JS linting

* JS fix
This commit is contained in:
Oliver
2022-08-18 11:36:02 +10:00
committed by GitHub
parent 1d4a20d1d4
commit 32b11ec5af
7 changed files with 355 additions and 76 deletions

View File

@ -519,7 +519,6 @@ function bomSubstitutesDialog(bom_item_id, substitutes, options={}) {
</a>
</td>
<td id='description-${pk}'><em>${part.description}</em></td>
<td id='stock-${pk}'><em>${part.stock}</em></td>
<td>${buttons}</td>
</tr>
`;
@ -552,7 +551,6 @@ function bomSubstitutesDialog(bom_item_id, substitutes, options={}) {
<tr>
<th>{% trans "Part" %}</th>
<th>{% trans "Description" %}</th>
<th>{% trans "Stock" %}</th>
<th><!-- Actions --></th>
</tr>
</thead>

View File

@ -2257,6 +2257,14 @@ function initPriceBreakSet(table, options) {
}
/*
* Load a chart which displays projected scheduling information for a particular part.
* This takes into account:
* - Current stock levels / availability
* - Upcoming / scheduled build orders
* - Upcoming / scheduled sales orders
* - Upcoming / scheduled purchase orders
*/
function loadPartSchedulingChart(canvas_id, part_id) {
var part_info = null;
@ -2269,16 +2277,30 @@ function loadPartSchedulingChart(canvas_id, part_id) {
}
});
if (!part_info) {
console.error(`Error loading part information for part ${part_id}`);
return;
}
var today = moment();
// Create an initial entry, using the available quantity
var stock_schedule = [
{
date: today,
delta: 0,
label: '{% trans "Current Stock" %}',
}
];
/* Construct initial datasets for:
* - Scheduled quantity
* - Minimum speculative quantity
* - Maximum speculative quantity
*/
var quantity_scheduled = [{
date: today,
delta: 0,
}];
// We will construct the HTML table as we go
var table_html = '';
// The "known" initial stock quantity
var initial_stock_min = part_info.in_stock;
var initial_stock_max = part_info.in_stock;
/* Request scheduling information for the part.
* Note that this information has already been 'curated' by the server,
@ -2290,28 +2312,85 @@ function loadPartSchedulingChart(canvas_id, part_id) {
{
async: false,
success: function(response) {
response.forEach(function(entry) {
stock_schedule.push({
for (var idx = 0; idx < response.length; idx++) {
var entry = response[idx];
var date = entry.date != null ? moment(entry.date) : null;
var date_string = entry.date;
if (date == null) {
date_string = '<em>{% trans "No date specified" %}</em>';
date_string += `<span class='fas fa-exclamation-circle icon-red float-right' title='{% trans "No date specified" %}'></span>`;
} else if (date < today) {
date_string += `<span class='fas fa-exclamation-circle icon-yellow float-right' title='{% trans "Specified date is in the past" %}'></span>`;
}
var quantity_string = entry.quantity + entry.speculative_quantity;
if (entry.speculative_quantity != 0) {
quantity_string += `<span class='fas fa-info-circle icon-blue float-right' title='{% trans "Speculative" %}'></span>`;
}
// Add an entry to the scheduling table
table_html += `
<tr>
<td><a href="${entry.url}">${entry.label}</a></td>
<td>${entry.title}</td>
<td>${date_string}</td>
<td>${quantity_string}</td>
</tr>
`;
// If the date is unknown or in the past, we cannot make use of this information
// So we update the "speculative quantity"
if (date == null || date < today) {
if (entry.quantity < 0) initial_stock_min += entry.quantity;
if (entry.speculative_quantity < 0) initial_stock_min += entry.speculative_quantity;
if (entry.quantity > 0) initial_stock_max += entry.quantity;
if (entry.speculative_quantity > 0) initial_stock_max += entry.speculative_quantity;
// We do not add this entry to the graph
continue;
}
// Add an entry to the scheduled quantity
quantity_scheduled.push({
date: moment(entry.date),
delta: entry.quantity,
speculative: entry.speculative_quantity,
title: entry.title,
label: entry.label,
url: entry.url,
});
});
}
},
error: function(response) {
console.error(`Error retrieving scheduling information for part ${part_id}`);
was_error = true;
}
}
);
// If no scheduling information is available for the part,
// remove the chart and display a message instead
if (stock_schedule.length <= 1) {
if (quantity_scheduled.length <= 1) {
var message = `
<div class='alert alert-block alert-info'>
{% trans "No scheduling information available for this part" %}.<br>
{% trans "No scheduling information available for this part" %}.
</div>`;
if (was_error) {
message = `
<div class='alert alert-block alert-danger'>
{% trans "Error fetching scheduling information for this part" %}.
</div>
`;
}
var canvas_element = $('#part-schedule-chart');
canvas_element.closest('div').html(message);
@ -2319,30 +2398,126 @@ function loadPartSchedulingChart(canvas_id, part_id) {
return;
}
// Iterate through future "events" to calculate expected quantity
var y_min = 0;
var y_max = 0;
// Iterate through future "events" to calculate expected quantity values
var quantity = part_info.in_stock;
var speculative_min = initial_stock_min;
var speculative_max = initial_stock_max;
for (var idx = 0; idx < stock_schedule.length; idx++) {
// Datasets for speculative quantity
var q_spec_min = [];
var q_spec_max = [];
quantity += stock_schedule[idx].delta;
for (var idx = 0; idx < quantity_scheduled.length; idx++) {
stock_schedule[idx].x = stock_schedule[idx].date.format('YYYY-MM-DD');
stock_schedule[idx].y = quantity;
var speculative = quantity_scheduled[idx].speculative;
var date = quantity_scheduled[idx].date.format('YYYY-MM-DD');
var delta = quantity_scheduled[idx].delta;
// Update the running quantity
quantity += delta;
quantity_scheduled[idx].x = date;
quantity_scheduled[idx].y = quantity;
// Update minimum "speculative" quantity
speculative_min += delta;
speculative_max += delta;
if (speculative < 0) {
speculative_min += speculative;
} else if (speculative > 0) {
speculative_max += speculative;
}
q_spec_min.push({
x: date,
y: speculative_min,
label: 'label',
title: '',
});
q_spec_max.push({
x: date,
y: speculative_max,
label: 'label',
title: '',
});
// Update min / max values
if (quantity < y_min) y_min = quantity;
if (quantity > y_max) y_max = 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)'
}],
var data = {
datasets: [
{
label: '{% trans "Scheduled Stock Quantities" %}',
data: quantity_scheduled,
backgroundColor: 'rgba(160, 80, 220, 0.75)',
borderWidth: 3,
borderColor: 'rgb(160, 80, 220)'
},
{
label: '{% trans "Minimum Quantity" %}',
data: q_spec_min,
backgroundColor: 'rgba(220, 160, 80, 0.25)',
borderWidth: 2,
borderColor: 'rgba(220, 160, 80, 0.35)',
borderDash: [10, 5],
fill: '-1',
},
{
label: '{% trans "Maximum Quantity" %}',
data: q_spec_max,
backgroundColor: 'rgba(220, 160, 80, 0.25)',
borderWidth: 2,
borderColor: 'rgba(220, 160, 80, 0.35)',
borderDash: [10, 5],
fill: '-2',
},
],
};
if (part_info.minimum_stock) {
// Construct a 'minimum stock' threshold line
var minimum_stock_curve = [
{
x: today.format(),
y: part_info.minimum_stock,
},
{
x: quantity_scheduled[quantity_scheduled.length - 1].x,
y: part_info.minimum_stock,
}
];
data.datasets.push({
data: minimum_stock_curve,
label: '{% trans "Minimum Stock Level" %}',
backgroundColor: 'rgba(250, 50, 50, 0.1)',
borderColor: 'rgba(250, 50, 50, 0.5)',
borderDash: [5, 5],
fill: {
target: {
value: 0,
}
}
});
}
// Update the table
$('#part-schedule-table').find('tbody').html(table_html);
var y_range = y_max - y_min;
y_max += 0.1 * y_range;
y_min -= 0.1 * y_range;
return new Chart(context, {
type: 'scatter',
data: data,
@ -2359,7 +2534,8 @@ function loadPartSchedulingChart(canvas_id, part_id) {
},
},
y: {
beginAtZero: true,
min: y_min,
max: y_max,
}
},
plugins: {