mirror of
https://github.com/inventree/InvenTree.git
synced 2025-06-17 20:45:44 +00:00
[Feature] Stocktake reports (#4345)
* Add settings to control upcoming stocktake features * Adds migration for "cost range" when performing stocktake * Add cost data to PartStocktakeSerializer Implement a new custom serializer for currency data type * Refactor existing currency serializers * Update stocktake table and forms * Prevent trailing zeroes in forms * Calculate cost range when adding manual stocktake entry * Display interactive chart for part stocktake history * Ensure chart data are converted to common currency * Adds new model for building stocktake reports * Add admin integration for new model * Adds API endpoint to expose list of stocktake reports available for download - No ability to edit or delete via API * Add setting to control automated deletion of old stocktake reports * Updates for settings page - Load part stocktake report table - Refactor function to render a downloadable media file - Fix bug with forcing files to be downloaded - Split js code into separate templates - Make use of onPanelLoad functionalitty * Fix conflicting migration files * Adds API endpoint for manual generation of stocktake report * Offload task to generate new stocktake report * Adds python function to perform stocktake on a single part instance * Small bug fixes * Various tweaks - Prevent new stocktake models from triggering plugin events when created - Construct a simple csv dataset * Generate new report * Updates for report generation - Prefetch related data - Add extra columns - Keep track of stocktake instances (for saving to database later on) * Updates: - Add confirmation message - Serializer validation checks * Ensure that background worker is running before manually scheduling a new stocktake report * Add extra fields to stocktake models Also move code from part/models.py to part/tasks.py * Add 'part_count' to PartStocktakeReport table * Updates for stocktake generation - remove old performStocktake javascript code - Now handled by automated server-side calculation - Generate report for a single part * Add a new "role" for stocktake - Allows fine-grained control on viewing / creating / deleting stocktake data - More in-line with existing permission controls - Remove STOCKTAKE_OWNER setting * Add serializer field to limit stocktake report to particular locations * Use location restriction when generating a stocktake report * Add UI buttons to perform stocktake for a whole category tree * Add button to perform stocktake report for a location tree * Adds a background tasks to handle periodic generation of stocktake reports - Reports are generated at fixed intervals - Deletes old reports after certain number of days * Implement notifications for new stocktake reports - If manually requested by a user, notify that user - Cleanup notification table - Amend PartStocktakeModel for better notification rendering * Hide buttons on location and category page if stocktake is not enabled * Cleanup log messages during server start * Extend functionality of RoleRequired permission mixin - Allow 'role_required' attribute to be added to an API view - Useful when using a serializer class that does not have a model defined * Add boolean option to toggle whether a report will be generated * Update generateStocktake function * Improve location filtering - Don't limit the actual stock items - Instead, select only parts which exist within a given location tree * Update API version * String tweaks * Fix permissions for PartStocktake API * More unit testing for stocktake functionality * QoL fix * Fix for assigning inherited permissions
This commit is contained in:
45
InvenTree/templates/InvenTree/settings/part_stocktake.html
Normal file
45
InvenTree/templates/InvenTree/settings/part_stocktake.html
Normal file
@ -0,0 +1,45 @@
|
||||
{% extends "panel.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block label %}stocktake{% endblock %}
|
||||
|
||||
{% block heading %}
|
||||
{% trans "Stocktake Settings" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block panel_content %}
|
||||
|
||||
<div class='panel-content'>
|
||||
<table class='table table-striped table-condensed'>
|
||||
<tbody>
|
||||
{% include "InvenTree/settings/setting.html" with key="STOCKTAKE_ENABLE" icon="fa-clipboard-check" %}
|
||||
{% include "InvenTree/settings/setting.html" with key="STOCKTAKE_AUTO_DAYS" icon="fa-calendar-alt" %}
|
||||
{% include "InvenTree/settings/setting.html" with key="STOCKTAKE_DELETE_REPORT_DAYS" icon="fa-trash-alt" %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class='panel-heading'>
|
||||
<div class='d-flex flex-wrap'>
|
||||
<h4>{% trans "Stocktake Reports" %}</h4>
|
||||
{% include "spacer.html" %}
|
||||
<div class='btn-group' role='group'>
|
||||
{% if roles.stocktake.add %}
|
||||
<button type='button' id='btn-generate-stocktake' class='btn btn-primary float-right'>
|
||||
<span class='fas fa-clipboard-check'></span> {% trans "Stocktake" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class='panel-content'>
|
||||
<div id='part-stocktake-report-toolbar'>
|
||||
<div class='btn-group' role='group'>
|
||||
{% include "filter_list.html" with id="stocktakereport" %}
|
||||
</div>
|
||||
</div>
|
||||
<table class='table table-striped table-condensed' id='stocktake-report-table' data-toolbar='#part-stocktake-report-toolbar'></table>
|
||||
</div>
|
||||
|
||||
{% endblock panel_content %}
|
@ -36,8 +36,9 @@
|
||||
{% include "InvenTree/settings/label.html" %}
|
||||
{% include "InvenTree/settings/report.html" %}
|
||||
{% include "InvenTree/settings/part.html" %}
|
||||
{% include "InvenTree/settings/pricing.html" %}
|
||||
{% include "InvenTree/settings/part_stocktake.html" %}
|
||||
{% include "InvenTree/settings/category.html" %}
|
||||
{% include "InvenTree/settings/pricing.html" %}
|
||||
{% include "InvenTree/settings/stock.html" %}
|
||||
{% include "InvenTree/settings/build.html" %}
|
||||
{% include "InvenTree/settings/po.html" %}
|
||||
@ -62,426 +63,16 @@
|
||||
{% block js_ready %}
|
||||
{{ block.super }}
|
||||
|
||||
// Callback for when boolean settings are edited
|
||||
$('table').find('.boolean-setting').change(function() {
|
||||
{% include "InvenTree/settings/settings_js.html" %}
|
||||
|
||||
var pk = $(this).attr('pk');
|
||||
var setting = $(this).attr('setting');
|
||||
var plugin = $(this).attr('plugin');
|
||||
var user = $(this).attr('user');
|
||||
var notification = $(this).attr('notification');
|
||||
|
||||
var checked = this.checked;
|
||||
|
||||
// Global setting by default
|
||||
var url = `/api/settings/global/${setting}/`;
|
||||
|
||||
if (notification) {
|
||||
url = `/api/settings/notification/${pk}/`;
|
||||
} else if (plugin) {
|
||||
url = `/api/plugins/settings/${plugin}/${setting}/`;
|
||||
} else if (user) {
|
||||
url = `/api/settings/user/${setting}/`;
|
||||
}
|
||||
|
||||
inventreePut(
|
||||
url,
|
||||
{
|
||||
value: checked.toString(),
|
||||
},
|
||||
{
|
||||
method: 'PATCH',
|
||||
success: function(data) {
|
||||
},
|
||||
error: function(xhr) {
|
||||
showApiError(xhr, url);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
});
|
||||
|
||||
// Callback for when non-boolean settings are edited
|
||||
$('table').find('.btn-edit-setting').click(function() {
|
||||
var setting = $(this).attr('setting');
|
||||
var plugin = $(this).attr('plugin');
|
||||
var is_global = true;
|
||||
var notification = $(this).attr('notification');
|
||||
|
||||
if ($(this).attr('user')){
|
||||
is_global = false;
|
||||
}
|
||||
|
||||
var title = '';
|
||||
|
||||
if (plugin != null) {
|
||||
title = '{% trans "Edit Plugin Setting" %}';
|
||||
} else if (notification) {
|
||||
title = '{% trans "Edit Notification Setting" %}';
|
||||
setting = $(this).attr('pk');
|
||||
} else if (is_global) {
|
||||
title = '{% trans "Edit Global Setting" %}';
|
||||
} else {
|
||||
title = '{% trans "Edit User Setting" %}';
|
||||
}
|
||||
|
||||
editSetting(setting, {
|
||||
plugin: plugin,
|
||||
global: is_global,
|
||||
notification: notification,
|
||||
title: title,
|
||||
});
|
||||
});
|
||||
|
||||
$("#edit-user").on('click', function() {
|
||||
launchModalForm(
|
||||
"{% url 'edit-user' %}",
|
||||
{
|
||||
reload: true,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
$("#edit-password").on('click', function() {
|
||||
launchModalForm(
|
||||
"{% url 'set-password' %}",
|
||||
{
|
||||
reload: true,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
$('#btn-update-rates').click(function() {
|
||||
inventreePut(
|
||||
'{% url "api-currency-refresh" %}',
|
||||
{},
|
||||
{
|
||||
method: 'POST',
|
||||
success: function(data) {
|
||||
location.reload();
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
$('#exchange-rate-table').inventreeTable({
|
||||
url: '{% url "api-currency-exchange" %}',
|
||||
search: false,
|
||||
showColumns: false,
|
||||
sortable: true,
|
||||
sidePagination: 'client',
|
||||
onLoadSuccess: function(response) {
|
||||
var data = response.exchange_rates || {};
|
||||
|
||||
var rows = [];
|
||||
|
||||
for (var currency in data) {
|
||||
rows.push({
|
||||
'currency': currency,
|
||||
'rate': data[currency],
|
||||
});
|
||||
}
|
||||
|
||||
$('#exchange-rate-table').bootstrapTable('load', rows);
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
field: 'currency',
|
||||
sortable: true,
|
||||
title: '{% trans "Currency" %}',
|
||||
},
|
||||
{
|
||||
field: 'rate',
|
||||
sortable: true,
|
||||
title: '{% trans "Rate" %}',
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
$('#category-select').select2({
|
||||
placeholder: '',
|
||||
width: '100%',
|
||||
ajax: {
|
||||
url: '{% url "api-part-category-list" %}',
|
||||
dataType: 'json',
|
||||
delay: 250,
|
||||
cache: false,
|
||||
data: function(params) {
|
||||
if (!params.page) {
|
||||
offset = 0;
|
||||
} else {
|
||||
offset = (params.page - 1) * 25;
|
||||
}
|
||||
|
||||
return {
|
||||
search: params.term,
|
||||
offset: offset,
|
||||
limit: 25,
|
||||
};
|
||||
},
|
||||
processResults: function(response) {
|
||||
var data = [];
|
||||
|
||||
var more = false;
|
||||
|
||||
if ('count' in response && 'results' in response) {
|
||||
// Response is paginated
|
||||
data = response.results;
|
||||
|
||||
// Any more data available?
|
||||
if (response.next) {
|
||||
more = true;
|
||||
}
|
||||
|
||||
} else {
|
||||
// Non-paginated response
|
||||
data = response;
|
||||
}
|
||||
|
||||
// Each 'row' must have the 'id' attribute
|
||||
for (var idx = 0; idx < data.length; idx++) {
|
||||
data[idx].id = data[idx].pk;
|
||||
data[idx].text = data[idx].pathstring;
|
||||
}
|
||||
|
||||
// Ref: https://select2.org/data-sources/formats
|
||||
var results = {
|
||||
results: data,
|
||||
pagination: {
|
||||
more: more,
|
||||
}
|
||||
};
|
||||
|
||||
return results;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
$('#cat-param-table').inventreeTable({
|
||||
formatNoMatches: function() { return '{% trans "No category parameter templates found" %}'; },
|
||||
columns: [
|
||||
{
|
||||
field: 'pk',
|
||||
title: 'ID',
|
||||
visible: false,
|
||||
switchable: false,
|
||||
},
|
||||
{
|
||||
field: 'parameter_template_detail.name',
|
||||
title: '{% trans "Parameter Template" %}',
|
||||
sortable: 'true',
|
||||
},
|
||||
{
|
||||
field: 'category_detail.pathstring',
|
||||
title: '{% trans "Category" %}',
|
||||
},
|
||||
{
|
||||
field: 'default_value',
|
||||
title: '{% trans "Default Value" %}',
|
||||
sortable: 'true',
|
||||
formatter: function(value, row, index, field) {
|
||||
var bEdit = "<button title='{% trans "Edit Template" %}' class='template-edit btn btn-outline-secondary' type='button' pk='" + row.pk + "'><span class='fas fa-edit'></span></button>";
|
||||
var bDel = "<button title='{% trans "Delete Template" %}' class='template-delete btn btn-outline-secondary' type='button' pk='" + row.pk + "'><span class='fas fa-trash-alt icon-red'></span></button>";
|
||||
|
||||
var html = value
|
||||
html += "<div class='btn-group float-right' role='group'>" + bEdit + bDel + "</div>";
|
||||
|
||||
return html;
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
function loadTemplateTable(pk) {
|
||||
|
||||
var query = {};
|
||||
|
||||
if (pk) {
|
||||
query['category'] = pk;
|
||||
}
|
||||
|
||||
// Load the parameter table
|
||||
$("#cat-param-table").bootstrapTable('refresh', {
|
||||
query: query,
|
||||
url: '{% url "api-part-category-parameter-list" %}',
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Initially load table with *all* categories
|
||||
loadTemplateTable();
|
||||
|
||||
$('body').on('change', '#category-select', function() {
|
||||
var pk = $(this).val();
|
||||
loadTemplateTable(pk);
|
||||
});
|
||||
|
||||
$("#new-cat-param").click(function() {
|
||||
|
||||
var pk = $('#category-select').val();
|
||||
|
||||
constructForm('{% url "api-part-category-parameter-list" %}', {
|
||||
title: '{% trans "Create Category Parameter Template" %}',
|
||||
method: 'POST',
|
||||
fields: {
|
||||
parameter_template: {},
|
||||
category: {
|
||||
icon: 'fa-sitemap',
|
||||
value: pk,
|
||||
},
|
||||
default_value: {},
|
||||
},
|
||||
onSuccess: function() {
|
||||
loadTemplateTable(pk);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$("#cat-param-table").on('click', '.template-edit', function() {
|
||||
|
||||
var category = $('#category-select').val();
|
||||
var pk = $(this).attr('pk');
|
||||
|
||||
constructForm(`/api/part/category/parameters/${pk}/`, {
|
||||
fields: {
|
||||
parameter_template: {},
|
||||
category: {
|
||||
icon: 'fa-sitemap',
|
||||
},
|
||||
default_value: {},
|
||||
},
|
||||
onSuccess: function() {
|
||||
loadTemplateTable(pk);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
$("#cat-param-table").on('click', '.template-delete', function() {
|
||||
|
||||
var category = $('#category-select').val();
|
||||
var pk = $(this).attr('pk');
|
||||
|
||||
var url = `/part/category/${category}/parameters/${pk}/delete/`;
|
||||
|
||||
constructForm(`/api/part/category/parameters/${pk}/`, {
|
||||
method: 'DELETE',
|
||||
title: '{% trans "Delete Category Parameter Template" %}',
|
||||
onSuccess: function() {
|
||||
loadTemplateTable(pk);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$("#param-table").inventreeTable({
|
||||
url: "{% url 'api-part-parameter-template-list' %}",
|
||||
queryParams: {
|
||||
ordering: 'name',
|
||||
},
|
||||
formatNoMatches: function() { return '{% trans "No part parameter templates found" %}'; },
|
||||
columns: [
|
||||
{
|
||||
field: 'pk',
|
||||
title: '{% trans "ID" %}',
|
||||
visible: false,
|
||||
switchable: false,
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
title: '{% trans "Name" %}',
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
field: 'units',
|
||||
title: '{% trans "Units" %}',
|
||||
sortable: true,
|
||||
switchable: true,
|
||||
},
|
||||
{
|
||||
field: 'description',
|
||||
title: '{% trans "Description" %}',
|
||||
sortable: false,
|
||||
switchable: true,
|
||||
},
|
||||
{
|
||||
formatter: function(value, row, index, field) {
|
||||
var bEdit = "<button title='{% trans "Edit Template" %}' class='template-edit btn btn-outline-secondary' type='button' pk='" + row.pk + "'><span class='fas fa-edit icon-green'></span></button>";
|
||||
var bDel = "<button title='{% trans "Delete Template" %}' class='template-delete btn btn-outline-secondary' type='button' pk='" + row.pk + "'><span class='fas fa-trash-alt icon-red'></span></button>";
|
||||
|
||||
var html = "<div class='btn-group float-right' role='group'>" + bEdit + bDel + "</div>";
|
||||
|
||||
return html;
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
$("#new-param").click(function() {
|
||||
constructForm('{% url "api-part-parameter-template-list" %}', {
|
||||
fields: {
|
||||
name: {},
|
||||
units: {},
|
||||
description: {},
|
||||
},
|
||||
method: 'POST',
|
||||
title: '{% trans "Create Part Parameter Template" %}',
|
||||
onSuccess: function() {
|
||||
$("#param-table").bootstrapTable('refresh');
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
$("#param-table").on('click', '.template-edit', function() {
|
||||
var button = $(this);
|
||||
var pk = button.attr('pk');
|
||||
|
||||
constructForm(
|
||||
`/api/part/parameter/template/${pk}/`,
|
||||
{
|
||||
fields: {
|
||||
name: {},
|
||||
units: {},
|
||||
description: {},
|
||||
},
|
||||
title: '{% trans "Edit Part Parameter Template" %}',
|
||||
onSuccess: function() {
|
||||
$("#param-table").bootstrapTable('refresh');
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
$("#param-table").on('click', '.template-delete', function() {
|
||||
var button = $(this);
|
||||
var pk = button.attr('pk');
|
||||
|
||||
var html = `
|
||||
<div class='alert alert-block alert-danger'>
|
||||
{% trans "Any parameters which reference this template will also be deleted" %}
|
||||
</div>`;
|
||||
|
||||
constructForm(
|
||||
`/api/part/parameter/template/${pk}/`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
preFormContent: html,
|
||||
title: '{% trans "Delete Part Parameter Template" %}',
|
||||
onSuccess: function() {
|
||||
$("#param-table").bootstrapTable('refresh');
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
$("#import-part").click(function() {
|
||||
launchModalForm("{% url 'api-part-import' %}?reset", {});
|
||||
});
|
||||
|
||||
{% plugins_enabled as plug %}
|
||||
{% if plug %}
|
||||
$("#install-plugin").click(function() {
|
||||
installPlugin();
|
||||
});
|
||||
{% if user.is_staff %}
|
||||
{% include "InvenTree/settings/settings_staff_js.html" %}
|
||||
{% plugins_enabled as plug %}
|
||||
{% if plug %}
|
||||
$("#install-plugin").click(function() {
|
||||
installPlugin();
|
||||
});
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
enableSidebar('settings');
|
||||
|
92
InvenTree/templates/InvenTree/settings/settings_js.html
Normal file
92
InvenTree/templates/InvenTree/settings/settings_js.html
Normal file
@ -0,0 +1,92 @@
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
{% load inventree_extras %}
|
||||
|
||||
// Callback for when boolean settings are edited
|
||||
$('table').find('.boolean-setting').change(function() {
|
||||
|
||||
var pk = $(this).attr('pk');
|
||||
var setting = $(this).attr('setting');
|
||||
var plugin = $(this).attr('plugin');
|
||||
var user = $(this).attr('user');
|
||||
var notification = $(this).attr('notification');
|
||||
|
||||
var checked = this.checked;
|
||||
|
||||
// Global setting by default
|
||||
var url = `/api/settings/global/${setting}/`;
|
||||
|
||||
if (notification) {
|
||||
url = `/api/settings/notification/${pk}/`;
|
||||
} else if (plugin) {
|
||||
url = `/api/plugins/settings/${plugin}/${setting}/`;
|
||||
} else if (user) {
|
||||
url = `/api/settings/user/${setting}/`;
|
||||
}
|
||||
|
||||
inventreePut(
|
||||
url,
|
||||
{
|
||||
value: checked.toString(),
|
||||
},
|
||||
{
|
||||
method: 'PATCH',
|
||||
success: function(data) {
|
||||
},
|
||||
error: function(xhr) {
|
||||
showApiError(xhr, url);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
});
|
||||
|
||||
// Callback for when non-boolean settings are edited
|
||||
$('table').find('.btn-edit-setting').click(function() {
|
||||
var setting = $(this).attr('setting');
|
||||
var plugin = $(this).attr('plugin');
|
||||
var is_global = true;
|
||||
var notification = $(this).attr('notification');
|
||||
|
||||
if ($(this).attr('user')){
|
||||
is_global = false;
|
||||
}
|
||||
|
||||
var title = '';
|
||||
|
||||
if (plugin != null) {
|
||||
title = '{% trans "Edit Plugin Setting" %}';
|
||||
} else if (notification) {
|
||||
title = '{% trans "Edit Notification Setting" %}';
|
||||
setting = $(this).attr('pk');
|
||||
} else if (is_global) {
|
||||
title = '{% trans "Edit Global Setting" %}';
|
||||
} else {
|
||||
title = '{% trans "Edit User Setting" %}';
|
||||
}
|
||||
|
||||
editSetting(setting, {
|
||||
plugin: plugin,
|
||||
global: is_global,
|
||||
notification: notification,
|
||||
title: title,
|
||||
});
|
||||
});
|
||||
|
||||
$("#edit-user").on('click', function() {
|
||||
launchModalForm(
|
||||
"{% url 'edit-user' %}",
|
||||
{
|
||||
reload: true,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
$("#edit-password").on('click', function() {
|
||||
launchModalForm(
|
||||
"{% url 'set-password' %}",
|
||||
{
|
||||
reload: true,
|
||||
}
|
||||
);
|
||||
});
|
401
InvenTree/templates/InvenTree/settings/settings_staff_js.html
Normal file
401
InvenTree/templates/InvenTree/settings/settings_staff_js.html
Normal file
@ -0,0 +1,401 @@
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
{% load inventree_extras %}
|
||||
|
||||
// Javascript for Pricing panel
|
||||
onPanelLoad('pricing', function() {
|
||||
$('#btn-update-rates').click(function() {
|
||||
inventreePut(
|
||||
'{% url "api-currency-refresh" %}',
|
||||
{},
|
||||
{
|
||||
method: 'POST',
|
||||
success: function(data) {
|
||||
location.reload();
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
$('#exchange-rate-table').inventreeTable({
|
||||
url: '{% url "api-currency-exchange" %}',
|
||||
search: false,
|
||||
showColumns: false,
|
||||
sortable: true,
|
||||
sidePagination: 'client',
|
||||
onLoadSuccess: function(response) {
|
||||
var data = response.exchange_rates || {};
|
||||
|
||||
var rows = [];
|
||||
|
||||
for (var currency in data) {
|
||||
rows.push({
|
||||
'currency': currency,
|
||||
'rate': data[currency],
|
||||
});
|
||||
}
|
||||
|
||||
$('#exchange-rate-table').bootstrapTable('load', rows);
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
field: 'currency',
|
||||
sortable: true,
|
||||
title: '{% trans "Currency" %}',
|
||||
},
|
||||
{
|
||||
field: 'rate',
|
||||
sortable: true,
|
||||
title: '{% trans "Rate" %}',
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
// Javascript for Part Category panel
|
||||
onPanelLoad('category', function() {
|
||||
$('#category-select').select2({
|
||||
placeholder: '',
|
||||
width: '100%',
|
||||
ajax: {
|
||||
url: '{% url "api-part-category-list" %}',
|
||||
dataType: 'json',
|
||||
delay: 250,
|
||||
cache: false,
|
||||
data: function(params) {
|
||||
if (!params.page) {
|
||||
offset = 0;
|
||||
} else {
|
||||
offset = (params.page - 1) * 25;
|
||||
}
|
||||
|
||||
return {
|
||||
search: params.term,
|
||||
offset: offset,
|
||||
limit: 25,
|
||||
};
|
||||
},
|
||||
processResults: function(response) {
|
||||
var data = [];
|
||||
|
||||
var more = false;
|
||||
|
||||
if ('count' in response && 'results' in response) {
|
||||
// Response is paginated
|
||||
data = response.results;
|
||||
|
||||
// Any more data available?
|
||||
if (response.next) {
|
||||
more = true;
|
||||
}
|
||||
|
||||
} else {
|
||||
// Non-paginated response
|
||||
data = response;
|
||||
}
|
||||
|
||||
// Each 'row' must have the 'id' attribute
|
||||
for (var idx = 0; idx < data.length; idx++) {
|
||||
data[idx].id = data[idx].pk;
|
||||
data[idx].text = data[idx].pathstring;
|
||||
}
|
||||
|
||||
// Ref: https://select2.org/data-sources/formats
|
||||
var results = {
|
||||
results: data,
|
||||
pagination: {
|
||||
more: more,
|
||||
}
|
||||
};
|
||||
|
||||
return results;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
$('#cat-param-table').inventreeTable({
|
||||
formatNoMatches: function() { return '{% trans "No category parameter templates found" %}'; },
|
||||
columns: [
|
||||
{
|
||||
field: 'pk',
|
||||
title: 'ID',
|
||||
visible: false,
|
||||
switchable: false,
|
||||
},
|
||||
{
|
||||
field: 'parameter_template_detail.name',
|
||||
title: '{% trans "Parameter Template" %}',
|
||||
sortable: 'true',
|
||||
},
|
||||
{
|
||||
field: 'category_detail.pathstring',
|
||||
title: '{% trans "Category" %}',
|
||||
},
|
||||
{
|
||||
field: 'default_value',
|
||||
title: '{% trans "Default Value" %}',
|
||||
sortable: 'true',
|
||||
formatter: function(value, row, index, field) {
|
||||
var bEdit = "<button title='{% trans "Edit Template" %}' class='template-edit btn btn-outline-secondary' type='button' pk='" + row.pk + "'><span class='fas fa-edit'></span></button>";
|
||||
var bDel = "<button title='{% trans "Delete Template" %}' class='template-delete btn btn-outline-secondary' type='button' pk='" + row.pk + "'><span class='fas fa-trash-alt icon-red'></span></button>";
|
||||
|
||||
var html = value
|
||||
html += "<div class='btn-group float-right' role='group'>" + bEdit + bDel + "</div>";
|
||||
|
||||
return html;
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
$("#cat-param-table").on('click', '.template-edit', function() {
|
||||
|
||||
var category = $('#category-select').val();
|
||||
var pk = $(this).attr('pk');
|
||||
|
||||
constructForm(`/api/part/category/parameters/${pk}/`, {
|
||||
fields: {
|
||||
parameter_template: {},
|
||||
category: {
|
||||
icon: 'fa-sitemap',
|
||||
},
|
||||
default_value: {},
|
||||
},
|
||||
onSuccess: function() {
|
||||
loadTemplateTable(pk);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$("#cat-param-table").on('click', '.template-delete', function() {
|
||||
|
||||
var category = $('#category-select').val();
|
||||
var pk = $(this).attr('pk');
|
||||
|
||||
var url = `/part/category/${category}/parameters/${pk}/delete/`;
|
||||
|
||||
constructForm(`/api/part/category/parameters/${pk}/`, {
|
||||
method: 'DELETE',
|
||||
title: '{% trans "Delete Category Parameter Template" %}',
|
||||
onSuccess: function() {
|
||||
loadTemplateTable(pk);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function loadTemplateTable(pk) {
|
||||
|
||||
var query = {};
|
||||
|
||||
if (pk) {
|
||||
query['category'] = pk;
|
||||
}
|
||||
|
||||
// Load the parameter table
|
||||
$("#cat-param-table").bootstrapTable('refresh', {
|
||||
query: query,
|
||||
url: '{% url "api-part-category-parameter-list" %}',
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Initially load table with *all* categories
|
||||
loadTemplateTable();
|
||||
|
||||
$('body').on('change', '#category-select', function() {
|
||||
var pk = $(this).val();
|
||||
loadTemplateTable(pk);
|
||||
});
|
||||
|
||||
$("#new-cat-param").click(function() {
|
||||
|
||||
var pk = $('#category-select').val();
|
||||
|
||||
constructForm('{% url "api-part-category-parameter-list" %}', {
|
||||
title: '{% trans "Create Category Parameter Template" %}',
|
||||
method: 'POST',
|
||||
fields: {
|
||||
parameter_template: {},
|
||||
category: {
|
||||
icon: 'fa-sitemap',
|
||||
value: pk,
|
||||
},
|
||||
default_value: {},
|
||||
},
|
||||
onSuccess: function() {
|
||||
loadTemplateTable(pk);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// Javascript for the Part settings panel
|
||||
onPanelLoad('parts', function() {
|
||||
$("#param-table").inventreeTable({
|
||||
url: "{% url 'api-part-parameter-template-list' %}",
|
||||
queryParams: {
|
||||
ordering: 'name',
|
||||
},
|
||||
formatNoMatches: function() { return '{% trans "No part parameter templates found" %}'; },
|
||||
columns: [
|
||||
{
|
||||
field: 'pk',
|
||||
title: '{% trans "ID" %}',
|
||||
visible: false,
|
||||
switchable: false,
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
title: '{% trans "Name" %}',
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
field: 'units',
|
||||
title: '{% trans "Units" %}',
|
||||
sortable: true,
|
||||
switchable: true,
|
||||
},
|
||||
{
|
||||
field: 'description',
|
||||
title: '{% trans "Description" %}',
|
||||
sortable: false,
|
||||
switchable: true,
|
||||
},
|
||||
{
|
||||
formatter: function(value, row, index, field) {
|
||||
var bEdit = "<button title='{% trans "Edit Template" %}' class='template-edit btn btn-outline-secondary' type='button' pk='" + row.pk + "'><span class='fas fa-edit icon-green'></span></button>";
|
||||
var bDel = "<button title='{% trans "Delete Template" %}' class='template-delete btn btn-outline-secondary' type='button' pk='" + row.pk + "'><span class='fas fa-trash-alt icon-red'></span></button>";
|
||||
|
||||
var html = "<div class='btn-group float-right' role='group'>" + bEdit + bDel + "</div>";
|
||||
|
||||
return html;
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
$("#new-param").click(function() {
|
||||
constructForm('{% url "api-part-parameter-template-list" %}', {
|
||||
fields: {
|
||||
name: {},
|
||||
units: {},
|
||||
description: {},
|
||||
},
|
||||
method: 'POST',
|
||||
title: '{% trans "Create Part Parameter Template" %}',
|
||||
onSuccess: function() {
|
||||
$("#param-table").bootstrapTable('refresh');
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
$("#param-table").on('click', '.template-edit', function() {
|
||||
var button = $(this);
|
||||
var pk = button.attr('pk');
|
||||
|
||||
constructForm(
|
||||
`/api/part/parameter/template/${pk}/`,
|
||||
{
|
||||
fields: {
|
||||
name: {},
|
||||
units: {},
|
||||
description: {},
|
||||
},
|
||||
title: '{% trans "Edit Part Parameter Template" %}',
|
||||
onSuccess: function() {
|
||||
$("#param-table").bootstrapTable('refresh');
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
$("#param-table").on('click', '.template-delete', function() {
|
||||
var button = $(this);
|
||||
var pk = button.attr('pk');
|
||||
|
||||
var html = `
|
||||
<div class='alert alert-block alert-danger'>
|
||||
{% trans "Any parameters which reference this template will also be deleted" %}
|
||||
</div>`;
|
||||
|
||||
constructForm(
|
||||
`/api/part/parameter/template/${pk}/`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
preFormContent: html,
|
||||
title: '{% trans "Delete Part Parameter Template" %}',
|
||||
onSuccess: function() {
|
||||
$("#param-table").bootstrapTable('refresh');
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
$("#import-part").click(function() {
|
||||
launchModalForm("{% url 'api-part-import' %}?reset", {});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// Javascript for the Stocktake settings panel
|
||||
onPanelLoad('stocktake', function() {
|
||||
|
||||
{% if roles.stocktake.view %}
|
||||
var table = '#stocktake-report-table';
|
||||
|
||||
var filters = loadTableFilters('stocktakereport');
|
||||
setupFilterList('stocktakereport', $(table), '#filter-list-stocktakereport');
|
||||
|
||||
$(table).inventreeTable({
|
||||
url: '{% url "api-part-stocktake-report-list" %}',
|
||||
search: false,
|
||||
queryParams: filters,
|
||||
name: 'stocktakereport',
|
||||
showColumns: false,
|
||||
sidePagination: 'server',
|
||||
sortable: true,
|
||||
sortName: 'date',
|
||||
sortOrder: 'desc',
|
||||
columns: [
|
||||
{
|
||||
field: 'report',
|
||||
title: '{% trans "Report" %}',
|
||||
formatter: function(value, row) {
|
||||
return attachmentLink(value);
|
||||
}
|
||||
},
|
||||
{
|
||||
field: 'part_count',
|
||||
title: '{% trans "Part Count" %}',
|
||||
},
|
||||
{
|
||||
field: 'date',
|
||||
title: '{% trans "Date" %}',
|
||||
sortable: true,
|
||||
formatter: function(value, row) {
|
||||
let html = renderDate(value);
|
||||
|
||||
if (row.user_detail) {
|
||||
html += `<span class='badge bg-dark rounded-pill float-right'>${row.user_detail.username}</span>`;
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
||||
},
|
||||
]
|
||||
});
|
||||
{% endif %}
|
||||
|
||||
{% if roles.stocktake.add %}
|
||||
$('#btn-generate-stocktake').click(function() {
|
||||
generateStocktakeReport({
|
||||
part: {},
|
||||
category: {},
|
||||
location: {},
|
||||
generate_report: {},
|
||||
update_parts: {},
|
||||
});
|
||||
});
|
||||
{% endif %}
|
||||
});
|
@ -40,12 +40,14 @@
|
||||
{% include "sidebar_item.html" with label='labels' text=text icon='fa-tag' %}
|
||||
{% trans "Reporting" as text %}
|
||||
{% include "sidebar_item.html" with label='reporting' text=text icon="fa-file-pdf" %}
|
||||
{% trans "Parts" as text %}
|
||||
{% include "sidebar_item.html" with label='parts' text=text icon="fa-shapes" %}
|
||||
{% trans "Categories" as text %}
|
||||
{% include "sidebar_item.html" with label='category' text=text icon="fa-sitemap" %}
|
||||
{% trans "Parts" as text %}
|
||||
{% include "sidebar_item.html" with label='parts' text=text icon="fa-shapes" %}
|
||||
{% trans "Stock" as text %}
|
||||
{% include "sidebar_item.html" with label='stock' text=text icon="fa-boxes" %}
|
||||
{% trans "Stocktake" as text %}
|
||||
{% include "sidebar_item.html" with label='stocktake' text=text icon="fa-clipboard-check" %}
|
||||
{% trans "Build Orders" as text %}
|
||||
{% include "sidebar_item.html" with label='build-order' text=text icon="fa-tools" %}
|
||||
{% trans "Purchase Orders" as text %}
|
||||
|
@ -21,6 +21,7 @@
|
||||
{% include "InvenTree/settings/setting.html" with key="STOCK_ALLOW_EXPIRED_BUILD" icon="fa-tools" %}
|
||||
{% include "InvenTree/settings/setting.html" with key="STOCK_OWNERSHIP_CONTROL" icon="fa-users" %}
|
||||
{% include "InvenTree/settings/setting.html" with key="STOCK_LOCATION_DEFAULT_ICON" icon="fa-icons" %}
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
{% endblock %}
|
||||
|
@ -6,6 +6,7 @@
|
||||
*/
|
||||
|
||||
/* exported
|
||||
attachmentLink,
|
||||
addAttachmentButtonCallbacks,
|
||||
loadAttachmentTable,
|
||||
reloadAttachmentTable,
|
||||
@ -130,6 +131,50 @@ function reloadAttachmentTable() {
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Render a link (with icon) to an internal attachment (file)
|
||||
*/
|
||||
function attachmentLink(filename) {
|
||||
|
||||
if (!filename) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Default file icon (if no better choice is found)
|
||||
let icon = 'fa-file-alt';
|
||||
let fn = filename.toLowerCase();
|
||||
|
||||
// Look for some "known" file types
|
||||
if (fn.endsWith('.csv')) {
|
||||
icon = 'fa-file-csv';
|
||||
} else if (fn.endsWith('.pdf')) {
|
||||
icon = 'fa-file-pdf';
|
||||
} else if (fn.endsWith('.xls') || fn.endsWith('.xlsx')) {
|
||||
icon = 'fa-file-excel';
|
||||
} else if (fn.endsWith('.doc') || fn.endsWith('.docx')) {
|
||||
icon = 'fa-file-word';
|
||||
} else if (fn.endsWith('.zip') || fn.endsWith('.7z')) {
|
||||
icon = 'fa-file-archive';
|
||||
} else {
|
||||
let images = ['.png', '.jpg', '.bmp', '.gif', '.svg', '.tif'];
|
||||
|
||||
images.forEach(function(suffix) {
|
||||
if (fn.endsWith(suffix)) {
|
||||
icon = 'fa-file-image';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let split = filename.split('/');
|
||||
fn = split[split.length - 1];
|
||||
|
||||
let html = `<span class='fas ${icon}'></span> ${fn}`;
|
||||
|
||||
return renderLink(html, filename, {download: true});
|
||||
|
||||
}
|
||||
|
||||
|
||||
/* Load a table of attachments against a specific model.
|
||||
* Note that this is a 'generic' table which is used for multiple attachment model classes
|
||||
*/
|
||||
@ -242,36 +287,7 @@ function loadAttachmentTable(url, options) {
|
||||
formatter: function(value, row) {
|
||||
|
||||
if (row.attachment) {
|
||||
var icon = 'fa-file-alt';
|
||||
|
||||
var fn = value.toLowerCase();
|
||||
|
||||
if (fn.endsWith('.csv')) {
|
||||
icon = 'fa-file-csv';
|
||||
} else if (fn.endsWith('.pdf')) {
|
||||
icon = 'fa-file-pdf';
|
||||
} else if (fn.endsWith('.xls') || fn.endsWith('.xlsx')) {
|
||||
icon = 'fa-file-excel';
|
||||
} else if (fn.endsWith('.doc') || fn.endsWith('.docx')) {
|
||||
icon = 'fa-file-word';
|
||||
} else if (fn.endsWith('.zip') || fn.endsWith('.7z')) {
|
||||
icon = 'fa-file-archive';
|
||||
} else {
|
||||
var images = ['.png', '.jpg', '.bmp', '.gif', '.svg', '.tif'];
|
||||
|
||||
images.forEach(function(suffix) {
|
||||
if (fn.endsWith(suffix)) {
|
||||
icon = 'fa-file-image';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var split = value.split('/');
|
||||
var filename = split[split.length - 1];
|
||||
|
||||
var html = `<span class='fas ${icon}'></span> ${filename}`;
|
||||
|
||||
return renderLink(html, value, {download: true});
|
||||
return attachmentLink(row.attachment);
|
||||
} else if (row.link) {
|
||||
var html = `<span class='fas fa-link'></span> ${row.link}`;
|
||||
return renderLink(html, row.link);
|
||||
|
@ -974,6 +974,10 @@ function updateFieldValue(name, value, field, options) {
|
||||
}
|
||||
|
||||
switch (field.type) {
|
||||
case 'decimal':
|
||||
// Strip trailing zeros
|
||||
el.val(formatDecimal(value));
|
||||
break;
|
||||
case 'boolean':
|
||||
if (value == true || value.toString().toLowerCase() == 'true') {
|
||||
el.prop('checked');
|
||||
|
@ -274,6 +274,10 @@ function renderLink(text, url, options={}) {
|
||||
extras += ` title="${url}"`;
|
||||
}
|
||||
|
||||
if (options.download) {
|
||||
extras += ` download`;
|
||||
}
|
||||
|
||||
return `<a href="${url}" ${extras}>${text}</a>`;
|
||||
}
|
||||
|
||||
|
@ -50,25 +50,16 @@ function loadNotificationTable(table, options={}, enableDelete=false) {
|
||||
title: '{% trans "Category" %}',
|
||||
sortable: 'true',
|
||||
},
|
||||
{
|
||||
field: 'target',
|
||||
title: '{% trans "Item" %}',
|
||||
sortable: 'true',
|
||||
formatter: function(value, row, index, field) {
|
||||
if (value == null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
var html = `${value.model}: ${value.name}`;
|
||||
if (value.link ) {
|
||||
html = `<a href='${value.link}'>${html}</a>`;
|
||||
}
|
||||
return html;
|
||||
}
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
title: '{% trans "Name" %}',
|
||||
title: '{% trans "Notification" %}',
|
||||
formatter: function(value, row) {
|
||||
if (row.target && row.target.link) {
|
||||
return renderLink(value, row.target.link);
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
field: 'message',
|
||||
|
@ -27,6 +27,7 @@
|
||||
duplicatePart,
|
||||
editCategory,
|
||||
editPart,
|
||||
generateStocktakeReport,
|
||||
loadParametricPartTable,
|
||||
loadPartCategoryTable,
|
||||
loadPartParameterTable,
|
||||
@ -40,7 +41,6 @@
|
||||
loadSimplePartTable,
|
||||
partDetail,
|
||||
partStockLabel,
|
||||
performStocktake,
|
||||
toggleStar,
|
||||
validateBom,
|
||||
*/
|
||||
@ -702,133 +702,178 @@ function partDetail(part, options={}) {
|
||||
|
||||
|
||||
/*
|
||||
* Guide user through "stocktake" process
|
||||
* Initiate generation of a stocktake report
|
||||
*/
|
||||
function performStocktake(partId, options={}) {
|
||||
function generateStocktakeReport(options={}) {
|
||||
|
||||
var part_quantity = 0;
|
||||
let fields = {
|
||||
};
|
||||
|
||||
var date_threshold = moment().subtract(30, 'days');
|
||||
|
||||
// Helper function for formatting a StockItem row
|
||||
function buildStockItemRow(item) {
|
||||
|
||||
var pk = item.pk;
|
||||
|
||||
// Part detail
|
||||
var part = partDetail(item.part_detail, {
|
||||
thumb: true,
|
||||
});
|
||||
|
||||
// Location detail
|
||||
var location = locationDetail(item);
|
||||
|
||||
// Quantity detail
|
||||
var quantity = item.quantity;
|
||||
|
||||
part_quantity += item.quantity;
|
||||
|
||||
if (item.serial && item.quantity == 1) {
|
||||
quantity = `{% trans "Serial" %}: ${item.serial}`;
|
||||
}
|
||||
|
||||
quantity += stockStatusDisplay(item.status, {classes: 'float-right'});
|
||||
|
||||
// Last update
|
||||
var updated = item.stocktake_date || item.updated;
|
||||
|
||||
var update_rendered = renderDate(updated);
|
||||
|
||||
if (updated) {
|
||||
if (moment(updated) < date_threshold) {
|
||||
update_rendered += `<div class='float-right' title='{% trans "Stock item has not been checked recently" %}'><span class='fas fa-calendar-alt icon-red'></span></div>`;
|
||||
}
|
||||
}
|
||||
|
||||
// Actions
|
||||
var actions = `<div class='btn-group float-right' role='group'>`;
|
||||
|
||||
// TODO: Future work
|
||||
// actions += makeIconButton('fa-check-circle icon-green', 'button-line-count', pk, '{% trans "Update item" %}');
|
||||
// actions += makeIconButton('fa-trash-alt icon-red', 'button-line-delete', pk, '{% trans "Delete item" %}');
|
||||
|
||||
actions += `</div>`;
|
||||
|
||||
return `
|
||||
<tr>
|
||||
<td id='part-${pk}'>${part}</td>
|
||||
<td id='loc-${pk}'>${location}</td>
|
||||
<td id='quantity-${pk}'>${quantity}</td>
|
||||
<td id='updated-${pk}'>${update_rendered}</td>
|
||||
<td id='actions-${pk}'>${actions}</td>
|
||||
</tr>`;
|
||||
if (options.part != null) {
|
||||
fields.part = options.part;
|
||||
}
|
||||
|
||||
// First, load stock information for the part
|
||||
inventreeGet(
|
||||
'{% url "api-stock-list" %}',
|
||||
if (options.category != null) {
|
||||
fields.category = options.category;
|
||||
}
|
||||
|
||||
if (options.location != null) {
|
||||
fields.location = options.location;
|
||||
}
|
||||
|
||||
if (options.generate_report) {
|
||||
fields.generate_report = options.generate_report;
|
||||
}
|
||||
|
||||
if (options.update_parts) {
|
||||
fields.update_parts = options.update_parts;
|
||||
}
|
||||
|
||||
let content = `
|
||||
<div class='alert alert-block alert-info'>
|
||||
{% trans "Schedule generation of a new stocktake report." %} {% trans "Once complete, the stocktake report will be available for download." %}
|
||||
</div>
|
||||
`;
|
||||
|
||||
constructForm(
|
||||
'{% url "api-part-stocktake-report-generate" %}',
|
||||
{
|
||||
part: partId,
|
||||
in_stock: true,
|
||||
location_detail: true,
|
||||
part_detail: true,
|
||||
include_variants: true,
|
||||
ordering: '-stock',
|
||||
},
|
||||
{
|
||||
success: function(response) {
|
||||
var html = '';
|
||||
|
||||
html += `
|
||||
<table class='table table-striped table-condensed'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Stock Item" %}</th>
|
||||
<th>{% trans "Location" %}</th>
|
||||
<th>{% trans "Quantity" %}</th>
|
||||
<th>{% trans "Updated" %}</th>
|
||||
<th><!-- Actions --></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
`;
|
||||
|
||||
response.forEach(function(item) {
|
||||
html += buildStockItemRow(item);
|
||||
});
|
||||
|
||||
html += `</tbody></table>`;
|
||||
|
||||
constructForm(`/api/part/stocktake/`, {
|
||||
preFormContent: html,
|
||||
method: 'POST',
|
||||
title: '{% trans "Part Stocktake" %}',
|
||||
confirm: true,
|
||||
fields: {
|
||||
part: {
|
||||
value: partId,
|
||||
hidden: true,
|
||||
},
|
||||
quantity: {
|
||||
value: part_quantity,
|
||||
},
|
||||
note: {},
|
||||
},
|
||||
onSuccess: function(response) {
|
||||
handleFormSuccess(response, options);
|
||||
}
|
||||
method: 'POST',
|
||||
title: '{% trans "Generate Stocktake Report" %}',
|
||||
preFormContent: content,
|
||||
fields: fields,
|
||||
onSuccess: function(response) {
|
||||
showMessage('{% trans "Stocktake report scheduled" %}', {
|
||||
style: 'success',
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
var stocktakeChart = null;
|
||||
|
||||
/*
|
||||
* Load chart to display part stocktake information
|
||||
*/
|
||||
function loadStocktakeChart(data, options={}) {
|
||||
|
||||
var chart = 'part-stocktake-chart';
|
||||
var context = document.getElementById(chart);
|
||||
|
||||
var quantity_data = [];
|
||||
var cost_min_data = [];
|
||||
var cost_max_data = [];
|
||||
|
||||
var base_currency = baseCurrency();
|
||||
var rate_data = getCurrencyConversionRates();
|
||||
|
||||
data.forEach(function(row) {
|
||||
var date = moment(row.date);
|
||||
quantity_data.push({
|
||||
x: date,
|
||||
y: row.quantity
|
||||
});
|
||||
|
||||
if (row.cost_min) {
|
||||
cost_min_data.push({
|
||||
x: date,
|
||||
y: convertCurrency(
|
||||
row.cost_min,
|
||||
row.cost_min_currency || base_currency,
|
||||
base_currency,
|
||||
rate_data
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
if (row.cost_max) {
|
||||
cost_max_data.push({
|
||||
x: date,
|
||||
y: convertCurrency(
|
||||
row.cost_max,
|
||||
row.cost_max_currency || base_currency,
|
||||
base_currency,
|
||||
rate_data
|
||||
),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
var chart_data = {
|
||||
datasets: [
|
||||
{
|
||||
label: '{% trans "Quantity" %}',
|
||||
data: quantity_data,
|
||||
backgroundColor: 'rgba(160, 80, 220, 0.75)',
|
||||
borderWidth: 3,
|
||||
borderColor: 'rgb(160, 80, 220)',
|
||||
yAxisID: 'y',
|
||||
},
|
||||
{
|
||||
label: '{% trans "Minimum Cost" %}',
|
||||
data: cost_min_data,
|
||||
backgroundColor: 'rgba(220, 160, 80, 0.25)',
|
||||
borderWidth: 2,
|
||||
borderColor: 'rgba(220, 160, 80, 0.35)',
|
||||
borderDash: [10, 5],
|
||||
yAxisID: 'y1',
|
||||
fill: '+1',
|
||||
},
|
||||
{
|
||||
label: '{% trans "Maximum Cost" %}',
|
||||
data: cost_max_data,
|
||||
backgroundColor: 'rgba(220, 160, 80, 0.25)',
|
||||
borderWidth: 2,
|
||||
borderColor: 'rgba(220, 160, 80, 0.35)',
|
||||
borderDash: [10, 5],
|
||||
yAxisID: 'y1',
|
||||
fill: '-1',
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
if (stocktakeChart != null) {
|
||||
stocktakeChart.destroy();
|
||||
}
|
||||
|
||||
stocktakeChart = new Chart(context, {
|
||||
type: 'scatter',
|
||||
data: chart_data,
|
||||
options: {
|
||||
showLine: true,
|
||||
scales: {
|
||||
x: {
|
||||
type: 'time',
|
||||
// suggestedMax: today.format(),
|
||||
position: 'bottom',
|
||||
time: {
|
||||
minUnit: 'day',
|
||||
}
|
||||
},
|
||||
y: {
|
||||
type: 'linear',
|
||||
display: true,
|
||||
position: 'left',
|
||||
},
|
||||
y1: {
|
||||
type: 'linear',
|
||||
display: true,
|
||||
position: 'right',
|
||||
}
|
||||
},
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*
|
||||
* Load table for part stocktake information
|
||||
*/
|
||||
function loadPartStocktakeTable(partId, options={}) {
|
||||
|
||||
// HTML elements
|
||||
var table = options.table || '#part-stocktake-table';
|
||||
|
||||
var params = options.params || {};
|
||||
@ -853,13 +898,32 @@ function loadPartStocktakeTable(partId, options={}) {
|
||||
formatNoMatches: function() {
|
||||
return '{% trans "No stocktake information available" %}';
|
||||
},
|
||||
onLoadSuccess: function(response) {
|
||||
var data = response.results || response;
|
||||
|
||||
loadStocktakeChart(data);
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
field: 'item_count',
|
||||
title: '{% trans "Stock Items" %}',
|
||||
switchable: true,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
field: 'quantity',
|
||||
title: '{% trans "Quantity" %}',
|
||||
title: '{% trans "Total Quantity" %}',
|
||||
switchable: false,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
field: 'cost',
|
||||
title: '{% trans "Total Cost" %}',
|
||||
switchable: false,
|
||||
formatter: function(value, row) {
|
||||
return formatPriceRange(row.cost_min, row.cost_max);
|
||||
}
|
||||
},
|
||||
{
|
||||
field: 'note',
|
||||
title: '{% trans "Notes" %}',
|
||||
@ -883,7 +947,7 @@ function loadPartStocktakeTable(partId, options={}) {
|
||||
{
|
||||
field: 'actions',
|
||||
title: '',
|
||||
visible: options.admin,
|
||||
visible: options.allow_edit || options.allow_delete,
|
||||
switchable: false,
|
||||
sortable: false,
|
||||
formatter: function(value, row) {
|
||||
@ -910,7 +974,12 @@ function loadPartStocktakeTable(partId, options={}) {
|
||||
|
||||
constructForm(`/api/part/stocktake/${pk}/`, {
|
||||
fields: {
|
||||
item_count: {},
|
||||
quantity: {},
|
||||
cost_min: {},
|
||||
cost_min_currency: {},
|
||||
cost_max: {},
|
||||
cost_max_currency: {},
|
||||
note: {},
|
||||
},
|
||||
title: '{% trans "Edit Stocktake Entry" %}',
|
||||
|
@ -205,6 +205,11 @@ function calculateTotalPrice(dataset, value_func, currency_func, options={}) {
|
||||
total += value;
|
||||
}
|
||||
|
||||
// Return raw total instead of formatted value
|
||||
if (options.raw) {
|
||||
return total;
|
||||
}
|
||||
|
||||
return formatCurrency(total, {
|
||||
currency: currency,
|
||||
});
|
||||
|
Reference in New Issue
Block a user