mirror of
https://github.com/inventree/InvenTree.git
synced 2025-06-18 13:05:42 +00:00
Part stocktake (#4069)
* Remove stat context variables
* Revert "Remove stat context variables"
This reverts commit 0989c308d0
.
* Allow longer timeout for image download tests
* Create PartStocktake model
Model for representing stocktake entries against any given part
* Admin interface support for new model
* Adds API endpoint for listing stocktake information
* Bump API version
* Enable filtering and ordering for API endpoint
* Add model to permission group
* Add UI hooks for displaying stocktake data for a particular part
* Fix encoded type for 'quantity' field
* Load stocktake table for part
* Add "stocktake" button
* Add "note" field for stocktake
* Add user information when performing stocktake
* First pass at UI elements for performing stocktake
* Add user information to stocktake table
* Auto-calculate quantity based on available stock items
* add stocktake data as tabular inline (admin)
* js linting
* Add indication that a stock item has not been updated recently
* Display last stocktake information on part page
* js style fix
* Test fix for ongoing CI issues
* Add configurable option for controlling default "delete_on_deplete" behaviour
* Add id values to cells
* Hide action buttons (at least for now)
* Adds refresh button to table
* Add API endpoint to delete or edit stocktake entries
* Adds unit test for API list endpoint
* javascript linting
* More unit testing
* Add Part API filter for stocktake
* Add 'last_stocktake' field to Part model
- Gets filled out automatically when a new PartStocktake instance is created
* Update part table to include last_stocktake date
* Add simple unit test
* Fix test
This commit is contained in:
@ -19,6 +19,7 @@
|
||||
{% 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="DISPLAY_SCHEDULE_TAB" icon="fa-calendar-alt" user_setting=True %}
|
||||
{% include "InvenTree/settings/setting.html" with key="DISPLAY_STOCKTAKE_TAB" icon="fa-clipboard-check" user_setting=True %}
|
||||
{% include "InvenTree/settings/setting.html" with key="TABLE_STRING_MAX_LENGTH" icon="fa-table" user_setting=True %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
@ -292,7 +292,7 @@ function loadAttachmentTable(url, options) {
|
||||
var html = renderDate(value);
|
||||
|
||||
if (row.user_detail) {
|
||||
html += `<span class='badge bg-dark rounded-pill float-right'>${row.user_detail.username}</div>`;
|
||||
html += `<span class='badge bg-dark rounded-pill float-right'>${row.user_detail.username}</span>`;
|
||||
}
|
||||
|
||||
return html;
|
||||
|
@ -343,6 +343,9 @@ function constructForm(url, options) {
|
||||
// Request OPTIONS endpoint from the API
|
||||
getApiEndpointOptions(url, function(OPTIONS) {
|
||||
|
||||
// Copy across entire actions struct
|
||||
options.actions = OPTIONS.actions.POST || OPTIONS.actions.PUT || OPTIONS.actions.PATCH || OPTIONS.actions.DELETE || {};
|
||||
|
||||
// Extract any custom 'context' information from the OPTIONS data
|
||||
options.context = OPTIONS.context || {};
|
||||
|
||||
|
@ -33,10 +33,13 @@
|
||||
loadPartTable,
|
||||
loadPartTestTemplateTable,
|
||||
loadPartSchedulingChart,
|
||||
loadPartStocktakeTable,
|
||||
loadPartVariantTable,
|
||||
loadRelatedPartsTable,
|
||||
loadSimplePartTable,
|
||||
partDetail,
|
||||
partStockLabel,
|
||||
performStocktake,
|
||||
toggleStar,
|
||||
validateBom,
|
||||
*/
|
||||
@ -678,13 +681,281 @@ function makePartIcons(part) {
|
||||
}
|
||||
|
||||
return html;
|
||||
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Render part information for a table view
|
||||
*
|
||||
* part: JSON part object
|
||||
* options:
|
||||
* icons: Display part icons
|
||||
* thumb: Display part thumbnail
|
||||
* link: Display URL
|
||||
*/
|
||||
function partDetail(part, options={}) {
|
||||
|
||||
var html = '';
|
||||
|
||||
var name = part.full_name;
|
||||
|
||||
if (options.thumb) {
|
||||
html += imageHoverIcon(part.thumbnail || part.image);
|
||||
}
|
||||
|
||||
if (options.link) {
|
||||
var url = `/part/${part.pk}/`;
|
||||
html += renderLink(shortenString(name), url);
|
||||
} else {
|
||||
html += shortenString(name);
|
||||
}
|
||||
|
||||
if (options.icons) {
|
||||
html += makePartIcons(part);
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Guide user through "stocktake" process
|
||||
*/
|
||||
function performStocktake(partId, options={}) {
|
||||
|
||||
var part_quantity = 0;
|
||||
|
||||
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>`;
|
||||
}
|
||||
|
||||
// First, load stock information for the part
|
||||
inventreeGet(
|
||||
'{% url "api-stock-list" %}',
|
||||
{
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Load table for part stocktake information
|
||||
*/
|
||||
function loadPartStocktakeTable(partId, options={}) {
|
||||
|
||||
var table = options.table || '#part-stocktake-table';
|
||||
|
||||
var params = options.params || {};
|
||||
|
||||
params.part = partId;
|
||||
|
||||
var filters = loadTableFilters('stocktake');
|
||||
|
||||
for (var key in params) {
|
||||
filters[key] = params[key];
|
||||
}
|
||||
|
||||
setupFilterList('stocktake', $(table), '#filter-list-partstocktake');
|
||||
|
||||
$(table).inventreeTable({
|
||||
url: '{% url "api-part-stocktake-list" %}',
|
||||
queryParams: filters,
|
||||
name: 'partstocktake',
|
||||
original: options.params,
|
||||
showColumns: true,
|
||||
sortable: true,
|
||||
formatNoMatches: function() {
|
||||
return '{% trans "No stocktake information available" %}';
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
field: 'quantity',
|
||||
title: '{% trans "Quantity" %}',
|
||||
switchable: false,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
field: 'note',
|
||||
title: '{% trans "Notes" %}',
|
||||
switchable: true,
|
||||
},
|
||||
{
|
||||
field: 'date',
|
||||
title: '{% trans "Date" %}',
|
||||
switchable: false,
|
||||
sortable: true,
|
||||
formatter: function(value, row) {
|
||||
var html = renderDate(value);
|
||||
|
||||
if (row.user_detail) {
|
||||
html += `<span class='badge bg-dark rounded-pill float-right'>${row.user_detail.username}</span>`;
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
||||
},
|
||||
{
|
||||
field: 'actions',
|
||||
title: '',
|
||||
visible: options.admin,
|
||||
switchable: false,
|
||||
sortable: false,
|
||||
formatter: function(value, row) {
|
||||
var html = `<div class='btn-group float-right' role='group'>`;
|
||||
|
||||
if (options.allow_edit) {
|
||||
html += makeIconButton('fa-edit icon-blue', 'button-edit-stocktake', row.pk, '{% trans "Edit Stocktake Entry" %}');
|
||||
}
|
||||
|
||||
if (options.allow_delete) {
|
||||
html += makeIconButton('fa-trash-alt icon-red', 'button-delete-stocktake', row.pk, '{% trans "Delete Stocktake Entry" %}');
|
||||
}
|
||||
|
||||
html += `</div>`;
|
||||
|
||||
return html;
|
||||
}
|
||||
}
|
||||
],
|
||||
onPostBody: function() {
|
||||
// Button callbacks
|
||||
$(table).find('.button-edit-stocktake').click(function() {
|
||||
var pk = $(this).attr('pk');
|
||||
|
||||
constructForm(`/api/part/stocktake/${pk}/`, {
|
||||
fields: {
|
||||
quantity: {},
|
||||
note: {},
|
||||
},
|
||||
title: '{% trans "Edit Stocktake Entry" %}',
|
||||
onSuccess: function() {
|
||||
$(table).bootstrapTable('refresh');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$(table).find('.button-delete-stocktake').click(function() {
|
||||
var pk = $(this).attr('pk');
|
||||
|
||||
constructForm(`/api/part/stocktake/${pk}/`, {
|
||||
method: 'DELETE',
|
||||
title: '{% trans "Delete Stocktake Entry" %}',
|
||||
onSuccess: function() {
|
||||
$(table).bootstrapTable('refresh');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/* Load part variant table
|
||||
*/
|
||||
function loadPartVariantTable(table, partId, options={}) {
|
||||
/* Load part variant table
|
||||
*/
|
||||
|
||||
var params = options.params || {};
|
||||
|
||||
@ -1625,6 +1896,16 @@ function loadPartTable(table, url, options={}) {
|
||||
}
|
||||
});
|
||||
|
||||
columns.push({
|
||||
field: 'last_stocktake',
|
||||
title: '{% trans "Last Stocktake" %}',
|
||||
sortable: true,
|
||||
switchable: true,
|
||||
formatter: function(value) {
|
||||
return renderDate(value);
|
||||
}
|
||||
});
|
||||
|
||||
// Push an "actions" column
|
||||
if (options.actions) {
|
||||
columns.push({
|
||||
|
@ -19,7 +19,6 @@
|
||||
makeIconBadge,
|
||||
makeIconButton,
|
||||
makeOptionsList,
|
||||
makePartIcons,
|
||||
modalEnable,
|
||||
modalSetContent,
|
||||
modalSetTitle,
|
||||
@ -1742,15 +1741,11 @@ function loadStockTable(table, options) {
|
||||
switchable: params['part_detail'],
|
||||
formatter: function(value, row) {
|
||||
|
||||
var url = `/part/${row.part}/`;
|
||||
var thumb = row.part_detail.thumbnail;
|
||||
var name = row.part_detail.full_name;
|
||||
|
||||
var html = imageHoverIcon(thumb) + renderLink(shortenString(name), url);
|
||||
|
||||
html += makePartIcons(row.part_detail);
|
||||
|
||||
return html;
|
||||
return partDetail(row.part_detail, {
|
||||
thumb: true,
|
||||
link: true,
|
||||
icons: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -499,6 +499,10 @@ function getAvailableTableFilters(tableKey) {
|
||||
type: 'bool',
|
||||
title: '{% trans "Subscribed" %}',
|
||||
},
|
||||
stocktake: {
|
||||
type: 'bool',
|
||||
title: '{% trans "Has stocktake entries" %}',
|
||||
},
|
||||
is_template: {
|
||||
type: 'bool',
|
||||
title: '{% trans "Template" %}',
|
||||
|
Reference in New Issue
Block a user