Merge pull request #1242 from SchrodingersGat/batch-reports

Batch reports
This commit is contained in:
Oliver 2021-01-18 23:48:19 +11:00 committed by GitHub
commit ce28b84f34
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 1236 additions and 725 deletions

View File

@ -29,6 +29,7 @@ from stock.api import stock_api_urls
from build.api import build_api_urls
from order.api import order_api_urls
from label.api import label_api_urls
from report.api import report_api_urls
from django.conf import settings
from django.conf.urls.static import static
@ -60,6 +61,7 @@ apipatterns = [
url(r'^build/', include(build_api_urls)),
url(r'^order/', include(order_api_urls)),
url(r'^label/', include(label_api_urls)),
url(r'^report/', include(report_api_urls)),
# User URLs
url(r'^user/', include(user_urls)),
@ -101,6 +103,7 @@ dynamic_javascript_urls = [
url(r'^order.js', DynamicJsView.as_view(template_name='js/order.js'), name='order.js'),
url(r'^part.js', DynamicJsView.as_view(template_name='js/part.js'), name='part.js'),
url(r'^label.js', DynamicJsView.as_view(template_name='js/label.js'), name='label.js'),
url(r'^report.js', DynamicJsView.as_view(template_name='js/report.js'), name='report.js'),
url(r'^stock.js', DynamicJsView.as_view(template_name='js/stock.js'), name='stock.js'),
url(r'^table_filters.js', DynamicJsView.as_view(template_name='js/table_filters.js'), name='table_filters.js'),
]

View File

@ -310,7 +310,7 @@ class StockLocationLabelPrint(generics.RetrieveAPIView, StockLocationLabelMixin)
"""
queryset = StockLocationLabel.objects.all()
seiralizers_class = StockLocationLabelSerializer
seiralizer_class = StockLocationLabelSerializer
def get(self, request, *args, **kwargs):

View File

@ -0,0 +1,70 @@
# Tests for labels
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from rest_framework.test import APITestCase
from django.urls import reverse
from django.contrib.auth import get_user_model
class TestReportTests(APITestCase):
"""
Tests for the StockItem TestReport templates
"""
fixtures = [
'category',
'part',
'location',
'stock',
]
list_url = reverse('api-stockitem-testreport-list')
def setUp(self):
user = get_user_model()
self.user = user.objects.create_user('testuser', 'test@testing.com', 'password')
self.user.is_staff = True
self.user.save()
self.client.login(username='testuser', password='password')
def do_list(self, filters={}):
response = self.client.get(self.list_url, filters, format='json')
self.assertEqual(response.status_code, 200)
return response.data
def test_list(self):
response = self.do_list()
# TODO - Add some report templates to the fixtures
self.assertEqual(len(response), 0)
# TODO - Add some tests to this response
response = self.do_list(
{
'item': 10,
}
)
# TODO - Add some tests to this response
response = self.do_list(
{
'item': 100000,
}
)
# TODO - Add some tests to this response
response = self.do_list(
{
'items': [10, 11, 12],
}
)

View File

@ -1,4 +1,4 @@
# Tests for Part Parameters
# Tests for labels
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1856,9 +1856,6 @@ class BomItem(models.Model):
self.clean()
super().save(*args, **kwargs)
def get_absolute_url(self):
return reverse('bom-item-detail', kwargs={'pk': self.id})
# A link to the parent part
# Each part will get a reverse lookup field 'bom_items'
part = models.ForeignKey(Part, on_delete=models.CASCADE, related_name='bom_items',

View File

@ -1,10 +1,11 @@
{% extends "modal_delete_form.html" %}
{% load i18n %}
{% block pre_form_content %}
Are you sure you want to delete this BOM item?
{% trans "Are you sure you want to delete this BOM item?" %}
<br>
Deleting this entry will remove the BOM row from the following part:
{% trans "Deleting this entry will remove the BOM row from the following part" %}:
<ul class='list-group'>
<li class='list-group-item'>

View File

@ -1,18 +0,0 @@
{% extends "base.html" %}
{% block content %}
<h3>BOM Item</h3>
<table class="table table-striped">
<tr><td>Parent</td><td><a href="{% url 'part-bom' item.part.id %}">{{ item.part.full_name }}</a></td></tr>
<tr><td>Child</td><td><a href="{% url 'part-used-in' item.sub_part.id %}">{{ item.sub_part.full_name }}</a></td></tr>
<tr><td>Quantity</td><td>{{ item.quantity }}</td></tr>
</table>
<div class='container-fluid'>
<a href="{% url 'bom-item-edit' item.id %}"><button class="btn btn-info">Edit BOM item</button></a>
<a href="{% url 'bom-item-delete' item.id %}"><button class="btn btn-danger">Delete BOM item</button></a>
</div>
{% endblock %}

View File

@ -63,9 +63,9 @@
<span class='fas fa-file-download'></span> {% trans "Export" %}
</button>
{% endif %}
</div>
<div class='filter-list' id='filter-list-bom'>
<!-- Empty div (will be filled out with avilable BOM filters) -->
<div class='filter-list' id='filter-list-bom'>
<!-- Empty div (will be filled out with avilable BOM filters) -->
</div>
</div>
</div>
@ -179,8 +179,8 @@
secondary: [
{
field: 'sub_part',
label: 'New Part',
title: 'Create New Part',
label: '{% trans "New Part" %}',
title: '{% trans "Create New Part" %}',
url: "{% url 'part-create' %}",
},
]

View File

@ -49,14 +49,14 @@
{% for row in bom_rows %}
<tr {% if row.errors %} style='background: #ffeaea;'{% endif %} part-name='{{ row.part_name }}' part-description='{{ row.description }}' part-select='#select_part_{{ row.index }}'>
<td>
<button class='btn btn-default btn-remove' onClick='removeRowFromBomWizard()' id='del_row_{{ forloop.counter }}' style='display: inline; float: right;' title='Remove row'>
<button class='btn btn-default btn-remove' onClick='removeRowFromBomWizard()' id='del_row_{{ forloop.counter }}' style='display: inline; float: right;' title='{% trans "Remove row" %}'>
<span row_id='{{ forloop.counter }}' class='fas fa-trash-alt icon-red'></span>
</button>
</td>
<td></td>
<td>{% add row.index 1 %}</td>
<td>
<button class='btn btn-default btn-create' onClick='newPartFromBomWizard()' id='new_part_row_{{ row.index }}' title='Create new part' type='button'>
<button class='btn btn-default btn-create' onClick='newPartFromBomWizard()' id='new_part_row_{{ row.index }}' title='{% trans "Create new part" %}' type='button'>
<span row_id='{{ row.index }}' class='fas fa-plus icon-green'/>
</button>
<select class='select bomselect' id='select_part_{{ row.index }}' name='part_{{ row.index }}'>

View File

@ -126,9 +126,9 @@
<li><a href='#' id='multi-part-export' title='{% trans "Export" %}'>{% trans "Export Data" %}</a></li>
</ul>
</div>
</div>
<div class='filter-list' id='filter-list-parts'>
<!-- Empty div -->
<div class='filter-list' id='filter-list-parts'>
<!-- Empty div -->
</div>
</div>
</div>

View File

@ -1,4 +1,5 @@
{% extends "modal_form.html" %}
{% load i18n %}
{% block pre_form_content %}
@ -10,8 +11,8 @@
</div>
{% if matches %}
<b>Possible Matching Parts</b>
<p>The new part may be a duplicate of these existing parts:</p>
<b>{% trans "Possible Matching Parts" %}</b>
<p>{% trans "The new part may be a duplicate of these existing parts" %}:</p>
<ul class='list-group'>
{% for match in matches %}
<li class='list-group-item list-group-item-condensed'>

View File

@ -1,12 +1,13 @@
{% extends "modal_form.html" %}
{% load i18n %}
{% block pre_form_content %}
{{ block.super }}
<div class='alert alert-info alert-block'>
<b>Create new part variant</b><br>
Create a new variant of template <i>'{{ part.full_name }}'</i>.
<b>{% trans "Create new part variant" %}</b><br>
{% trans "Create a new variant of template" %} <i>'{{ part.full_name }}'</i>.
</div>
{% endblock %}

View File

@ -99,8 +99,6 @@ part_category_urls = [
part_bom_urls = [
url(r'^edit/?', views.BomItemEdit.as_view(), name='bom-item-edit'),
url('^delete/?', views.BomItemDelete.as_view(), name='bom-item-delete'),
url(r'^.*$', views.BomItemDetail.as_view(), name='bom-item-detail'),
]
# URL list for part web interface

View File

@ -2411,15 +2411,6 @@ class CategoryParameterTemplateDelete(AjaxDeleteView):
return self.object
class BomItemDetail(InvenTreeRoleMixin, DetailView):
""" Detail view for BomItem """
context_object_name = 'item'
queryset = BomItem.objects.all()
template_name = 'part/bom-detail.html'
role_required = 'part.view'
class BomItemCreate(AjaxCreateView):
""" Create view for making a new BomItem object """
model = BomItem

208
InvenTree/report/api.py Normal file
View File

@ -0,0 +1,208 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.utils.translation import ugettext as _
from django.conf.urls import url, include
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import generics, filters
from rest_framework.response import Response
import InvenTree.helpers
from stock.models import StockItem
from .models import TestReport
from .serializers import TestReportSerializer
class ReportListView(generics.ListAPIView):
"""
Generic API class for report templates
"""
filter_backends = [
DjangoFilterBackend,
filters.SearchFilter,
]
filter_fields = [
'enabled',
]
search_fields = [
'name',
'description',
]
class StockItemReportMixin:
"""
Mixin for extracting stock items from query params
"""
def get_items(self):
"""
Return a list of requested stock items
"""
items = []
params = self.request.query_params
if 'items[]' in params:
items = params.getlist('items[]', [])
elif 'item' in params:
items = [params.get('item', None)]
if type(items) not in [list, tuple]:
item = [items]
valid_ids = []
for item in items:
try:
valid_ids.append(int(item))
except (ValueError):
pass
# List of StockItems which match provided values
valid_items = StockItem.objects.filter(pk__in=valid_ids)
return valid_items
class StockItemTestReportList(ReportListView, StockItemReportMixin):
"""
API endpoint for viewing list of TestReport objects.
Filterable by:
- enabled: Filter by enabled / disabled status
- item: Filter by single stock item
- items: Filter by list of stock items
"""
queryset = TestReport.objects.all()
serializer_class = TestReportSerializer
def filter_queryset(self, queryset):
queryset = super().filter_queryset(queryset)
# List of StockItem objects to match against
items = self.get_items()
if len(items) > 0:
"""
We wish to filter by stock items.
We need to compare the 'filters' string of each report,
and see if it matches against each of the specified stock items.
TODO: In the future, perhaps there is a way to make this more efficient.
"""
valid_report_ids = set()
for report in queryset.all():
matches = True
# Filter string defined for the report object
filters = InvenTree.helpers.validateFilterString(report.filters)
for item in items:
item_query = StockItem.objects.filter(pk=item.pk)
if not item_query.filter(**filters).exists():
matches = False
break
if matches:
valid_report_ids.add(report.pk)
else:
continue
# Reduce queryset to only valid matches
queryset = queryset.filter(pk__in=[pk for pk in valid_report_ids])
return queryset
class StockItemTestReportDetail(generics.RetrieveUpdateDestroyAPIView):
"""
API endpoint for a single TestReport object
"""
queryset = TestReport.objects.all()
serializer_class = TestReportSerializer
class StockItemTestReportPrint(generics.RetrieveAPIView, StockItemReportMixin):
"""
API endpoint for printing a TestReport object
"""
queryset = TestReport.objects.all()
serializer_class = TestReportSerializer
def get(self, request, *args, **kwargs):
"""
Check if valid stock item(s) have been provided.
"""
items = self.get_items()
if len(items) == 0:
# No valid items provided, return an error message
data = {
'error': _('Must provide valid StockItem(s)')
}
return Response(data, status=400)
outputs = []
# Merge one or more PDF files into a single download
for item in items:
report = self.get_object()
report.stock_item = item
outputs.append(report.render(request))
pages = []
if len(outputs) > 1:
# If more than one output is generated, merge them into a single file
for output in outputs:
doc = output.get_document()
for page in doc.pages:
pages.append(page)
pdf = outputs[0].get_document().copy(pages).write_pdf()
else:
pdf = outputs[0].get_document().write_pdf()
return InvenTree.helpers.DownloadFile(
pdf,
'test_report.pdf',
content_type='application/pdf'
)
report_api_urls = [
# Stock item test reports
url(r'test/', include([
# Detail views
url(r'^(?P<pk>\d+)/', include([
url(r'print/?', StockItemTestReportPrint.as_view(), name='api-stockitem-testreport-print'),
url(r'^.*$', StockItemTestReportDetail.as_view(), name='api-stockitem-testreport-detail'),
])),
# List view
url(r'^.*$', StockItemTestReportList.as_view(), name='api-stockitem-testreport-list'),
])),
]

View File

@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from InvenTree.serializers import InvenTreeModelSerializer
from InvenTree.serializers import InvenTreeAttachmentSerializerField
from .models import TestReport
class TestReportSerializer(InvenTreeModelSerializer):
template = InvenTreeAttachmentSerializerField(required=True)
class Meta:
model = TestReport
fields = [
'pk',
'name',
'description',
'template',
'filters',
'enabled',
]

View File

@ -137,7 +137,7 @@ InvenTree | {% trans "Stock Item" %} - {{ item }}
<!-- Document / label menu -->
{% if item.has_labels or item.has_test_reports %}
<div class='btn-group'>
<button id='document-options' title='{% trans "Document actions" %}' class='btn btn-default dropdown-toggle' type='button' data-toggle='dropdown'><span class='fas fa-file-alt'></span> <span class='caret'></span></button>
<button id='document-options' title='{% trans "Printing actions" %}' class='btn btn-default dropdown-toggle' type='button' data-toggle='dropdown'><span class='fas fa-print'></span> <span class='caret'></span></button>
<ul class='dropdown-menu' role='menu'>
{% if item.has_labels %}
<li><a href='#' id='print-label'><span class='fas fa-tag'></span> {% trans "Print Label" %}</a></li>
@ -414,12 +414,7 @@ $('#stock-uninstall').click(function() {
});
$("#stock-test-report").click(function() {
launchModalForm(
"{% url 'stock-item-test-report-select' item.id %}",
{
follow: true,
}
);
printTestReports([{{ item.pk }}]);
});
$("#print-label").click(function() {

View File

@ -50,14 +50,9 @@ function reloadTable() {
//$("#test-result-table").bootstrapTable("refresh");
}
{% if item.part.has_test_report_templates %}
{% if item.has_test_reports %}
$("#test-report").click(function() {
launchModalForm(
"{% url 'stock-item-test-report-select' item.id %}",
{
follow: true,
}
);
printTestReports([{{ item.pk }}]);
});
{% endif %}

View File

@ -29,8 +29,6 @@ stock_item_detail_urls = [
url(r'^add_tracking/', views.StockItemTrackingCreate.as_view(), name='stock-tracking-create'),
url(r'^test-report-select/', views.StockItemTestReportSelect.as_view(), name='stock-item-test-report-select'),
url(r'^test/', views.StockItemDetail.as_view(template_name='stock/item_tests.html'), name='stock-item-test-results'),
url(r'^children/', views.StockItemDetail.as_view(template_name='stock/item_childs.html'), name='stock-item-children'),
url(r'^attachments/', views.StockItemDetail.as_view(template_name='stock/item_attachments.html'), name='stock-item-attachments'),
@ -62,8 +60,6 @@ stock_urls = [
url(r'^item/uninstall/', views.StockItemUninstall.as_view(), name='stock-item-uninstall'),
url(r'^item/test-report-download/', views.StockItemTestReportDownload.as_view(), name='stock-item-test-report-download'),
# URLs for StockItem attachments
url(r'^item/attachment/', include([
url(r'^new/', views.StockItemAttachmentCreate.as_view(), name='stock-item-attachment-create'),

View File

@ -32,7 +32,6 @@ from datetime import datetime, timedelta
from company.models import Company, SupplierPart
from part.models import Part
from report.models import TestReport
from .models import StockItem, StockLocation, StockItemTracking, StockItemAttachment, StockItemTestResult
import common.settings
@ -512,92 +511,6 @@ class StockItemTestResultDelete(AjaxDeleteView):
role_required = 'stock.delete'
class StockItemTestReportSelect(AjaxView):
"""
View for selecting a TestReport template,
and generating a TestReport as a PDF.
"""
model = StockItem
ajax_form_title = _("Select Test Report Template")
role_required = 'stock.view'
def get_form(self):
stock_item = StockItem.objects.get(pk=self.kwargs['pk'])
form = StockForms.TestReportFormatForm(stock_item)
return form
def get_initial(self):
initials = super().get_initial()
form = self.get_form()
options = form.fields['template'].queryset
# If only a single template is available, pre-select it
if options.count() == 1:
initials['template'] = options[0]
return initials
def post(self, request, *args, **kwargs):
template_id = request.POST.get('template', None)
try:
template = TestReport.objects.get(pk=template_id)
except (ValueError, TestReport.DoesNoteExist):
raise ValidationError({'template': _("Select valid template")})
stock_item = StockItem.objects.get(pk=self.kwargs['pk'])
url = reverse('stock-item-test-report-download')
url += '?stock_item={id}'.format(id=stock_item.pk)
url += '&template={id}'.format(id=template.pk)
data = {
'form_valid': True,
'url': url,
}
return self.renderJsonResponse(request, self.get_form(), data=data)
class StockItemTestReportDownload(AjaxView):
"""
Download a TestReport against a StockItem.
Requires the following arguments to be passed as URL params:
stock_item - Valid PK of a StockItem object
template - Valid PK of a TestReport template object
"""
role_required = 'stock.view'
def get(self, request, *args, **kwargs):
template = request.GET.get('template', None)
stock_item = request.GET.get('stock_item', None)
try:
template = TestReport.objects.get(pk=template)
except (ValueError, TestReport.DoesNotExist):
raise ValidationError({'template': 'Invalid template ID'})
try:
stock_item = StockItem.objects.get(pk=stock_item)
except (ValueError, StockItem.DoesNotExist):
raise ValidationError({'stock_item': 'Invalid StockItem ID'})
template.stock_item = stock_item
return template.render(request)
class StockExportOptions(AjaxView):
""" Form for selecting StockExport options """

View File

@ -121,6 +121,7 @@ InvenTree
<script type='text/javascript' src="{% url 'part.js' %}"></script>
<script type='text/javascript' src="{% url 'modals.js' %}"></script>
<script type='text/javascript' src="{% url 'label.js' %}"></script>
<script type='text/javascript' src="{% url 'report.js' %}"></script>
<script type='text/javascript' src="{% url 'stock.js' %}"></script>
<script type='text/javascript' src="{% url 'build.js' %}"></script>
<script type='text/javascript' src="{% url 'order.js' %}"></script>

View File

@ -214,47 +214,45 @@ function loadBomTable(table, options) {
},
});
if (!options.editable) {
cols.push(
{
field: 'sub_part_detail.stock',
title: '{% trans "Available" %}',
searchable: false,
sortable: true,
formatter: function(value, row, index, field) {
cols.push(
{
field: 'sub_part_detail.stock',
title: '{% trans "Available" %}',
searchable: false,
sortable: true,
formatter: function(value, row, index, field) {
var url = `/part/${row.sub_part_detail.pk}/stock/`;
var text = value;
var url = `/part/${row.sub_part_detail.pk}/stock/`;
var text = value;
if (value == null || value <= 0) {
text = `<span class='label label-warning'>{% trans "No Stock" %}</span>`;
}
return renderLink(text, url);
if (value == null || value <= 0) {
text = `<span class='label label-warning'>{% trans "No Stock" %}</span>`;
}
});
/*
return renderLink(text, url);
}
});
// TODO - Re-introduce the pricing column at a later stage,
// once the pricing has been "fixed"
// O.W. 2020-11-24
/*
cols.push(
{
field: 'price_range',
title: '{% trans "Price" %}',
sortable: true,
formatter: function(value, row, index, field) {
if (value) {
return value;
} else {
return "<span class='warning-msg'>{% trans "No pricing available" %}</span>";
}
// TODO - Re-introduce the pricing column at a later stage,
// once the pricing has been "fixed"
// O.W. 2020-11-24
cols.push(
{
field: 'price_range',
title: '{% trans "Price" %}',
sortable: true,
formatter: function(value, row, index, field) {
if (value) {
return value;
} else {
return "<span class='warning-msg'>{% trans "No pricing available" %}</span>";
}
});
*/
}
}
});
*/
cols.push(
{

View File

@ -133,8 +133,17 @@ function selectLabel(labels, items, options={}) {
);
// Construct form
var html = `
var html = '';
if (items.length > 0) {
html += `
<div class='alert alert-block alert-info'>
${items.length} {% trans "stock items selected" %}
</div>`;
}
html += `
<form method='post' action='' class='js-modal-form' enctype='multipart/form-data'>
<div class='form-group'>
<label class='control-label requiredField' for='id_label'>
@ -170,4 +179,4 @@ function selectLabel(labels, items, options={}) {
options.success(pk);
}
});
}
}

View File

@ -0,0 +1,133 @@
{% load i18n %}
function selectTestReport(reports, items, options={}) {
/**
* Present the user with the available test reports,
* and allow them to select which test report to print.
*
* The intent is that the available report templates have been requested
* (via AJAX) from the server.
*/
var modal = options.modal || '#modal-form';
var report_list = makeOptionsList(
reports,
function(item) {
var text = item.name;
if (item.description) {
text += ` - ${item.description}`;
}
return text;
},
function(item) {
return item.pk;
}
);
// Construct form
var html = '';
if (items.length > 0) {
html += `
<div class='alert alert-block alert-info'>
${items.length} {% trans "stock items selected" %}
</div>`;
}
html += `
<form method='post' action='' class='js-modal-form' enctype='multipart/form-data'>
<div class='form-group'>
<label class='control-label requiredField' for='id_report'>
{% trans "Select Label" %}
</label>
<div class='controls'>
<select id='id_report' class='select form-control name='report'>
${report_list}
</select>
</div>
</div>
</form>`;
openModal({
modal: modal,
});
modalEnable(modal, true);
modalSetTitle(modal, '{% trans "Select Test Report Template" %}');
modalSetContent(modal, html);
attachSelect(modal);
modalSubmit(modal, function() {
var label = $(modal).find('#id_report');
var pk = label.val();
closeModal(modal);
if (options.success) {
options.success(pk);
}
});
}
function printTestReports(items, options={}) {
/**
* Print test reports for the provided stock item(s)
*/
if (items.length == 0) {
showAlertDialog(
'{% trans "Select Stock Items" %}',
'{% trans "Stock item(s) must be selected before printing reports" %}'
);
return;
}
// Request available labels from the server
inventreeGet(
'{% url "api-stockitem-testreport-list" %}',
{
enabled: true,
items: items,
},
{
success: function(response) {
if (response.length == 0) {
showAlertDialog(
'{% trans "No Reports Found" %}',
'{% trans "No report templates found which match selected stock item(s)" %}',
);
return;
}
// Select report template to print
selectTestReport(
response,
items,
{
success: function(pk) {
var href = `/api/report/test/${pk}/print/?`;
items.forEach(function(item) {
href += `items[]=${item}&`;
});
window.location.href = href;
}
}
);
}
}
);
}

View File

@ -625,9 +625,19 @@ function loadStockTable(table, options) {
],
});
/*
if (options.buttons) {
linkButtonsToSelection(table, options.buttons);
}
*/
linkButtonsToSelection(
table,
[
'#stock-print-options',
'#stock-options',
]
);
function stockAdjustment(action) {
var items = $("#stock-table").bootstrapTable("getSelections");
@ -665,6 +675,7 @@ function loadStockTable(table, options) {
}
// Automatically link button callbacks
$('#multi-item-print-label').click(function() {
var selections = $('#stock-table').bootstrapTable('getSelections');
@ -677,6 +688,18 @@ function loadStockTable(table, options) {
printStockItemLabels(items);
});
$('#multi-item-print-test-report').click(function() {
var selections = $('#stock-table').bootstrapTable('getSelections');
var items = [];
selections.forEach(function(item) {
items.push(item.pk);
});
printTestReports(items);
})
$('#multi-item-stocktake').click(function() {
stockAdjustment('count');
});

View File

@ -10,44 +10,48 @@
<div class='button-toolbar container-fluid' style='float: right;'>
<div class='btn-group'>
<button class='btn btn-default' id='stock-export' title='{% trans "Export Stock Information" %}'>
<span class='fas fa-file-download'></span> {% trans "Export" %}
<span class='fas fa-download'></span>
</button>
{% if read_only %}
{% else %}
<!-- Check permissions and owner -->
{% if owner_control.value == "True" and user in owners or user.is_superuser or owner_control.value == "False" %}
{% if roles.stock.add %}
<button class="btn btn-success" id='item-create'>
<span class='fas fa-plus-circle'></span> {% trans "New Stock Item" %}
</button>
{% if owner_control.value == "True" and user in owners or user.is_superuser or owner_control.value == "False" %}
{% if 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 %}
<div class='btn-group'>
<button id='stock-print-options' class='btn btn-primary dropdown-toggle' type='button' data-toggle="dropdown" title='{% trans "Printing Actions" %}'>
<span class='fas fa-print'></span> <span class='caret'></span>
</button>
<ul class='dropdown-menu'>
<li><a href='#' id='multi-item-print-label' title='{% trans "Print labels" %}'><span class='fas fa-tags'></span> {% trans "Print labels" %}</a></li>
<li><a href='#' id='multi-item-print-test-report' title='{% trans "Print test reports" %}'><span class='fas fa-file-pdf'></span> {% trans "Print test reports" %}</a></li>
</ul>
</div>
{% if roles.stock.change or roles.stock.delete %}
<div class="btn-group">
<button id='stock-options' class="btn btn-primary dropdown-toggle" type="button" data-toggle="dropdown" title='{% trans "Stock Options" %}'>
<span class='fas fa-boxes'></span> <span class="caret"></span>
</button>
<ul class="dropdown-menu">
{% if roles.stock.change %}
<li><a href="#" id='multi-item-add' title='{% trans "Add to selected stock items" %}'><span class='fas fa-plus-circle'></span> {% trans "Add stock" %}</a></li>
<li><a href="#" id='multi-item-remove' title='{% trans "Remove from selected stock items" %}'><span class='fas fa-minus-circle'></span> {% trans "Remove stock" %}</a></li>
<li><a href="#" id='multi-item-stocktake' title='{% trans "Stocktake selected stock items" %}'><span class='fas fa-check-circle'></span> {% trans "Count stock" %}</a></li>
<li><a href='#' id='multi-item-move' title='{% trans "Move selected stock items" %}'><span class='fas fa-exchange-alt'></span> {% trans "Move stock" %}</a></li>
<li><a href='#' id='multi-item-order' title='{% trans "Order selected items" %}'><span class='fas fa-shopping-cart'></span> {% trans "Order stock" %}</a></li>
<li><a href='#' id='multi-item-set-status' title='{% trans "Change status" %}'><span class='fas fa-exclamation-circle'></span> {% trans "Change stock status" %}</a></li>
{% endif %}
{% if roles.stock.delete %}
<li><a href='#' id='multi-item-delete' title='{% trans "Delete selected items" %}'><span class='fas fa-trash-alt'></span> {% trans "Delete Stock" %}</a></li>
{% endif %}
{% endif %}
{% if roles.stock.change or roles.stock.delete %}
<div class="btn-group">
<button id='stock-options' class="btn btn-primary dropdown-toggle" type="button" data-toggle="dropdown">{% trans "Options" %}<span class="caret"></span></button>
<ul class="dropdown-menu">
<li><a href='#' id='multi-item-print-label' title='{% trans "Print labels" %}'><span class='fas fa-tags'></span> {% trans "Print labels" %}</a></li>
<!-- Check permissions and owner -->
{% if owner_control.value == "True" and user in owners or user.is_superuser or owner_control.value == "False" %}
{% if roles.stock.change %}
<li><a href="#" id='multi-item-add' title='{% trans "Add to selected stock items" %}'><span class='fas fa-plus-circle'></span> {% trans "Add stock" %}</a></li>
<li><a href="#" id='multi-item-remove' title='{% trans "Remove from selected stock items" %}'><span class='fas fa-minus-circle'></span> {% trans "Remove stock" %}</a></li>
<li><a href="#" id='multi-item-stocktake' title='{% trans "Stocktake selected stock items" %}'><span class='fas fa-check-circle'></span> {% trans "Count stock" %}</a></li>
<li><a href='#' id='multi-item-move' title='{% trans "Move selected stock items" %}'><span class='fas fa-exchange-alt'></span> {% trans "Move stock" %}</a></li>
<li><a href='#' id='multi-item-order' title='{% trans "Order selected items" %}'><span class='fas fa-shopping-cart'></span> {% trans "Order stock" %}</a></li>
<li><a href='#' id='multi-item-set-status' title='{% trans "Change status" %}'><span class='fas fa-exclamation-circle'></span> {% trans "Change stock status" %}</a></li>
{% endif %}
{% if roles.stock.delete %}
<li><a href='#' id='multi-item-delete' title='{% trans "Delete selected items" %}'><span class='fas fa-trash-alt'></span> {% trans "Delete Stock" %}</a></li>
{% endif %}
{% endif %}
</ul>
</div>
{% endif %}
{% endif %}
</div>
<div class='filter-list' id='filter-list-stock'>
<!-- An empty div in which the filter list will be constructed -->
{% endif %}
<div class='filter-list' id='filter-list-stock'>
<!-- An empty div in which the filter list will be constructed -->
</div>
</div>
</div>
</div>