mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge pull request #1242 from SchrodingersGat/batch-reports
Batch reports
This commit is contained in:
commit
ce28b84f34
@ -29,6 +29,7 @@ from stock.api import stock_api_urls
|
|||||||
from build.api import build_api_urls
|
from build.api import build_api_urls
|
||||||
from order.api import order_api_urls
|
from order.api import order_api_urls
|
||||||
from label.api import label_api_urls
|
from label.api import label_api_urls
|
||||||
|
from report.api import report_api_urls
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.conf.urls.static import static
|
from django.conf.urls.static import static
|
||||||
@ -60,6 +61,7 @@ apipatterns = [
|
|||||||
url(r'^build/', include(build_api_urls)),
|
url(r'^build/', include(build_api_urls)),
|
||||||
url(r'^order/', include(order_api_urls)),
|
url(r'^order/', include(order_api_urls)),
|
||||||
url(r'^label/', include(label_api_urls)),
|
url(r'^label/', include(label_api_urls)),
|
||||||
|
url(r'^report/', include(report_api_urls)),
|
||||||
|
|
||||||
# User URLs
|
# User URLs
|
||||||
url(r'^user/', include(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'^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'^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'^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'^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'),
|
url(r'^table_filters.js', DynamicJsView.as_view(template_name='js/table_filters.js'), name='table_filters.js'),
|
||||||
]
|
]
|
||||||
|
@ -310,7 +310,7 @@ class StockLocationLabelPrint(generics.RetrieveAPIView, StockLocationLabelMixin)
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
queryset = StockLocationLabel.objects.all()
|
queryset = StockLocationLabel.objects.all()
|
||||||
seiralizers_class = StockLocationLabelSerializer
|
seiralizer_class = StockLocationLabelSerializer
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
|
|
||||||
|
70
InvenTree/label/test_api.py
Normal file
70
InvenTree/label/test_api.py
Normal 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],
|
||||||
|
}
|
||||||
|
)
|
@ -1,4 +1,4 @@
|
|||||||
# Tests for Part Parameters
|
# Tests for labels
|
||||||
|
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from __future__ import unicode_literals
|
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
@ -1856,9 +1856,6 @@ class BomItem(models.Model):
|
|||||||
self.clean()
|
self.clean()
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
def get_absolute_url(self):
|
|
||||||
return reverse('bom-item-detail', kwargs={'pk': self.id})
|
|
||||||
|
|
||||||
# A link to the parent part
|
# A link to the parent part
|
||||||
# Each part will get a reverse lookup field 'bom_items'
|
# Each part will get a reverse lookup field 'bom_items'
|
||||||
part = models.ForeignKey(Part, on_delete=models.CASCADE, related_name='bom_items',
|
part = models.ForeignKey(Part, on_delete=models.CASCADE, related_name='bom_items',
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
{% extends "modal_delete_form.html" %}
|
{% extends "modal_delete_form.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
{% block pre_form_content %}
|
{% 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>
|
<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'>
|
<ul class='list-group'>
|
||||||
<li class='list-group-item'>
|
<li class='list-group-item'>
|
||||||
|
@ -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 %}
|
|
@ -63,9 +63,9 @@
|
|||||||
<span class='fas fa-file-download'></span> {% trans "Export" %}
|
<span class='fas fa-file-download'></span> {% trans "Export" %}
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
<div class='filter-list' id='filter-list-bom'>
|
||||||
<div class='filter-list' id='filter-list-bom'>
|
<!-- Empty div (will be filled out with avilable BOM filters) -->
|
||||||
<!-- Empty div (will be filled out with avilable BOM filters) -->
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -179,8 +179,8 @@
|
|||||||
secondary: [
|
secondary: [
|
||||||
{
|
{
|
||||||
field: 'sub_part',
|
field: 'sub_part',
|
||||||
label: 'New Part',
|
label: '{% trans "New Part" %}',
|
||||||
title: 'Create New Part',
|
title: '{% trans "Create New Part" %}',
|
||||||
url: "{% url 'part-create' %}",
|
url: "{% url 'part-create' %}",
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
@ -49,14 +49,14 @@
|
|||||||
{% for row in bom_rows %}
|
{% 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 }}'>
|
<tr {% if row.errors %} style='background: #ffeaea;'{% endif %} part-name='{{ row.part_name }}' part-description='{{ row.description }}' part-select='#select_part_{{ row.index }}'>
|
||||||
<td>
|
<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>
|
<span row_id='{{ forloop.counter }}' class='fas fa-trash-alt icon-red'></span>
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
<td></td>
|
<td></td>
|
||||||
<td>{% add row.index 1 %}</td>
|
<td>{% add row.index 1 %}</td>
|
||||||
<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'/>
|
<span row_id='{{ row.index }}' class='fas fa-plus icon-green'/>
|
||||||
</button>
|
</button>
|
||||||
<select class='select bomselect' id='select_part_{{ row.index }}' name='part_{{ row.index }}'>
|
<select class='select bomselect' id='select_part_{{ row.index }}' name='part_{{ row.index }}'>
|
||||||
|
@ -126,9 +126,9 @@
|
|||||||
<li><a href='#' id='multi-part-export' title='{% trans "Export" %}'>{% trans "Export Data" %}</a></li>
|
<li><a href='#' id='multi-part-export' title='{% trans "Export" %}'>{% trans "Export Data" %}</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class='filter-list' id='filter-list-parts'>
|
||||||
<div class='filter-list' id='filter-list-parts'>
|
<!-- Empty div -->
|
||||||
<!-- Empty div -->
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
{% extends "modal_form.html" %}
|
{% extends "modal_form.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
{% block pre_form_content %}
|
{% block pre_form_content %}
|
||||||
|
|
||||||
@ -10,8 +11,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if matches %}
|
{% if matches %}
|
||||||
<b>Possible Matching Parts</b>
|
<b>{% trans "Possible Matching Parts" %}</b>
|
||||||
<p>The new part may be a duplicate of these existing parts:</p>
|
<p>{% trans "The new part may be a duplicate of these existing parts" %}:</p>
|
||||||
<ul class='list-group'>
|
<ul class='list-group'>
|
||||||
{% for match in matches %}
|
{% for match in matches %}
|
||||||
<li class='list-group-item list-group-item-condensed'>
|
<li class='list-group-item list-group-item-condensed'>
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
{% extends "modal_form.html" %}
|
{% extends "modal_form.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
{% block pre_form_content %}
|
{% block pre_form_content %}
|
||||||
|
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
|
|
||||||
<div class='alert alert-info alert-block'>
|
<div class='alert alert-info alert-block'>
|
||||||
<b>Create new part variant</b><br>
|
<b>{% trans "Create new part variant" %}</b><br>
|
||||||
Create a new variant of template <i>'{{ part.full_name }}'</i>.
|
{% trans "Create a new variant of template" %} <i>'{{ part.full_name }}'</i>.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
@ -99,8 +99,6 @@ part_category_urls = [
|
|||||||
part_bom_urls = [
|
part_bom_urls = [
|
||||||
url(r'^edit/?', views.BomItemEdit.as_view(), name='bom-item-edit'),
|
url(r'^edit/?', views.BomItemEdit.as_view(), name='bom-item-edit'),
|
||||||
url('^delete/?', views.BomItemDelete.as_view(), name='bom-item-delete'),
|
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
|
# URL list for part web interface
|
||||||
|
@ -2411,15 +2411,6 @@ class CategoryParameterTemplateDelete(AjaxDeleteView):
|
|||||||
return self.object
|
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):
|
class BomItemCreate(AjaxCreateView):
|
||||||
""" Create view for making a new BomItem object """
|
""" Create view for making a new BomItem object """
|
||||||
model = BomItem
|
model = BomItem
|
||||||
|
208
InvenTree/report/api.py
Normal file
208
InvenTree/report/api.py
Normal 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'),
|
||||||
|
])),
|
||||||
|
]
|
23
InvenTree/report/serializers.py
Normal file
23
InvenTree/report/serializers.py
Normal 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',
|
||||||
|
]
|
@ -137,7 +137,7 @@ InvenTree | {% trans "Stock Item" %} - {{ item }}
|
|||||||
<!-- Document / label menu -->
|
<!-- Document / label menu -->
|
||||||
{% if item.has_labels or item.has_test_reports %}
|
{% if item.has_labels or item.has_test_reports %}
|
||||||
<div class='btn-group'>
|
<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'>
|
<ul class='dropdown-menu' role='menu'>
|
||||||
{% if item.has_labels %}
|
{% if item.has_labels %}
|
||||||
<li><a href='#' id='print-label'><span class='fas fa-tag'></span> {% trans "Print Label" %}</a></li>
|
<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() {
|
$("#stock-test-report").click(function() {
|
||||||
launchModalForm(
|
printTestReports([{{ item.pk }}]);
|
||||||
"{% url 'stock-item-test-report-select' item.id %}",
|
|
||||||
{
|
|
||||||
follow: true,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#print-label").click(function() {
|
$("#print-label").click(function() {
|
||||||
|
@ -50,14 +50,9 @@ function reloadTable() {
|
|||||||
//$("#test-result-table").bootstrapTable("refresh");
|
//$("#test-result-table").bootstrapTable("refresh");
|
||||||
}
|
}
|
||||||
|
|
||||||
{% if item.part.has_test_report_templates %}
|
{% if item.has_test_reports %}
|
||||||
$("#test-report").click(function() {
|
$("#test-report").click(function() {
|
||||||
launchModalForm(
|
printTestReports([{{ item.pk }}]);
|
||||||
"{% url 'stock-item-test-report-select' item.id %}",
|
|
||||||
{
|
|
||||||
follow: true,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
@ -29,8 +29,6 @@ stock_item_detail_urls = [
|
|||||||
|
|
||||||
url(r'^add_tracking/', views.StockItemTrackingCreate.as_view(), name='stock-tracking-create'),
|
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'^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'^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'),
|
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/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
|
# URLs for StockItem attachments
|
||||||
url(r'^item/attachment/', include([
|
url(r'^item/attachment/', include([
|
||||||
url(r'^new/', views.StockItemAttachmentCreate.as_view(), name='stock-item-attachment-create'),
|
url(r'^new/', views.StockItemAttachmentCreate.as_view(), name='stock-item-attachment-create'),
|
||||||
|
@ -32,7 +32,6 @@ from datetime import datetime, timedelta
|
|||||||
|
|
||||||
from company.models import Company, SupplierPart
|
from company.models import Company, SupplierPart
|
||||||
from part.models import Part
|
from part.models import Part
|
||||||
from report.models import TestReport
|
|
||||||
from .models import StockItem, StockLocation, StockItemTracking, StockItemAttachment, StockItemTestResult
|
from .models import StockItem, StockLocation, StockItemTracking, StockItemAttachment, StockItemTestResult
|
||||||
|
|
||||||
import common.settings
|
import common.settings
|
||||||
@ -512,92 +511,6 @@ class StockItemTestResultDelete(AjaxDeleteView):
|
|||||||
role_required = 'stock.delete'
|
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):
|
class StockExportOptions(AjaxView):
|
||||||
""" Form for selecting StockExport options """
|
""" Form for selecting StockExport options """
|
||||||
|
|
||||||
|
@ -121,6 +121,7 @@ InvenTree
|
|||||||
<script type='text/javascript' src="{% url 'part.js' %}"></script>
|
<script type='text/javascript' src="{% url 'part.js' %}"></script>
|
||||||
<script type='text/javascript' src="{% url 'modals.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 '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 'stock.js' %}"></script>
|
||||||
<script type='text/javascript' src="{% url 'build.js' %}"></script>
|
<script type='text/javascript' src="{% url 'build.js' %}"></script>
|
||||||
<script type='text/javascript' src="{% url 'order.js' %}"></script>
|
<script type='text/javascript' src="{% url 'order.js' %}"></script>
|
||||||
|
@ -214,47 +214,45 @@ function loadBomTable(table, options) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!options.editable) {
|
cols.push(
|
||||||
cols.push(
|
{
|
||||||
{
|
field: 'sub_part_detail.stock',
|
||||||
field: 'sub_part_detail.stock',
|
title: '{% trans "Available" %}',
|
||||||
title: '{% trans "Available" %}',
|
searchable: false,
|
||||||
searchable: false,
|
sortable: true,
|
||||||
sortable: true,
|
formatter: function(value, row, index, field) {
|
||||||
formatter: function(value, row, index, field) {
|
|
||||||
|
|
||||||
var url = `/part/${row.sub_part_detail.pk}/stock/`;
|
var url = `/part/${row.sub_part_detail.pk}/stock/`;
|
||||||
var text = value;
|
var text = value;
|
||||||
|
|
||||||
if (value == null || value <= 0) {
|
if (value == null || value <= 0) {
|
||||||
text = `<span class='label label-warning'>{% trans "No Stock" %}</span>`;
|
text = `<span class='label label-warning'>{% trans "No Stock" %}</span>`;
|
||||||
}
|
|
||||||
|
|
||||||
return renderLink(text, url);
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
/*
|
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(
|
// TODO - Re-introduce the pricing column at a later stage,
|
||||||
{
|
// once the pricing has been "fixed"
|
||||||
field: 'price_range',
|
// O.W. 2020-11-24
|
||||||
title: '{% trans "Price" %}',
|
|
||||||
sortable: true,
|
cols.push(
|
||||||
formatter: function(value, row, index, field) {
|
{
|
||||||
if (value) {
|
field: 'price_range',
|
||||||
return value;
|
title: '{% trans "Price" %}',
|
||||||
} else {
|
sortable: true,
|
||||||
return "<span class='warning-msg'>{% trans "No pricing available" %}</span>";
|
formatter: function(value, row, index, field) {
|
||||||
}
|
if (value) {
|
||||||
|
return value;
|
||||||
|
} else {
|
||||||
|
return "<span class='warning-msg'>{% trans "No pricing available" %}</span>";
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
*/
|
});
|
||||||
}
|
*/
|
||||||
|
|
||||||
cols.push(
|
cols.push(
|
||||||
{
|
{
|
||||||
|
@ -133,8 +133,17 @@ function selectLabel(labels, items, options={}) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Construct form
|
// 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'>
|
<form method='post' action='' class='js-modal-form' enctype='multipart/form-data'>
|
||||||
<div class='form-group'>
|
<div class='form-group'>
|
||||||
<label class='control-label requiredField' for='id_label'>
|
<label class='control-label requiredField' for='id_label'>
|
||||||
@ -170,4 +179,4 @@ function selectLabel(labels, items, options={}) {
|
|||||||
options.success(pk);
|
options.success(pk);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
133
InvenTree/templates/js/report.js
Normal file
133
InvenTree/templates/js/report.js
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
@ -625,9 +625,19 @@ function loadStockTable(table, options) {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
if (options.buttons) {
|
if (options.buttons) {
|
||||||
linkButtonsToSelection(table, options.buttons);
|
linkButtonsToSelection(table, options.buttons);
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
linkButtonsToSelection(
|
||||||
|
table,
|
||||||
|
[
|
||||||
|
'#stock-print-options',
|
||||||
|
'#stock-options',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
function stockAdjustment(action) {
|
function stockAdjustment(action) {
|
||||||
var items = $("#stock-table").bootstrapTable("getSelections");
|
var items = $("#stock-table").bootstrapTable("getSelections");
|
||||||
@ -665,6 +675,7 @@ function loadStockTable(table, options) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Automatically link button callbacks
|
// Automatically link button callbacks
|
||||||
|
|
||||||
$('#multi-item-print-label').click(function() {
|
$('#multi-item-print-label').click(function() {
|
||||||
var selections = $('#stock-table').bootstrapTable('getSelections');
|
var selections = $('#stock-table').bootstrapTable('getSelections');
|
||||||
|
|
||||||
@ -677,6 +688,18 @@ function loadStockTable(table, options) {
|
|||||||
printStockItemLabels(items);
|
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() {
|
$('#multi-item-stocktake').click(function() {
|
||||||
stockAdjustment('count');
|
stockAdjustment('count');
|
||||||
});
|
});
|
||||||
|
@ -10,44 +10,48 @@
|
|||||||
<div class='button-toolbar container-fluid' style='float: right;'>
|
<div class='button-toolbar container-fluid' style='float: right;'>
|
||||||
<div class='btn-group'>
|
<div class='btn-group'>
|
||||||
<button class='btn btn-default' id='stock-export' title='{% trans "Export Stock Information" %}'>
|
<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>
|
</button>
|
||||||
{% if read_only %}
|
|
||||||
{% else %}
|
{% if owner_control.value == "True" and user in owners or user.is_superuser or owner_control.value == "False" %}
|
||||||
<!-- Check permissions and owner -->
|
{% if roles.stock.add %}
|
||||||
{% if owner_control.value == "True" and user in owners or user.is_superuser or owner_control.value == "False" %}
|
<button class="btn btn-success" id='item-create' title='{% trans "New Stock Item" %}'>
|
||||||
{% if roles.stock.add %}
|
<span class='fas fa-plus-circle'></span>
|
||||||
<button class="btn btn-success" id='item-create'>
|
</button>
|
||||||
<span class='fas fa-plus-circle'></span> {% trans "New Stock Item" %}
|
{% endif %}
|
||||||
</button>
|
<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 %}
|
||||||
{% 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>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
{% endif %}
|
||||||
<div class='filter-list' id='filter-list-stock'>
|
<div class='filter-list' id='filter-list-stock'>
|
||||||
<!-- An empty div in which the filter list will be constructed -->
|
<!-- An empty div in which the filter list will be constructed -->
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
Loading…
Reference in New Issue
Block a user