2
0
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:
Oliver
2022-12-31 23:14:43 +11:00
committed by GitHub
parent 4ae278d119
commit ab4e2aa8bb
23 changed files with 803 additions and 22 deletions

View File

@ -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>

View File

@ -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;

View File

@ -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 || {};

View File

@ -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({

View File

@ -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,
});
}
};

View File

@ -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" %}',