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 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'),
|
||||
]
|
||||
|
@ -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):
|
||||
|
||||
|
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 -*-
|
||||
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()
|
||||
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',
|
||||
|
@ -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'>
|
||||
|
@ -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" %}
|
||||
</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' %}",
|
||||
},
|
||||
]
|
||||
|
@ -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 }}'>
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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'>
|
||||
|
@ -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 %}
|
@ -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
|
||||
|
@ -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
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 -->
|
||||
{% 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() {
|
||||
|
@ -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 %}
|
||||
|
||||
|
@ -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'),
|
||||
|
@ -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 """
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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(
|
||||
{
|
||||
|
@ -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'>
|
||||
|
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) {
|
||||
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');
|
||||
});
|
||||
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user