2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-06-19 05:25:42 +00:00

merge upstream

This commit is contained in:
Matthias
2021-11-04 23:44:41 +01:00
68 changed files with 2310 additions and 1115 deletions

View File

@ -76,6 +76,7 @@ function addHeaderAction(label, title, icon, options) {
}
{% settings_value 'HOMEPAGE_PART_STARRED' user=request.user as setting_part_starred %}
{% settings_value 'HOMEPAGE_CATEGORY_STARRED' user=request.user as setting_category_starred %}
{% settings_value 'HOMEPAGE_PART_LATEST' user=request.user as setting_part_latest %}
{% settings_value 'HOMEPAGE_BOM_VALIDATION' user=request.user as setting_bom_validation %}
{% to_list setting_part_starred setting_part_latest setting_bom_validation as settings_list_part %}
@ -84,15 +85,25 @@ function addHeaderAction(label, title, icon, options) {
addHeaderTitle('{% trans "Parts" %}');
{% if setting_part_starred %}
addHeaderAction('starred-parts', '{% trans "Starred Parts" %}', 'fa-star');
addHeaderAction('starred-parts', '{% trans "Subscribed Parts" %}', 'fa-bell');
loadSimplePartTable("#table-starred-parts", "{% url 'api-part-list' %}", {
params: {
"starred": true,
starred: true,
},
name: 'starred_parts',
});
{% endif %}
{% if setting_category_starred %}
addHeaderAction('starred-categories', '{% trans "Subscribed Categories" %}', 'fa-bell');
loadPartCategoryTable($('#table-starred-categories'), {
params: {
starred: true,
},
name: 'starred_categories'
});
{% endif %}
{% if setting_part_latest %}
addHeaderAction('latest-parts', '{% trans "Latest Parts" %}', 'fa-newspaper');
loadSimplePartTable("#table-latest-parts", "{% url 'api-part-list' %}", {
@ -128,8 +139,7 @@ loadSimplePartTable("#table-bom-validation", "{% url 'api-part-list' %}", {
{% to_list setting_stock_recent setting_stock_low setting_stock_depleted setting_stock_needed as settings_list_stock %}
{% endif %}
{% if roles.stock.view and True in settings_list_stock %}
addHeaderTitle('{% trans "Stock" %}');
{% if roles.stock.view %}
{% if setting_stock_recent %}
addHeaderAction('recently-updated-stock', '{% trans "Recently Updated" %}', 'fa-clock');
@ -145,7 +155,7 @@ loadStockTable($('#table-recently-updated-stock'), {
{% endif %}
{% if setting_stock_low %}
addHeaderAction('low-stock', '{% trans "Low Stock" %}', 'fa-shopping-cart');
addHeaderAction('low-stock', '{% trans "Low Stock" %}', 'fa-flag');
loadSimplePartTable("#table-low-stock", "{% url 'api-part-list' %}", {
params: {
low_stock: true,

View File

@ -14,7 +14,8 @@
<div class='row'>
<table class='table table-striped table-condensed'>
<tbody>
{% include "InvenTree/settings/setting.html" with key="HOMEPAGE_PART_STARRED" icon='fa-star' user_setting=True %}
{% include "InvenTree/settings/setting.html" with key="HOMEPAGE_PART_STARRED" icon='fa-bell' user_setting=True %}
{% include "InvenTree/settings/setting.html" with key="HOMEPAGE_CATEGORY_STARRED" icon='fa-bell' user_setting=True %}
{% include "InvenTree/settings/setting.html" with key="HOMEPAGE_PART_LATEST" icon='fa-history' user_setting=True %}
{% include "InvenTree/settings/setting.html" with key="PART_RECENT_COUNT" icon="fa-clock" user_setting=True %}
{% include "InvenTree/settings/setting.html" with key="HOMEPAGE_BOM_VALIDATION" user_setting=True %}

View File

@ -16,6 +16,7 @@
<tbody>
{% include "InvenTree/settings/setting.html" with key="SEARCH_PREVIEW_RESULTS" user_setting=True icon='fa-search' %}
{% include "InvenTree/settings/setting.html" with key="SEARCH_SHOW_STOCK_LEVELS" user_setting=True icon='fa-boxes' %}
{% include "InvenTree/settings/setting.html" with key="SEARCH_HIDE_INACTIVE_PARTS" user_setting=True icon='fa-eye-slash' %}
</tbody>
</table>
</div>

View File

@ -10,6 +10,26 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Favicon -->
<link rel="apple-touch-icon" sizes="57x57" href="{% static 'img/favicon/apple-icon-57x57.png' %}">
<link rel="apple-touch-icon" sizes="60x60" href="{% static 'img/favicon/apple-icon-60x60.png' %}">
<link rel="apple-touch-icon" sizes="72x72" href="{% static 'img/favicon/apple-icon-72x72.png' %}">
<link rel="apple-touch-icon" sizes="76x76" href="{% static 'img/favicon/apple-icon-76x76.png' %}">
<link rel="apple-touch-icon" sizes="114x114" href="{% static 'img/favicon/apple-icon-114x114.png' %}">
<link rel="apple-touch-icon" sizes="120x120" href="{% static 'img/favicon/apple-icon-120x120.png' %}">
<link rel="apple-touch-icon" sizes="144x144" href="{% static 'img/favicon/apple-icon-144x144.png' %}">
<link rel="apple-touch-icon" sizes="152x152" href="{% static 'img/favicon/apple-icon-152x152.png' %}">
<link rel="apple-touch-icon" sizes="180x180" href="{% static 'img/favicon/apple-icon-180x180.png' %}">
<link rel="icon" type="image/png" sizes="192x192" href="{% static 'img/favicon/android-icon-192x192.png' %}">
<link rel="icon" type="image/png" sizes="32x32" href="{% static 'img/favicon/favicon-32x32.png' %}">
<link rel="icon" type="image/png" sizes="96x96" href="{% static 'img/favicon/favicon-96x96.png' %}">
<link rel="icon" type="image/png" sizes="16x16" href="{% static 'img/favicon/favicon-16x16.png' %}">
<link rel="manifest" href="{% static 'img/favicon/manifest.json' %}">
<meta name="msapplication-TileColor" content="#ffffff">
<meta name="msapplication-TileImage" content="{% static 'img/favicon/ms-icon-144x144.png' %}">
<meta name="theme-color" content="#ffffff">
<!-- CSS -->
<link rel="stylesheet" href="{% static 'fontawesome/css/brands.css' %}">
<link rel="stylesheet" href="{% static 'fontawesome/css/solid.css' %}">
@ -33,16 +53,23 @@
<!--
Background Image Attribution: https://unsplash.com/photos/Ixvv3YZkd7w
-->
<div class='container-fluid'>
<div class='notification-area' id='alerts'>
<!-- Div for displayed alerts -->
</div>
</div>
<div class='main body-wrapper login-screen d-flex'>
<div class='login-container'>
<div class="row">
<div class='container-fluid'>
<div class='clearfix content-heading login-header'>
<div class='clearfix content-heading login-header d-flex flex-wrap'>
<img class="pull-left" src="{% static 'img/inventree.png' %}" width="60" height="60"/>
<span><h3>{% inventree_title %}</h3></span>
{% include "spacer.html" %}
<span class='float-right'><h3>{% inventree_title %}</h3></span>
</div>
<hr>
<div class='container-fluid'>{% block content %}{% endblock %}</div>
@ -52,22 +79,31 @@
{% block extra_body %}
{% endblock %}
{% include 'notification.html' %}
</div>
<!-- Scripts -->
<script type="text/javascript" src="{% static 'script/jquery_3.3.1_jquery.min.js' %}"></script>
<script type='text/javascript' src="{% static 'script/jquery-ui/jquery-ui.min.js' %}"></script>
<script type="text/javascript" src="{% static 'bootstrap/js/bootstrap.bundle.min.js' %}"></script>
<!-- general InvenTree -->
<script type='text/javascript' src="{% static 'script/inventree/inventree.js' %}"></script>
<script type='text/javascript' src="{% static 'script/inventree/notification.js' %}"></script>
<!-- dynamic javascript templates -->
<script type='text/javascript' src="{% url 'inventree.js' %}"></script>
<!-- fontawesome -->
<script type='text/javascript' src="{% static 'fontawesome/js/solid.js' %}"></script>
<script type='text/javascript' src="{% static 'fontawesome/js/brands.js' %}"></script>
<script type='text/javascript' src="{% static 'fontawesome/js/fontawesome.js' %}"></script>
<!-- 3rd party general js -->
<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 'select2/js/select2.full.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/clipboard.min.js' %}"></script>
<script type='text/javascript' src="{% static 'script/randomColor.min.js' %}"></script>
<script type='text/javascript'>
@ -75,12 +111,16 @@ $(document).ready(function () {
// notifications
{% if messages %}
{% for message in messages %}
showAlertOrCache('alert-info', '{{message}}', true);
showAlertOrCache(
'{{ message }}',
true,
{
style: 'info',
}
);
{% endfor %}
{% endif %}
showCachedAlerts();
inventreeDocReady();
});

View File

@ -32,12 +32,12 @@ for a account and sign in below:{% endblocktrans %}</p>
<input type="hidden" name="{{ redirect_field_name }}" value="{{ redirect_field_value }}" />
{% endif %}
<div class="btn-toolbar">
<button class="btn btn-primary col-md-8" type="submit">{% trans "Sign In" %}</button>
{% if mail_conf and enable_pwd_forgot %}
<a class="btn btn-primary" href="{% url 'account_reset_password' %}">{% trans "Forgot Password?" %}</a>
{% endif %}
</div>
<div class="btn-group float-right" role="group">
<button class="btn btn-success" type="submit">{% trans "Sign In" %}</button>
</div>
{% if mail_conf and enable_pwd_forgot %}
<a class="" href="{% url 'account_reset_password' %}"><small>{% trans "Forgot Password?" %}</small></a>
{% endif %}
</form>
{% if enable_sso %}

View File

@ -14,7 +14,10 @@
{% if redirect_field_value %}
<input type="hidden" name="{{ redirect_field_name }}" value="{{ redirect_field_value }}"/>
{% endif %}
<button type="submit" class="btn btn-primary btn-block">{% trans 'Sign Out' %}</button>
<div class='btn-group float-right' role='group'>
<a type='button' class='btn btn-secondary' href='{% url "index" %}'><span class='fas fa-undo-alt'></span> {% trans "Back to Site" %}</a>
<button type="submit" class="btn btn-danger btn-block">{% trans 'Sign Out' %}</button>
</div>
</form>

View File

@ -14,7 +14,9 @@
<form method="POST" action="{{ action_url }}">
{% csrf_token %}
{{ form|crispy }}
<input type="submit" name="action" class="btn btn-primary btn-block" value="{% trans 'change password' %}"/>
<div class='btn-group float-right' role='group'>
<input type="submit" name="action" class="btn btn-success" value="{% trans 'Change password' %}"/>
</div>
</form>
{% else %}
<p>{% trans 'Your password is now changed.' %}</p>

View File

@ -84,6 +84,13 @@
</div>
</div>
<main class='col ps-md-2 pt-2'>
{% block alerts %}
<div class='notification-area' id='alerts'>
<!-- Div for displayed alerts -->
</div>
{% endblock %}
{% block breadcrumb_list %}
<div class='container-fluid navigation'>
<nav aria-label='breadcrumb'>
@ -102,7 +109,6 @@
</div>
{% include 'modals.html' %}
{% include 'about.html' %}
{% include 'notification.html' %}
</div>
<!-- Scripts -->
@ -135,9 +141,9 @@
<!-- general InvenTree -->
<script type='text/javascript' src="{% static 'script/inventree/notification.js' %}"></script>
<script type='text/javascript' src="{% static 'script/inventree/inventree.js' %}"></script>
<!-- dynamic javascript templates -->
<script type='text/javascript' src="{% url 'inventree.js' %}"></script>
<script type='text/javascript' src="{% url 'calendar.js' %}"></script>
<script type='text/javascript' src="{% url 'nav.js' %}"></script>
<script type='text/javascript' src="{% url 'settings.js' %}"></script>
@ -177,15 +183,13 @@ $(document).ready(function () {
inventreeDocReady();
showCachedAlerts();
{% if barcodes %}
$('#barcode-scan').click(function() {
barcodeScanDialog();
});
{% endif %}
moment.locale('{{request.LANGUAGE_CODE}}');
moment.locale('{{ request.LANGUAGE_CODE }}');
});
</script>

View File

@ -0,0 +1,39 @@
{% extends "email/email.html" %}
{% load i18n %}
{% load inventree_extras %}
{% block title %}
{% trans "Stock is required for the following build order" %}<br>
{% blocktrans with build=build.reference part=part.full_name quantity=build.quantity %}Build order {{ build }} - building {{ quantity }} x {{ part }}{% endblocktrans %}
<br>
<p>{% trans "Click on the following link to view this build order" %}: <a href='{{ link }}'>{{ link }}</a></p>
{% endblock title %}
{% block body %}
<tr colspan='100%' style='height: 2rem; text-align: center;'>{% trans "The following parts are low on required stock" %}</tr>
<tr style="height: 3rem; border-bottom: 1px solid">
<th>{% trans "Part" %}</th>
<th>{% trans "Required Quantity" %}</th>
<th>{% trans "Available" %}</th>
</tr>
{% for line in lines %}
<tr style="height: 2.5rem; border-bottom: 1px solid">
<td style='padding-left: 1em;'>
<a href='{{ line.link }}'>{{ line.part.full_name }}</a>{% if part.description %} - <em>{{ part.description }}</em>{% endif %}
</td>
<td style="text-align: center;">
{% decimal line.required %} {% if line.part.units %}{{ line.part.units }}{% endif %}
</td>
<td style="text-align: center;">{% decimal line.available %} {% if line.part.units %}{{ line.part.units }}{% endif %}</td>
</tr>
{% endfor %}
{% endblock body %}
{% block footer_prefix %}
<p><em>{% blocktrans with part=part.name %}You are receiving this email because you are subscribed to notifications for this part {% endblocktrans %}.</em></p>
{% endblock footer_prefix %}

View File

@ -8,22 +8,25 @@
{% if link %}
<p>{% trans "Click on the following link to view this part" %}: <a href="{{ link }}">{{ link }}</a></p>
{% endif %}
{% endblock %}
{% endblock title %}
{% block subtitle %}
<p><em>{% blocktrans with part=part.name %}You are receiving this email because you are subscribed to notifications for this part {% endblocktrans %}.</em></p>
{% endblock %}
{% block body %}
<tr style="height: 3rem; border-bottom: 1px solid">
<th>{% trans "Part Name" %}</th>
<th>{% trans "Available Quantity" %}</th>
<th>{% trans "Part" %}</th>
<th>{% trans "Total Stock" %}</th>
<th>{% trans "Available" %}</th>
<th>{% trans "Minimum Quantity" %}</th>
</tr>
<tr style="height: 3rem">
<td style="text-align: center;">{{ part.full_name }}</td>
<td style="text-align: center;">{{ part.total_stock }}</td>
<td style="text-align: center;">{{ part.minimum_stock }}</td>
<td style="text-align: center;">{% decimal part.total_stock %}</td>
<td style="text-align: center;">{% decimal part.available_stock %}</td>
<td style="text-align: center;">{% decimal part.minimum_stock %}</td>
</tr>
{% endblock %}
{% endblock body %}
{% block footer_prefix %}
<p><em>{% blocktrans with part=part.name %}You are receiving this email because you are subscribed to notifications for this part {% endblocktrans %}.</em></p>
{% endblock footer_prefix %}

View File

@ -1,312 +0,0 @@
{% load inventree_extras %}
/* globals
ClipboardJS,
inventreeFormDataUpload,
launchModalForm,
user_settings,
*/
/* exported
attachClipboard,
enableDragAndDrop,
exportFormatOptions,
inventreeDocReady,
inventreeLoad,
inventreeSave,
*/
function attachClipboard(selector, containerselector, textElement) {
// set container
if (containerselector) {
containerselector = document.getElementById(containerselector);
} else {
containerselector = document.body;
}
var text = null;
// set text-function
if (textElement) {
text = function() {
return document.getElementById(textElement).textContent;
};
} else {
text = function(trigger) {
var content = trigger.parentElement.parentElement.textContent;
return content.trim();
};
}
// create Clipboard
// eslint-disable-next-line no-unused-vars
var cis = new ClipboardJS(selector, {
text: text,
container: containerselector
});
}
/**
* Return a standard list of export format options *
*/
function exportFormatOptions() {
return [
{
value: 'csv',
display_name: 'CSV',
},
{
value: 'tsv',
display_name: 'TSV',
},
{
value: 'xls',
display_name: 'XLS',
},
{
value: 'xlsx',
display_name: 'XLSX',
},
];
}
function inventreeDocReady() {
/* Run this function when the HTML document is loaded.
* This will be called for every page that extends "base.html"
*/
window.addEventListener('dragover', function(e) {
e = e || event;
e.preventDefault();
}, false);
window.addEventListener('drop', function(e) {
e = e || event;
e.preventDefault();
}, false);
/* Add drag-n-drop functionality to any element
* marked with the class 'dropzone'
*/
$('.dropzone').on('dragenter', function(event) {
// TODO - Only indicate that a drop event will occur if a file is being dragged
var transfer = event.originalEvent.dataTransfer;
// eslint-disable-next-line no-constant-condition
if (true || isFileTransfer(transfer)) {
$(this).addClass('dragover');
}
});
$('.dropzone').on('dragleave drop', function() {
$(this).removeClass('dragover');
});
// Callback to launch the 'About' window
$('#launch-about').click(function() {
var modal = $('#modal-about');
modal.modal({
backdrop: 'static',
keyboard: true,
});
modal.modal('show');
});
// Callback to launch the 'Database Stats' window
$('#launch-stats').click(function() {
launchModalForm('/stats/', {
no_post: true,
});
});
// Initialize clipboard-buttons
attachClipboard('.clip-btn');
attachClipboard('.clip-btn', 'modal-about');
attachClipboard('.clip-btn-version', 'modal-about', 'about-copy-text');
// Add autocomplete to the search-bar
$('#search-bar').autocomplete({
source: function(request, response) {
$.ajax({
url: '/api/part/',
data: {
search: request.term,
limit: user_settings.SEARCH_PREVIEW_RESULTS,
offset: 0
},
success: function(data) {
var transformed = $.map(data.results, function(el) {
return {
label: el.full_name,
id: el.pk,
thumbnail: el.thumbnail,
data: el,
};
});
response(transformed);
},
error: function() {
response([]);
}
});
},
create: function() {
$(this).data('ui-autocomplete')._renderItem = function(ul, item) {
var html = `<a href='/part/${item.id}/'><span>`;
html += `<img class='hover-img-thumb' src='`;
html += item.thumbnail || `/static/img/blank_image.png`;
html += `'> `;
html += item.label;
html += '</span>';
if (user_settings.SEARCH_SHOW_STOCK_LEVELS) {
html += partStockLabel(item.data);
}
html += '</a>';
return $('<li>').append(html).appendTo(ul);
};
},
select: function( event, ui ) {
window.location = '/part/' + ui.item.id + '/';
},
minLength: 2,
classes: {
'ui-autocomplete': 'dropdown-menu search-menu',
},
});
// Generate brand-icons
$('.brand-icon').each(function(i, obj) {
loadBrandIcon($(this), $(this).attr('brand_name'));
});
// Callback for "admin view" button
$('#admin-button').click(function() {
var url = $(this).attr('url');
location.href = url;
});
}
function isFileTransfer(transfer) {
/* Determine if a transfer (e.g. drag-and-drop) is a file transfer
*/
return transfer.files.length > 0;
}
function enableDragAndDrop(element, url, options) {
/* Enable drag-and-drop file uploading for a given element.
Params:
element - HTML element lookup string e.g. "#drop-div"
url - URL to POST the file to
options - object with following possible values:
label - Label of the file to upload (default='file')
data - Other form data to upload
success - Callback function in case of success
error - Callback function in case of error
method - HTTP method
*/
var data = options.data || {};
$(element).on('drop', function(event) {
var transfer = event.originalEvent.dataTransfer;
var label = options.label || 'file';
var formData = new FormData();
// Add the extra data
for (var key in data) {
formData.append(key, data[key]);
}
if (isFileTransfer(transfer)) {
formData.append(label, transfer.files[0]);
inventreeFormDataUpload(
url,
formData,
{
success: function(data, status, xhr) {
console.log('Uploaded file via drag-and-drop');
if (options.success) {
options.success(data, status, xhr);
}
},
error: function(xhr, status, error) {
console.log('File upload failed');
if (options.error) {
options.error(xhr, status, error);
}
},
method: options.method || 'POST',
}
);
} else {
console.log('Ignoring drag-and-drop event (not a file)');
}
});
}
/**
* Save a key:value pair to local storage
* @param {String} name - settting key
* @param {String} value - setting value
*/
function inventreeSave(name, value) {
var key = `inventree-${name}`;
localStorage.setItem(key, value);
}
/**
* Retrieve a key:value pair from local storage
* @param {*} name - setting key
* @param {*} defaultValue - default value (returned if no matching key:value pair is found)
* @returns
*/
function inventreeLoad(name, defaultValue) {
var key = `inventree-${name}`;
var value = localStorage.getItem(key);
if (value == null) {
return defaultValue;
} else {
return value;
}
}
function loadBrandIcon(element, name) {
// check if icon exists
var icon = window.FontAwesome.icon({prefix: 'fab', iconName: name});
if (icon) {
// add icon to button
element.addClass('fab fa-' + name);
}
}
// Convenience function to determine if an element exists
$.fn.exists = function() {
return this.length !== 0;
};

View File

@ -2,8 +2,6 @@
{% load inventree_extras %}
/* globals
renderErrorMessage,
showAlertDialog,
*/
/* exported
@ -68,6 +66,8 @@ function inventreeGet(url, filters={}, options={}) {
options.error({
error: thrownError
});
} else {
showApiError(xhr, url);
}
}
});
@ -104,6 +104,8 @@ function inventreeFormDataUpload(url, data, options={}) {
if (options.error) {
options.error(xhr, status, error);
} else {
showApiError(xhr, url);
}
}
});
@ -139,6 +141,8 @@ function inventreePut(url, data={}, options={}) {
} else {
console.error(`Error on ${method} to '${url}' - STATUS ${xhr.status}`);
console.error(thrownError);
showApiError(xhr, url);
}
},
complete: function(xhr, status) {
@ -162,8 +166,10 @@ function inventreeDelete(url, options={}) {
return inventreePut(url, {}, options);
}
function showApiError(xhr) {
/*
* Display a notification with error information
*/
function showApiError(xhr, url) {
var title = null;
var message = null;
@ -208,7 +214,11 @@ function showApiError(xhr) {
}
message += '<hr>';
message += renderErrorMessage(xhr);
message += `URL: ${url}`;
showAlertDialog(title, message);
showMessage(title, {
style: 'danger',
icon: 'fas fa-server icon-red',
details: message,
});
}

View File

@ -10,7 +10,6 @@
modalSetSubmitText,
modalShowSubmitButton,
modalSubmit,
showAlertOrCache,
showQuestionDialog,
*/
@ -36,7 +35,7 @@ function makeBarcodeInput(placeholderText='', hintText='') {
<label class='control-label' for='barcode'>{% trans "Barcode" %}</label>
<div class='controls'>
<div class='input-group'>
<span class='input-group-addon'>
<span class='input-group-text'>
<span class='fas fa-qrcode'></span>
</span>
<input id='barcode' class='textinput textInput form-control' type='text' name='barcode' placeholder='${placeholderText}'>
@ -59,7 +58,7 @@ function makeNotesField(options={}) {
<label class='control-label' for='notes'>{% trans "Notes" %}</label>
<div class='controls'>
<div class='input-group'>
<span class='input-group-addon'>
<span class='input-group-text'>
<span class='fas fa-sticky-note'></span>
</span>
<input id='notes' class='textinput textInput form-control' type='text' name='notes' placeholder='${placeholder}'>
@ -480,10 +479,13 @@ function barcodeCheckIn(location_id) {
$(modal).modal('hide');
if (status == 'success' && 'success' in response) {
showAlertOrCache('alert-success', response.success, true);
addCachedAlert(response.success);
location.reload();
} else {
showAlertOrCache('alert-success', '{% trans "Error transferring stock" %}', false);
showMessage('{% trans "Error transferring stock" %}', {
style: 'danger',
icon: 'fas fa-times-circle',
});
}
}
}
@ -604,10 +606,12 @@ function scanItemsIntoLocation(item_id_list, options={}) {
$(modal).modal('hide');
if (status == 'success' && 'success' in response) {
showAlertOrCache('alert-success', response.success, true);
addCachedAlert(response.success);
location.reload();
} else {
showAlertOrCache('alert-danger', '{% trans "Error transferring stock" %}', false);
showMessage('{% trans "Error transferring stock" %}', {
style: 'danger',
});
}
}
}

View File

@ -339,7 +339,7 @@ function completeBuildOutputs(build_id, outputs, options={}) {
break;
default:
$(opts.modal).modal('hide');
showApiError(xhr);
showApiError(xhr, opts.url);
break;
}
}
@ -1527,7 +1527,7 @@ function allocateStockToBuild(build_id, part_id, bom_items, options={}) {
break;
default:
$(opts.modal).modal('hide');
showApiError(xhr);
showApiError(xhr, opts.url);
break;
}
}

View File

@ -25,7 +25,12 @@
*/
/* exported
setFormGroupVisibility
clearFormInput,
disableFormInput,
enableFormInput,
hideFormInput,
setFormGroupVisibility,
showFormInput,
*/
/**
@ -113,6 +118,10 @@ function canDelete(OPTIONS) {
*/
function getApiEndpointOptions(url, callback) {
if (!url) {
return;
}
// Return the ajax request object
$.ajax({
url: url,
@ -123,9 +132,10 @@ function getApiEndpointOptions(url, callback) {
json: 'application/json',
},
success: callback,
error: function() {
error: function(xhr) {
// TODO: Handle error
console.log(`ERROR in getApiEndpointOptions at '${url}'`);
showApiError(xhr, url);
}
});
}
@ -181,6 +191,7 @@ function constructChangeForm(fields, options) {
// Request existing data from the API endpoint
$.ajax({
url: options.url,
data: options.params || {},
type: 'GET',
contentType: 'application/json',
dataType: 'json',
@ -196,15 +207,28 @@ function constructChangeForm(fields, options) {
fields[field].value = data[field];
}
}
// An optional function can be provided to process the returned results,
// before they are rendered to the form
if (options.processResults) {
var processed = options.processResults(data, fields, options);
// If the processResults function returns data, it will be stored
if (processed) {
data = processed;
}
}
// Store the entire data object
options.instance = data;
constructFormBody(fields, options);
},
error: function() {
error: function(xhr) {
// TODO: Handle error here
console.log(`ERROR in constructChangeForm at '${options.url}'`);
showApiError(xhr, options.url);
}
});
}
@ -241,9 +265,11 @@ function constructDeleteForm(fields, options) {
constructFormBody(fields, options);
},
error: function() {
error: function(xhr) {
// TODO: Handle error here
console.log(`ERROR in constructDeleteForm at '${options.url}`);
showApiError(xhr, options.url);
}
});
}
@ -708,7 +734,9 @@ function submitFormData(fields, options) {
break;
default:
$(options.modal).modal('hide');
showApiError(xhr);
console.log(`upload error at ${options.url}`);
showApiError(xhr, options.url);
break;
}
}
@ -885,19 +913,19 @@ function handleFormSuccess(response, options) {
// Display any messages
if (response && response.success) {
showAlertOrCache('alert-success', response.success, cache);
showAlertOrCache(response.success, cache, {style: 'success'});
}
if (response && response.info) {
showAlertOrCache('alert-info', response.info, cache);
showAlertOrCache(response.info, cache, {style: 'info'});
}
if (response && response.warning) {
showAlertOrCache('alert-warning', response.warning, cache);
showAlertOrCache(response.warning, cache, {style: 'warning'});
}
if (response && response.danger) {
showAlertOrCache('alert-danger', response.danger, cache);
showAlertOrCache(response.danger, cache, {style: 'danger'});
}
if (options.onSuccess) {
@ -1236,6 +1264,35 @@ function initializeGroups(fields, options) {
}
}
// Clear a form input
function clearFormInput(name, options) {
updateFieldValue(name, null, {}, options);
}
// Disable a form input
function disableFormInput(name, options) {
$(options.modal).find(`#id_${name}`).prop('disabled', true);
}
// Enable a form input
function enableFormInput(name, options) {
$(options.modal).find(`#id_${name}`).prop('disabled', false);
}
// Hide a form input
function hideFormInput(name, options) {
$(options.modal).find(`#div_id_${name}`).hide();
}
// Show a form input
function showFormInput(name, options) {
$(options.modal).find(`#div_id_${name}`).show();
}
// Hide a form group
function hideFormGroup(group, options) {
$(options.modal).find(`#form-panel-${group}`).hide();

View File

@ -399,19 +399,19 @@ function afterForm(response, options) {
// Display any messages
if (response.success) {
showAlertOrCache('alert-success', response.success, cache);
showAlertOrCache(response.success, cache, {style: 'success'});
}
if (response.info) {
showAlertOrCache('alert-info', response.info, cache);
showAlertOrCache(response.info, cache, {style: 'info'});
}
if (response.warning) {
showAlertOrCache('alert-warning', response.warning, cache);
showAlertOrCache(response.warning, cache, {style: 'warning'});
}
if (response.danger) {
showAlertOrCache('alert-danger', response.danger, cache);
showAlertOrCache(response.danger, cache, {style: 'danger'});
}
// Was a callback provided?

View File

@ -555,7 +555,7 @@ function receivePurchaseOrderItems(order_id, line_items, options={}) {
break;
default:
$(opts.modal).modal('hide');
showApiError(xhr);
showApiError(xhr, opts.url);
break;
}
}

View File

@ -373,24 +373,23 @@ function duplicatePart(pk, options={}) {
}
/* Toggle the 'starred' status of a part.
* Performs AJAX queries and updates the display on the button.
*
* options:
* - button: ID of the button (default = '#part-star-icon')
* - URL: API url of the object
* - user: pk of the user
*/
function toggleStar(options) {
/* Toggle the 'starred' status of a part.
* Performs AJAX queries and updates the display on the button.
*
* options:
* - button: ID of the button (default = '#part-star-icon')
* - part: pk of the part object
* - user: pk of the user
*/
var url = `/api/part/${options.part}/`;
inventreeGet(url, {}, {
inventreeGet(options.url, {}, {
success: function(response) {
var starred = response.starred;
inventreePut(
url,
options.url,
{
starred: !starred,
},
@ -398,9 +397,19 @@ function toggleStar(options) {
method: 'PATCH',
success: function(response) {
if (response.starred) {
$(options.button).addClass('icon-yellow');
$(options.button).removeClass('fa fa-bell-slash').addClass('fas fa-bell icon-green');
$(options.button).attr('title', '{% trans "You are subscribed to notifications for this item" %}');
showMessage('{% trans "You have subscribed to notifications for this item" %}', {
style: 'success',
});
} else {
$(options.button).removeClass('icon-yellow');
$(options.button).removeClass('fas fa-bell icon-green').addClass('fa fa-bell-slash');
$(options.button).attr('title', '{% trans "Subscribe to notifications for this item" %}');
showMessage('{% trans "You have unsubscribed to notifications for this item" %}', {
style: 'warning',
});
}
}
}
@ -410,12 +419,12 @@ function toggleStar(options) {
}
function partStockLabel(part) {
function partStockLabel(part, options={}) {
if (part.in_stock) {
return `<span class='badge rounded-pill bg-success'>{% trans "Stock" %}: ${part.in_stock}</span>`;
return `<span class='badge rounded-pill bg-success ${options.classes}'>{% trans "Stock" %}: ${part.in_stock}</span>`;
} else {
return `<span class='badge rounded-pill bg-danger'>{% trans "No Stock" %}</span>`;
return `<span class='badge rounded-pill bg-danger ${options.classes}'>{% trans "No Stock" %}</span>`;
}
}
@ -443,7 +452,7 @@ function makePartIcons(part) {
}
if (part.starred) {
html += makeIconBadge('fa-star', '{% trans "Starred part" %}');
html += makeIconBadge('fa-bell icon-green', '{% trans "Subscribed part" %}');
}
if (part.salable) {
@ -451,7 +460,7 @@ function makePartIcons(part) {
}
if (!part.active) {
html += `<span class='badge badge-right rounded-pill bg-warning'>{% trans "Inactive" %}</span>`;
html += `<span class='badge badge-right rounded-pill bg-warning'>{% trans "Inactive" %}</span> `;
}
return html;
@ -1133,8 +1142,10 @@ function loadPartTable(table, url, options={}) {
}
/*
* Display a table of part categories
*/
function loadPartCategoryTable(table, options) {
/* Display a table of part categories */
var params = options.params || {};
@ -1148,6 +1159,13 @@ function loadPartCategoryTable(table, options) {
filters = loadTableFilters(filterKey);
}
var tree_view = options.allowTreeView && inventreeLoad('category-tree-view') == 1;
if (tree_view) {
params.cascade = true;
}
var original = {};
for (var key in params) {
@ -1157,15 +1175,13 @@ function loadPartCategoryTable(table, options) {
setupFilterList(filterKey, table, filterListElement);
var tree_view = inventreeLoad('category-tree-view') == 1;
table.inventreeTable({
treeEnable: tree_view,
rootParentId: options.params.parent,
rootParentId: tree_view ? options.params.parent : null,
uniqueId: 'pk',
idField: 'pk',
treeShowField: 'name',
parentIdField: 'parent',
parentIdField: tree_view ? 'parent' : null,
method: 'get',
url: options.url || '{% url "api-part-category-list" %}',
queryParams: filters,
@ -1176,7 +1192,7 @@ function loadPartCategoryTable(table, options) {
name: 'category',
original: original,
showColumns: true,
buttons: [
buttons: options.allowTreeView ? [
{
icon: 'fas fa-bars',
attributes: {
@ -1215,28 +1231,31 @@ function loadPartCategoryTable(table, options) {
);
}
}
],
] : [],
onPostBody: function() {
tree_view = inventreeLoad('category-tree-view') == 1;
if (options.allowTreeView) {
if (tree_view) {
tree_view = inventreeLoad('category-tree-view') == 1;
$('#view-category-list').removeClass('btn-secondary').addClass('btn-outline-secondary');
$('#view-category-tree').removeClass('btn-outline-secondary').addClass('btn-secondary');
table.treegrid({
treeColumn: 0,
onChange: function() {
table.bootstrapTable('resetView');
},
onExpand: function() {
}
});
} else {
$('#view-category-tree').removeClass('btn-secondary').addClass('btn-outline-secondary');
$('#view-category-list').removeClass('btn-outline-secondary').addClass('btn-secondary');
if (tree_view) {
$('#view-category-list').removeClass('btn-secondary').addClass('btn-outline-secondary');
$('#view-category-tree').removeClass('btn-outline-secondary').addClass('btn-secondary');
table.treegrid({
treeColumn: 0,
onChange: function() {
table.bootstrapTable('resetView');
},
onExpand: function() {
}
});
} else {
$('#view-category-tree').removeClass('btn-secondary').addClass('btn-outline-secondary');
$('#view-category-list').removeClass('btn-outline-secondary').addClass('btn-secondary');
}
}
},
columns: [
@ -1253,10 +1272,17 @@ function loadPartCategoryTable(table, options) {
switchable: true,
sortable: true,
formatter: function(value, row) {
return renderLink(
var html = renderLink(
value,
`/part/category/${row.pk}/`
);
if (row.starred) {
html += makeIconBadge('fa-bell icon-green', '{% trans "Subscribed category" %}');
}
return html;
}
},
{

View File

@ -4,9 +4,6 @@
/* globals
attachSelect,
enableField,
clearField,
clearFieldOptions,
closeModal,
constructField,
constructFormBody,
@ -33,10 +30,8 @@
printStockItemLabels,
printTestReports,
renderLink,
reloadFieldOptions,
scanItemsIntoLocation,
showAlertDialog,
setFieldValue,
setupFilterList,
showApiError,
stockStatusDisplay,
@ -44,6 +39,10 @@
/* exported
createNewStockItem,
createStockLocation,
duplicateStockItem,
editStockItem,
editStockLocation,
exportStock,
loadInstalledInTable,
loadStockLocationTable,
@ -51,20 +50,318 @@
loadStockTestResultsTable,
loadStockTrackingTable,
loadTableFilters,
locationFields,
removeStockRow,
serializeStockItem,
stockItemFields,
stockLocationFields,
stockStatusCodes,
*/
function locationFields() {
return {
/*
* Launches a modal form to serialize a particular StockItem
*/
function serializeStockItem(pk, options={}) {
var url = `/api/stock/${pk}/serialize/`;
options.method = 'POST';
options.title = '{% trans "Serialize Stock Item" %}';
options.fields = {
quantity: {},
serial_numbers: {
icon: 'fa-hashtag',
},
destination: {
icon: 'fa-sitemap',
},
notes: {},
};
constructForm(url, options);
}
function stockLocationFields(options={}) {
var fields = {
parent: {
help_text: '{% trans "Parent stock location" %}',
},
name: {},
description: {},
};
if (options.parent) {
fields.parent.value = options.parent;
}
return fields;
}
/*
* Launch an API form to edit a stock location
*/
function editStockLocation(pk, options={}) {
var url = `/api/stock/location/${pk}/`;
options.fields = stockLocationFields(options);
constructForm(url, options);
}
/*
* Launch an API form to create a new stock location
*/
function createStockLocation(options={}) {
var url = '{% url "api-location-list" %}';
options.method = 'POST';
options.fields = stockLocationFields(options);
options.title = '{% trans "New Stock Location" %}';
constructForm(url, options);
}
function stockItemFields(options={}) {
var fields = {
part: {
// Hide the part field unless we are "creating" a new stock item
hidden: !options.create,
onSelect: function(data, field, opts) {
// Callback when a new "part" is selected
// If we are "creating" a new stock item,
// change the available fields based on the part properties
if (options.create) {
// If a "trackable" part is selected, enable serial number field
if (data.trackable) {
enableFormInput('serial_numbers', opts);
// showFormInput('serial_numbers', opts);
} else {
clearFormInput('serial_numbers', opts);
disableFormInput('serial_numbers', opts);
}
// Enable / disable fields based on purchaseable status
if (data.purchaseable) {
enableFormInput('supplier_part', opts);
enableFormInput('purchase_price', opts);
enableFormInput('purchase_price_currency', opts);
} else {
clearFormInput('supplier_part', opts);
clearFormInput('purchase_price', opts);
disableFormInput('supplier_part', opts);
disableFormInput('purchase_price', opts);
disableFormInput('purchase_price_currency', opts);
}
}
}
},
supplier_part: {
icon: 'fa-building',
filters: {
part_detail: true,
supplier_detail: true,
},
adjustFilters: function(query, opts) {
var part = getFormFieldValue('part', {}, opts);
if (part) {
query.part = part;
}
return query;
}
},
location: {
icon: 'fa-sitemap',
},
quantity: {
help_text: '{% trans "Enter initial quantity for this stock item" %}',
},
serial_numbers: {
icon: 'fa-hashtag',
type: 'string',
label: '{% trans "Serial Numbers" %}',
help_text: '{% trans "Enter serial numbers for new stock (or leave blank)" %}',
required: false,
},
serial: {
icon: 'fa-hashtag',
},
status: {},
expiry_date: {},
batch: {},
purchase_price: {
icon: 'fa-dollar-sign',
},
purchase_price_currency: {},
packaging: {
icon: 'fa-box',
},
link: {
icon: 'fa-link',
},
owner: {},
delete_on_deplete: {},
};
if (options.create) {
// Use special "serial numbers" field when creating a new stock item
delete fields['serial'];
} else {
// These fields cannot be edited once the stock item has been created
delete fields['serial_numbers'];
delete fields['quantity'];
delete fields['location'];
}
// Remove stock expiry fields if feature is not enabled
if (!global_settings.STOCK_ENABLE_EXPIRY) {
delete fields['expiry_date'];
}
// Remove ownership field if feature is not enanbled
if (!global_settings.STOCK_OWNERSHIP_CONTROL) {
delete fields['owner'];
}
return fields;
}
function stockItemGroups(options={}) {
return {
};
}
/*
* Launch a modal form to duplicate a given StockItem
*/
function duplicateStockItem(pk, options) {
// First, we need the StockItem informatino
inventreeGet(`/api/stock/${pk}/`, {}, {
success: function(data) {
// Do not duplicate the serial number
delete data['serial'];
options.data = data;
options.create = true;
options.fields = stockItemFields(options);
options.groups = stockItemGroups(options);
options.method = 'POST';
options.title = '{% trans "Duplicate Stock Item" %}';
constructForm('{% url "api-stock-list" %}', options);
}
});
}
/*
* Launch a modal form to edit a given StockItem
*/
function editStockItem(pk, options={}) {
var url = `/api/stock/${pk}/`;
options.create = false;
options.fields = stockItemFields(options);
options.groups = stockItemGroups(options);
options.title = '{% trans "Edit Stock Item" %}';
// Query parameters for retrieving stock item data
options.params = {
part_detail: true,
supplier_part_detail: true,
};
// Augment the rendered form when we receive information about the StockItem
options.processResults = function(data, fields, options) {
if (data.part_detail.trackable) {
delete options.fields.delete_on_deplete;
} else {
// Remove serial number field if part is not trackable
delete options.fields.serial;
}
// Remove pricing fields if part is not purchaseable
if (!data.part_detail.purchaseable) {
delete options.fields.supplier_part;
delete options.fields.purchase_price;
delete options.fields.purchase_price_currency;
}
};
constructForm(url, options);
}
/*
* Launch an API form to contsruct a new stock item
*/
function createNewStockItem(options={}) {
var url = '{% url "api-stock-list" %}';
options.title = '{% trans "New Stock Item" %}';
options.method = 'POST';
options.create = true;
options.fields = stockItemFields(options);
options.groups = stockItemGroups(options);
if (!options.onSuccess) {
options.onSuccess = function(response) {
// If a single stock item has been created, follow it!
if (response.pk) {
var url = `/stock/item/${response.pk}/`;
addCachedAlert('{% trans "Created new stock item" %}', {
icon: 'fas fa-boxes',
});
window.location.href = url;
} else {
// Multiple stock items have been created (i.e. serialized stock)
var details = `
<br>{% trans "Quantity" %}: ${response.quantity}
<br>{% trans "Serial Numbers" %}: ${response.serial_numbers}
`;
showMessage('{% trans "Created multiple stock items" %}', {
icon: 'fas fa-boxes',
details: details,
});
var table = options.table || '#stock-table';
// Reload the table
$(table).bootstrapTable('refresh');
}
};
}
constructForm(url, options);
}
@ -427,7 +724,7 @@ function adjustStock(action, items, options={}) {
break;
default:
$(opts.modal).modal('hide');
showApiError(xhr);
showApiError(xhr, opts.url);
break;
}
}
@ -1416,13 +1713,22 @@ function loadStockTable(table, options) {
});
}
/*
* Display a table of stock locations
*/
function loadStockLocationTable(table, options) {
/* Display a table of stock locations */
var params = options.params || {};
var filterListElement = options.filterList || '#filter-list-location';
var tree_view = options.allowTreeView && inventreeLoad('location-tree-view') == 1;
if (tree_view) {
params.cascade = true;
}
var filters = {};
var filterKey = options.filterKey || options.name || 'location';
@ -1443,15 +1749,13 @@ function loadStockLocationTable(table, options) {
filters[key] = params[key];
}
var tree_view = inventreeLoad('location-tree-view') == 1;
table.inventreeTable({
treeEnable: tree_view,
rootParentId: options.params.parent,
rootParentId: tree_view ? options.params.parent : null,
uniqueId: 'pk',
idField: 'pk',
treeShowField: 'name',
parentIdField: 'parent',
parentIdField: tree_view ? 'parent' : null,
disablePagination: tree_view,
sidePagination: tree_view ? 'client' : 'server',
serverSort: !tree_view,
@ -1465,28 +1769,31 @@ function loadStockLocationTable(table, options) {
showColumns: true,
onPostBody: function() {
tree_view = inventreeLoad('location-tree-view') == 1;
if (options.allowTreeView) {
if (tree_view) {
tree_view = inventreeLoad('location-tree-view') == 1;
$('#view-location-list').removeClass('btn-secondary').addClass('btn-outline-secondary');
$('#view-location-tree').removeClass('btn-outline-secondary').addClass('btn-secondary');
table.treegrid({
treeColumn: 1,
onChange: function() {
table.bootstrapTable('resetView');
},
onExpand: function() {
}
});
} else {
$('#view-location-tree').removeClass('btn-secondary').addClass('btn-outline-secondary');
$('#view-location-list').removeClass('btn-outline-secondary').addClass('btn-secondary');
if (tree_view) {
$('#view-location-list').removeClass('btn-secondary').addClass('btn-outline-secondary');
$('#view-location-tree').removeClass('btn-outline-secondary').addClass('btn-secondary');
table.treegrid({
treeColumn: 1,
onChange: function() {
table.bootstrapTable('resetView');
},
onExpand: function() {
}
});
} else {
$('#view-location-tree').removeClass('btn-secondary').addClass('btn-outline-secondary');
$('#view-location-list').removeClass('btn-outline-secondary').addClass('btn-secondary');
}
}
},
buttons: [
buttons: options.allowTreeView ? [
{
icon: 'fas fa-bars',
attributes: {
@ -1525,7 +1832,7 @@ function loadStockLocationTable(table, options) {
);
}
}
],
] : [],
columns: [
{
checkbox: true,
@ -1800,79 +2107,6 @@ function loadStockTrackingTable(table, options) {
}
function createNewStockItem(options) {
/* Launch a modal form to create a new stock item.
*
* This is really just a helper function which calls launchModalForm,
* but it does get called a lot, so here we are ...
*/
// Add in some funky options
options.callback = [
{
field: 'part',
action: function(value) {
if (!value) {
// No part chosen
clearFieldOptions('supplier_part');
enableField('serial_numbers', false);
enableField('purchase_price_0', false);
enableField('purchase_price_1', false);
return;
}
// Reload options for supplier part
reloadFieldOptions(
'supplier_part',
{
url: '{% url "api-supplier-part-list" %}',
params: {
part: value,
pretty: true,
},
text: function(item) {
return item.pretty_name;
}
}
);
// Request part information from the server
inventreeGet(
`/api/part/${value}/`, {},
{
success: function(response) {
// Disable serial number field if the part is not trackable
enableField('serial_numbers', response.trackable);
clearField('serial_numbers');
enableField('purchase_price_0', response.purchaseable);
enableField('purchase_price_1', response.purchaseable);
// Populate the expiry date
if (response.default_expiry <= 0) {
// No expiry date
clearField('expiry_date');
} else {
var expiry = moment().add(response.default_expiry, 'days');
setFieldValue('expiry_date', expiry.format('YYYY-MM-DD'));
}
}
}
);
}
},
];
launchModalForm('{% url "stock-item-create" %}', options);
}
function loadInstalledInTable(table, options) {
/*
* Display a table showing the stock items which are installed in this stock item.

View File

@ -103,6 +103,10 @@ function getAvailableTableFilters(tableKey) {
title: '{% trans "Include subcategories" %}',
description: '{% trans "Include subcategories" %}',
},
starred: {
type: 'bool',
title: '{% trans "Subscribed" %}',
},
};
}
@ -368,7 +372,7 @@ function getAvailableTableFilters(tableKey) {
},
starred: {
type: 'bool',
title: '{% trans "Starred" %}',
title: '{% trans "Subscribed" %}',
},
salable: {
type: 'bool',

View File

@ -1,18 +0,0 @@
<div class='notification-area'>
<div class="alert alert-success alert-dismissable" id="alert-success">
<a href="#" class="close" data-bs-dismiss="alert" aria-label="close">&times;</a>
<div class='alert-msg'>Success alert</div>
</div>
<div class='alert alert-info alert-dismissable' id='alert-info'>
<a href="#" class="close" data-bs-dismiss="alert" aria-label="close">&times;</a>
<div class='alert-msg'>Info alert</div>
</div>
<div class='alert alert-warning alert-dismissable' id='alert-warning'>
<a href="#" class="close" data-bs-dismiss="alert" aria-label="close">&times;</a>
<div class='alert-msg'>Warning alert</div>
</div>
<div class='alert alert-danger alert-dismissable' id='alert-danger'>
<a href="#" class="close" data-bs-dismiss="alert" aria-label="close">&times;</a>
<div class='alert-msg'>Danger alert</div>
</div>
</div>

View File

@ -10,17 +10,10 @@
<div id='{{ prefix }}button-toolbar'>
<div class='button-toolbar container-fluid' style='float: right;'>
<div class='btn-group'>
<div class='btn-group' role='group'>
<button class='btn btn-outline-secondary' id='stock-export' title='{% trans "Export Stock Information" %}'>
<span class='fas fa-download'></span>
</button>
{% if owner_control.value == "True" and user in owners or user.is_superuser or owner_control.value == "False" %}
{% if not read_only and not prevent_new_stock and roles.stock.add %}
<button class="btn btn-success" id='item-create' title='{% trans "New Stock Item" %}'>
<span class='fas fa-plus-circle'></span>
</button>
{% endif %}
{% if barcodes %}
<!-- Barcode actions menu -->
<div class='btn-group' role='group'>
@ -46,7 +39,7 @@
</div>
{% if not read_only %}
{% if roles.stock.change or roles.stock.delete %}
<div class="btn-group">
<div class="btn-group" role="group">
<button id='stock-options' class="btn btn-primary dropdown-toggle" type="button" data-bs-toggle="dropdown" title='{% trans "Stock Options" %}'>
<span class='fas fa-boxes'></span> <span class="caret"></span>
</button>
@ -66,7 +59,6 @@
</div>
{% endif %}
{% endif %}
{% endif %}
{% include "filter_list.html" with id="stock" %}
</div>
</div>