mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge pull request #811 from SchrodingersGat/stock-item-testing
Stock item testing
This commit is contained in:
commit
c54cb2b280
@ -81,6 +81,7 @@ dynamic_javascript_urls = [
|
||||
url(r'^order.js', DynamicJsView.as_view(template_name='js/order.html'), name='order.js'),
|
||||
url(r'^company.js', DynamicJsView.as_view(template_name='js/company.html'), name='company.js'),
|
||||
url(r'^bom.js', DynamicJsView.as_view(template_name='js/bom.html'), name='bom.js'),
|
||||
url(r'^table_filters.js', DynamicJsView.as_view(template_name='js/table_filters.html'), name='table_filters.js'),
|
||||
]
|
||||
|
||||
urlpatterns = [
|
||||
|
@ -198,8 +198,8 @@ InvenTree | Allocate Parts
|
||||
var html = `<div class='btn-group float-right' role='group'>`;
|
||||
|
||||
{% if build.status == BuildStatus.PENDING %}
|
||||
html += makeIconButton('fa-edit', 'button-allocation-edit', pk, '{% trans "Edit stock allocation" %}');
|
||||
html += makeIconButton('fa-trash-alt', 'button-allocation-delete', pk, '{% trans "Delete stock allocation" %}');
|
||||
html += makeIconButton('fa-edit icon-blue', 'button-allocation-edit', pk, '{% trans "Edit stock allocation" %}');
|
||||
html += makeIconButton('fa-trash-alt icon-red', 'button-allocation-delete', pk, '{% trans "Delete stock allocation" %}');
|
||||
{% endif %}
|
||||
|
||||
html += `</div>`;
|
||||
@ -389,7 +389,7 @@ InvenTree | Allocate Parts
|
||||
html += makeIconButton('fa-tools', 'button-build', pk, '{% trans "Build parts" %}');
|
||||
}
|
||||
|
||||
html += makeIconButton('fa-plus', 'button-add', pk, '{% trans "Allocate stock" %}');
|
||||
html += makeIconButton('fa-plus icon-green', 'button-add', pk, '{% trans "Allocate stock" %}');
|
||||
{% endif %}
|
||||
|
||||
html += '</div>';
|
||||
|
@ -208,7 +208,7 @@ $("#po-table").inventreeTable({
|
||||
var pk = row.pk;
|
||||
|
||||
{% if order.status == PurchaseOrderStatus.PENDING %}
|
||||
html += makeIconButton('fa-edit', 'button-line-edit', pk, '{% trans "Edit line item" %}');
|
||||
html += makeIconButton('fa-edit icon-blue', 'button-line-edit', pk, '{% trans "Edit line item" %}');
|
||||
html += makeIconButton('fa-trash-alt icon-red', 'button-line-delete', pk, '{% trans "Delete line item" %}');
|
||||
{% endif %}
|
||||
|
||||
|
@ -89,8 +89,8 @@ function showAllocationSubTable(index, row, element) {
|
||||
var pk = row.pk;
|
||||
|
||||
{% if order.status == SalesOrderStatus.PENDING %}
|
||||
html += makeIconButton('fa-edit', 'button-allocation-edit', pk, '{% trans "Edit stock allocation" %}');
|
||||
html += makeIconButton('fa-trash-alt', 'button-allocation-delete', pk, '{% trans "Delete stock allocation" %}');
|
||||
html += makeIconButton('fa-edit icon-blue', 'button-allocation-edit', pk, '{% trans "Edit stock allocation" %}');
|
||||
html += makeIconButton('fa-trash-alt icon-red', 'button-allocation-delete', pk, '{% trans "Delete stock allocation" %}');
|
||||
{% endif %}
|
||||
|
||||
html += "</div>";
|
||||
@ -274,11 +274,11 @@ $("#so-lines-table").inventreeTable({
|
||||
html += makeIconButton('fa-tools', 'button-build', row.part, '{% trans "Build parts" %}');
|
||||
}
|
||||
|
||||
html += makeIconButton('fa-plus', 'button-add', pk, '{% trans "Allocate parts" %}');
|
||||
html += makeIconButton('fa-plus icon-green', 'button-add', pk, '{% trans "Allocate parts" %}');
|
||||
}
|
||||
|
||||
html += makeIconButton('fa-edit', 'button-edit', pk, '{% trans "Edit line item" %}');
|
||||
html += makeIconButton('fa-trash-alt', 'button-delete', pk, '{% trans "Delete line item " %}');
|
||||
html += makeIconButton('fa-edit icon-blue', 'button-edit', pk, '{% trans "Edit line item" %}');
|
||||
html += makeIconButton('fa-trash-alt icon-red', 'button-delete', pk, '{% trans "Delete line item " %}');
|
||||
|
||||
html += `</div>`;
|
||||
|
||||
|
@ -10,6 +10,7 @@ import import_export.widgets as widgets
|
||||
|
||||
from .models import StockLocation, StockItem, StockItemAttachment
|
||||
from .models import StockItemTracking
|
||||
from .models import StockItemTestResult
|
||||
|
||||
from build.models import Build
|
||||
from company.models import SupplierPart
|
||||
@ -117,7 +118,13 @@ class StockTrackingAdmin(ImportExportModelAdmin):
|
||||
list_display = ('item', 'date', 'title')
|
||||
|
||||
|
||||
class StockItemTestResultAdmin(admin.ModelAdmin):
|
||||
|
||||
list_display = ('stock_item', 'test', 'result', 'value')
|
||||
|
||||
|
||||
admin.site.register(StockLocation, LocationAdmin)
|
||||
admin.site.register(StockItem, StockItemAdmin)
|
||||
admin.site.register(StockItemTracking, StockTrackingAdmin)
|
||||
admin.site.register(StockItemAttachment, StockAttachmentAdmin)
|
||||
admin.site.register(StockItemTestResult, StockItemTestResultAdmin)
|
||||
|
@ -15,6 +15,7 @@ from django.db.models import Q
|
||||
from .models import StockLocation, StockItem
|
||||
from .models import StockItemTracking
|
||||
from .models import StockItemAttachment
|
||||
from .models import StockItemTestResult
|
||||
|
||||
from part.models import Part, PartCategory
|
||||
from part.serializers import PartBriefSerializer
|
||||
@ -26,6 +27,7 @@ from .serializers import StockItemSerializer
|
||||
from .serializers import LocationSerializer, LocationBriefSerializer
|
||||
from .serializers import StockTrackingSerializer
|
||||
from .serializers import StockItemAttachmentSerializer
|
||||
from .serializers import StockItemTestResultSerializer
|
||||
|
||||
from InvenTree.views import TreeSerializer
|
||||
from InvenTree.helpers import str2bool, isNull
|
||||
@ -536,11 +538,10 @@ class StockList(generics.ListCreateAPIView):
|
||||
try:
|
||||
part = Part.objects.get(pk=part_id)
|
||||
|
||||
# If the part is a Template part, select stock items for any "variant" parts under that template
|
||||
if part.is_template:
|
||||
queryset = queryset.filter(part__in=[part.id for part in Part.objects.filter(variant_of=part_id)])
|
||||
else:
|
||||
queryset = queryset.filter(part=part_id)
|
||||
# Filter by any parts "under" the given part
|
||||
parts = part.get_descendants(include_self=True)
|
||||
|
||||
queryset = queryset.filter(part__in=parts)
|
||||
|
||||
except (ValueError, Part.DoesNotExist):
|
||||
raise ValidationError({"part": "Invalid Part ID specified"})
|
||||
@ -654,11 +655,89 @@ class StockAttachmentList(generics.ListCreateAPIView, AttachmentMixin):
|
||||
queryset = StockItemAttachment.objects.all()
|
||||
serializer_class = StockItemAttachmentSerializer
|
||||
|
||||
filter_backends = [
|
||||
DjangoFilterBackend,
|
||||
filters.OrderingFilter,
|
||||
filters.SearchFilter,
|
||||
]
|
||||
|
||||
filter_fields = [
|
||||
'stock_item',
|
||||
]
|
||||
|
||||
|
||||
class StockItemTestResultList(generics.ListCreateAPIView):
|
||||
"""
|
||||
API endpoint for listing (and creating) a StockItemTestResult object.
|
||||
"""
|
||||
|
||||
queryset = StockItemTestResult.objects.all()
|
||||
serializer_class = StockItemTestResultSerializer
|
||||
|
||||
permission_classes = [
|
||||
permissions.IsAuthenticated,
|
||||
]
|
||||
|
||||
filter_backends = [
|
||||
DjangoFilterBackend,
|
||||
filters.SearchFilter,
|
||||
filters.OrderingFilter,
|
||||
]
|
||||
|
||||
filter_fields = [
|
||||
'stock_item',
|
||||
'test',
|
||||
'user',
|
||||
'result',
|
||||
'value',
|
||||
]
|
||||
|
||||
def get_serializer(self, *args, **kwargs):
|
||||
try:
|
||||
kwargs['user_detail'] = str2bool(self.request.query_params.get('user_detail', False))
|
||||
except:
|
||||
pass
|
||||
|
||||
try:
|
||||
kwargs['attachment_detail'] = str2bool(self.request.query_params.get('attachment_detail', False))
|
||||
except:
|
||||
pass
|
||||
|
||||
kwargs['context'] = self.get_serializer_context()
|
||||
|
||||
return self.serializer_class(*args, **kwargs)
|
||||
|
||||
def perform_create(self, serializer):
|
||||
"""
|
||||
Create a new test result object.
|
||||
|
||||
Also, check if an attachment was uploaded alongside the test result,
|
||||
and save it to the database if it were.
|
||||
"""
|
||||
|
||||
# Capture the user information
|
||||
test_result = serializer.save()
|
||||
test_result.user = self.request.user
|
||||
|
||||
# Check if a file has been attached to the request
|
||||
attachment_file = self.request.FILES.get('attachment', None)
|
||||
|
||||
if attachment_file:
|
||||
# Create a new attachment associated with the stock item
|
||||
attachment = StockItemAttachment(
|
||||
attachment=attachment_file,
|
||||
stock_item=test_result.stock_item,
|
||||
user=test_result.user
|
||||
)
|
||||
|
||||
attachment.save()
|
||||
|
||||
# Link the attachment back to the test result
|
||||
test_result.attachment = attachment
|
||||
|
||||
test_result.save()
|
||||
|
||||
|
||||
class StockTrackingList(generics.ListCreateAPIView):
|
||||
""" API endpoint for list view of StockItemTracking objects.
|
||||
|
||||
@ -769,6 +848,11 @@ stock_api_urls = [
|
||||
url(r'^$', StockAttachmentList.as_view(), name='api-stock-attachment-list'),
|
||||
])),
|
||||
|
||||
# Base URL for StockItemTestResult API endpoints
|
||||
url(r'^test/', include([
|
||||
url(r'^$', StockItemTestResultList.as_view(), name='api-stock-test-result-list'),
|
||||
])),
|
||||
|
||||
url(r'track/?', StockTrackingList.as_view(), name='api-stock-track'),
|
||||
|
||||
url(r'^tree/?', StockCategoryTree.as_view(), name='api-stock-tree'),
|
||||
|
@ -70,6 +70,18 @@
|
||||
lft: 0
|
||||
rght: 0
|
||||
|
||||
- model: stock.stockitem
|
||||
pk: 105
|
||||
fields:
|
||||
part: 25
|
||||
location: 7
|
||||
quantity: 1
|
||||
serial: 1000
|
||||
level: 0
|
||||
tree_id: 0
|
||||
lft: 0
|
||||
rght: 0
|
||||
|
||||
# Stock items for template / variant parts
|
||||
- model: stock.stockitem
|
||||
pk: 500
|
||||
|
31
InvenTree/stock/fixtures/stock_tests.yaml
Normal file
31
InvenTree/stock/fixtures/stock_tests.yaml
Normal file
@ -0,0 +1,31 @@
|
||||
- model: stock.stockitemtestresult
|
||||
fields:
|
||||
stock_item: 105
|
||||
test: "Firmware Version"
|
||||
value: "0xA1B2C3D4"
|
||||
result: True
|
||||
date: 2020-02-02
|
||||
|
||||
- model: stock.stockitemtestresult
|
||||
fields:
|
||||
stock_item: 105
|
||||
test: "Settings Checksum"
|
||||
value: "0xAABBCCDD"
|
||||
result: True
|
||||
date: 2020-02-02
|
||||
|
||||
- model: stock.stockitemtestresult
|
||||
fields:
|
||||
stock_item: 105
|
||||
test: "Temperature Test"
|
||||
result: False
|
||||
date: 2020-05-16
|
||||
notes: 'Got too hot or something'
|
||||
|
||||
- model: stock.stockitemtestresult
|
||||
fields:
|
||||
stock_item: 105
|
||||
test: "Temperature Test"
|
||||
result: True
|
||||
date: 2020-05-17
|
||||
notes: 'Passed temperature test by making it cooler'
|
@ -15,7 +15,9 @@ from InvenTree.helpers import GetExportFormats
|
||||
from InvenTree.forms import HelperForm
|
||||
from InvenTree.fields import RoundingDecimalFormField
|
||||
|
||||
from .models import StockLocation, StockItem, StockItemTracking, StockItemAttachment
|
||||
from .models import StockLocation, StockItem, StockItemTracking
|
||||
from .models import StockItemAttachment
|
||||
from .models import StockItemTestResult
|
||||
|
||||
|
||||
class EditStockItemAttachmentForm(HelperForm):
|
||||
@ -32,6 +34,22 @@ class EditStockItemAttachmentForm(HelperForm):
|
||||
]
|
||||
|
||||
|
||||
class EditStockItemTestResultForm(HelperForm):
|
||||
"""
|
||||
Form for creating / editing a StockItemTestResult object.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = StockItemTestResult
|
||||
fields = [
|
||||
'stock_item',
|
||||
'test',
|
||||
'result',
|
||||
'value',
|
||||
'notes',
|
||||
]
|
||||
|
||||
|
||||
class EditStockLocationForm(HelperForm):
|
||||
""" Form for editing a StockLocation """
|
||||
|
||||
|
29
InvenTree/stock/migrations/0040_stockitemtestresult.py
Normal file
29
InvenTree/stock/migrations/0040_stockitemtestresult.py
Normal file
@ -0,0 +1,29 @@
|
||||
# Generated by Django 3.0.5 on 2020-05-16 09:55
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('stock', '0039_auto_20200513_0016'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='StockItemTestResult',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('test', models.CharField(help_text='Test name', max_length=100, verbose_name='Test')),
|
||||
('result', models.BooleanField(default=False, help_text='Test result', verbose_name='Result')),
|
||||
('value', models.CharField(blank=True, help_text='Test output value', max_length=500, verbose_name='Value')),
|
||||
('date', models.DateTimeField(auto_now_add=True)),
|
||||
('attachment', models.ForeignKey(blank=True, help_text='Test result attachment', null=True, on_delete=django.db.models.deletion.SET_NULL, to='stock.StockItemAttachment', verbose_name='Attachment')),
|
||||
('stock_item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='test_results', to='stock.StockItem')),
|
||||
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
]
|
18
InvenTree/stock/migrations/0041_stockitemtestresult_notes.py
Normal file
18
InvenTree/stock/migrations/0041_stockitemtestresult_notes.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.0.5 on 2020-05-16 10:14
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('stock', '0040_stockitemtestresult'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='stockitemtestresult',
|
||||
name='notes',
|
||||
field=models.CharField(blank=True, help_text='Test notes', max_length=500, verbose_name='Notes'),
|
||||
),
|
||||
]
|
@ -921,6 +921,51 @@ class StockItem(MPTTModel):
|
||||
|
||||
return s
|
||||
|
||||
def getTestResults(self, test=None, result=None, user=None):
|
||||
"""
|
||||
Return all test results associated with this StockItem.
|
||||
|
||||
Optionally can filter results by:
|
||||
- Test name
|
||||
- Test result
|
||||
- User
|
||||
"""
|
||||
|
||||
results = self.test_results
|
||||
|
||||
if test:
|
||||
# Filter by test name
|
||||
results = results.filter(test=test)
|
||||
|
||||
if result is not None:
|
||||
# Filter by test status
|
||||
results = results.filter(result=result)
|
||||
|
||||
if user:
|
||||
# Filter by user
|
||||
results = results.filter(user=user)
|
||||
|
||||
return results
|
||||
|
||||
def testResultMap(self, **kwargs):
|
||||
"""
|
||||
Return a map of test-results using the test name as the key.
|
||||
Where multiple test results exist for a given name,
|
||||
the *most recent* test is used.
|
||||
|
||||
This map is useful for rendering to a template (e.g. a test report),
|
||||
as all named tests are accessible.
|
||||
"""
|
||||
|
||||
results = self.getTestResults(**kwargs).order_by('-date')
|
||||
|
||||
result_map = {}
|
||||
|
||||
for result in results:
|
||||
result_map[result.test] = result
|
||||
|
||||
return result_map
|
||||
|
||||
|
||||
@receiver(pre_delete, sender=StockItem, dispatch_uid='stock_item_pre_delete_log')
|
||||
def before_delete_stock_item(sender, instance, using, **kwargs):
|
||||
@ -993,3 +1038,86 @@ class StockItemTracking(models.Model):
|
||||
|
||||
# TODO
|
||||
# file = models.FileField()
|
||||
|
||||
|
||||
class StockItemTestResult(models.Model):
|
||||
"""
|
||||
A StockItemTestResult records results of custom tests against individual StockItem objects.
|
||||
This is useful for tracking unit acceptance tests, and particularly useful when integrated
|
||||
with automated testing setups.
|
||||
|
||||
Multiple results can be recorded against any given test, allowing tests to be run many times.
|
||||
|
||||
Attributes:
|
||||
stock_item: Link to StockItem
|
||||
test: Test name (simple string matching)
|
||||
result: Test result value (pass / fail / etc)
|
||||
value: Recorded test output value (optional)
|
||||
attachment: Link to StockItem attachment (optional)
|
||||
notes: Extra user notes related to the test (optional)
|
||||
user: User who uploaded the test result
|
||||
date: Date the test result was recorded
|
||||
"""
|
||||
|
||||
def clean(self):
|
||||
|
||||
super().clean()
|
||||
|
||||
# If an attachment is linked to this result, the attachment must also point to the item
|
||||
try:
|
||||
if self.attachment:
|
||||
if not self.attachment.stock_item == self.stock_item:
|
||||
raise ValidationError({
|
||||
'attachment': _("Test result attachment must be linked to the same StockItem"),
|
||||
})
|
||||
except (StockItem.DoesNotExist, StockItemAttachment.DoesNotExist):
|
||||
pass
|
||||
|
||||
stock_item = models.ForeignKey(
|
||||
StockItem,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='test_results'
|
||||
)
|
||||
|
||||
test = models.CharField(
|
||||
blank=False, max_length=100,
|
||||
verbose_name=_('Test'),
|
||||
help_text=_('Test name')
|
||||
)
|
||||
|
||||
result = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_('Result'),
|
||||
help_text=_('Test result')
|
||||
)
|
||||
|
||||
value = models.CharField(
|
||||
blank=True, max_length=500,
|
||||
verbose_name=_('Value'),
|
||||
help_text=_('Test output value')
|
||||
)
|
||||
|
||||
attachment = models.ForeignKey(
|
||||
StockItemAttachment,
|
||||
on_delete=models.SET_NULL,
|
||||
blank=True, null=True,
|
||||
verbose_name=_('Attachment'),
|
||||
help_text=_('Test result attachment'),
|
||||
)
|
||||
|
||||
notes = models.CharField(
|
||||
blank=True, max_length=500,
|
||||
verbose_name=_('Notes'),
|
||||
help_text=_("Test notes"),
|
||||
)
|
||||
|
||||
user = models.ForeignKey(
|
||||
User,
|
||||
on_delete=models.SET_NULL,
|
||||
blank=True, null=True
|
||||
)
|
||||
|
||||
date = models.DateTimeField(
|
||||
auto_now_add=True,
|
||||
editable=False
|
||||
)
|
||||
|
@ -7,6 +7,7 @@ from rest_framework import serializers
|
||||
from .models import StockItem, StockLocation
|
||||
from .models import StockItemTracking
|
||||
from .models import StockItemAttachment
|
||||
from .models import StockItemTestResult
|
||||
|
||||
from django.db.models import Sum, Count
|
||||
from django.db.models.functions import Coalesce
|
||||
@ -193,7 +194,7 @@ class LocationSerializer(InvenTreeModelSerializer):
|
||||
class StockItemAttachmentSerializer(InvenTreeModelSerializer):
|
||||
""" Serializer for StockItemAttachment model """
|
||||
|
||||
def __init_(self, *args, **kwargs):
|
||||
def __init__(self, *args, **kwargs):
|
||||
user_detail = kwargs.pop('user_detail', False)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
@ -211,11 +212,55 @@ class StockItemAttachmentSerializer(InvenTreeModelSerializer):
|
||||
'stock_item',
|
||||
'attachment',
|
||||
'comment',
|
||||
'upload_date',
|
||||
'user',
|
||||
'user_detail',
|
||||
]
|
||||
|
||||
|
||||
class StockItemTestResultSerializer(InvenTreeModelSerializer):
|
||||
""" Serializer for the StockItemTestResult model """
|
||||
|
||||
user_detail = UserSerializerBrief(source='user', read_only=True)
|
||||
attachment_detail = StockItemAttachmentSerializer(source='attachment', read_only=True)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
user_detail = kwargs.pop('user_detail', False)
|
||||
attachment_detail = kwargs.pop('attachment_detail', False)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if user_detail is not True:
|
||||
self.fields.pop('user_detail')
|
||||
|
||||
if attachment_detail is not True:
|
||||
self.fields.pop('attachment_detail')
|
||||
|
||||
class Meta:
|
||||
model = StockItemTestResult
|
||||
|
||||
fields = [
|
||||
'pk',
|
||||
'stock_item',
|
||||
'test',
|
||||
'result',
|
||||
'value',
|
||||
'attachment',
|
||||
'attachment_detail',
|
||||
'notes',
|
||||
'user',
|
||||
'user_detail',
|
||||
'date'
|
||||
]
|
||||
|
||||
read_only_fields = [
|
||||
'pk',
|
||||
'attachment',
|
||||
'user',
|
||||
'date',
|
||||
]
|
||||
|
||||
|
||||
class StockTrackingSerializer(InvenTreeModelSerializer):
|
||||
""" Serializer for StockItemTracking model """
|
||||
|
||||
|
@ -8,8 +8,9 @@
|
||||
|
||||
{% include "stock/tabs.html" with tab="tracking" %}
|
||||
|
||||
<hr>
|
||||
<h4>{% trans "Stock Tracking Information" %}</h4>
|
||||
<hr>
|
||||
|
||||
<div id='table-toolbar'>
|
||||
<div class='btn-group'>
|
||||
<button class='btn btn-success' type='button' title='New tracking entry' id='new-entry'>New Entry</button>
|
||||
|
@ -7,8 +7,8 @@
|
||||
|
||||
{% include "stock/tabs.html" with tab='attachments' %}
|
||||
|
||||
<hr>
|
||||
<h4>{% trans "Stock Item Attachments" %}</h4>
|
||||
<hr>
|
||||
|
||||
{% include "attachment_table.html" with attachments=item.attachments.all %}
|
||||
|
||||
|
@ -10,6 +10,7 @@
|
||||
{% include "stock/tabs.html" with tab="notes" %}
|
||||
|
||||
{% if editing %}
|
||||
|
||||
<h4>{% trans "Stock Item Notes" %}</h4>
|
||||
<hr>
|
||||
|
||||
|
76
InvenTree/stock/templates/stock/item_tests.html
Normal file
76
InvenTree/stock/templates/stock/item_tests.html
Normal file
@ -0,0 +1,76 @@
|
||||
{% extends "stock/item_base.html" %}
|
||||
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block details %}
|
||||
|
||||
{% include "stock/tabs.html" with tab='tests' %}
|
||||
|
||||
<h4>{% trans "Test Results" %}</h4>
|
||||
<hr>
|
||||
|
||||
<div id='button-toolbar'>
|
||||
<div class='button-toolbar container-fluid' style="float: right;">
|
||||
<div class='btn-group' role='group'>
|
||||
<button type='button' class='btn btn-success' id='add-test-result'>{% trans "Add Test Result" %}</button>
|
||||
</div>
|
||||
<div class='filter-list' id='filter-list-stocktests'>
|
||||
<!-- Empty div -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class='table table-striped table-condensed' data-toolbar='#button-toolbar' id='test-result-table'></table>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block js_ready %}
|
||||
{{ block.super }}
|
||||
|
||||
loadStockTestResultsTable(
|
||||
$("#test-result-table"), {
|
||||
params: {
|
||||
stock_item: {{ item.id }},
|
||||
user_detail: true,
|
||||
attachment_detail: true,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
function reloadTable() {
|
||||
$("#test-result-table").bootstrapTable("refresh");
|
||||
}
|
||||
|
||||
$("#add-test-result").click(function() {
|
||||
launchModalForm(
|
||||
"{% url 'stock-item-test-create' %}", {
|
||||
data: {
|
||||
stock_item: {{ item.id }},
|
||||
},
|
||||
success: reloadTable,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
$("#test-result-table").on('click', '.button-test-edit', function() {
|
||||
var button = $(this);
|
||||
|
||||
var url = `/stock/item/test/${button.attr('pk')}/edit/`;
|
||||
|
||||
launchModalForm(url, {
|
||||
success: reloadTable,
|
||||
});
|
||||
});
|
||||
|
||||
$("#test-result-table").on('click', '.button-test-delete', function() {
|
||||
var button = $(this);
|
||||
|
||||
var url = `/stock/item/test/${button.attr('pk')}/delete/`;
|
||||
|
||||
launchModalForm(url, {
|
||||
success: reloadTable,
|
||||
});
|
||||
});
|
||||
|
||||
{% endblock %}
|
@ -1,15 +1,20 @@
|
||||
{% load i18n %}
|
||||
|
||||
<ul class='nav nav-tabs'>
|
||||
<li{% ifequal tab 'children' %} class='active'{% endifequal %}>
|
||||
<a href="{% url 'stock-item-children' item.id %}">{% trans "Children" %}{% if item.child_count > 0 %}<span class='badge'>{{ item.child_count }}</span>{% endif %}</a>
|
||||
</li>
|
||||
<li{% ifequal tab 'tracking' %} class='active'{% endifequal %}>
|
||||
<a href="{% url 'stock-item-detail' item.id %}">
|
||||
{% trans "Tracking" %}
|
||||
{% if item.tracking_info.count > 0 %}<span class='badge'>{{ item.tracking_info.count }}</span>{% endif %}
|
||||
</a>
|
||||
</li>
|
||||
{% if item.part.trackable %}
|
||||
<li{% if tab == 'tests' %} class='active'{% endif %}>
|
||||
<a href="{% url 'stock-item-test-results' item.id %}">
|
||||
{% trans "Test Results" %}
|
||||
{% if item.test_results.count > 0 %}<span class='badge'>{{ item.test_results.count }}</span>{% endif %}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if 0 %}
|
||||
<!-- These tabs are to be implemented in the future -->
|
||||
<li{% ifequal tab 'builds' %} class='active'{% endifequal %}>
|
||||
@ -28,4 +33,9 @@
|
||||
{% if item.attachments.count > 0 %}<span class='badge'>{{ item.attachments.count }}</span>{% endif %}
|
||||
</a>
|
||||
</li>
|
||||
{% if item.child_count > 0 %}
|
||||
<li{% ifequal tab 'children' %} class='active'{% endifequal %}>
|
||||
<a href="{% url 'stock-item-children' item.id %}">{% trans "Children" %}{% if item.child_count > 0 %}<span class='badge'>{{ item.child_count }}</span>{% endif %}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
@ -6,11 +6,17 @@ from django.contrib.auth import get_user_model
|
||||
from .models import StockLocation
|
||||
|
||||
|
||||
class StockLocationTest(APITestCase):
|
||||
"""
|
||||
Series of API tests for the StockLocation API
|
||||
"""
|
||||
list_url = reverse('api-location-list')
|
||||
class StockAPITestCase(APITestCase):
|
||||
|
||||
fixtures = [
|
||||
'category',
|
||||
'part',
|
||||
'company',
|
||||
'location',
|
||||
'supplier_part',
|
||||
'stock',
|
||||
'stock_tests',
|
||||
]
|
||||
|
||||
def setUp(self):
|
||||
# Create a user for auth
|
||||
@ -18,6 +24,21 @@ class StockLocationTest(APITestCase):
|
||||
User.objects.create_user('testuser', 'test@testing.com', 'password')
|
||||
self.client.login(username='testuser', password='password')
|
||||
|
||||
def doPost(self, url, data={}):
|
||||
response = self.client.post(url, data=data, format='json')
|
||||
|
||||
return response
|
||||
|
||||
|
||||
class StockLocationTest(StockAPITestCase):
|
||||
"""
|
||||
Series of API tests for the StockLocation API
|
||||
"""
|
||||
list_url = reverse('api-location-list')
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
# Add some stock locations
|
||||
StockLocation.objects.create(name='top', description='top category')
|
||||
|
||||
@ -38,7 +59,7 @@ class StockLocationTest(APITestCase):
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
|
||||
|
||||
class StockItemTest(APITestCase):
|
||||
class StockItemTest(StockAPITestCase):
|
||||
"""
|
||||
Series of API tests for the StockItem API
|
||||
"""
|
||||
@ -49,11 +70,7 @@ class StockItemTest(APITestCase):
|
||||
return reverse('api-stock-detail', kwargs={'pk': pk})
|
||||
|
||||
def setUp(self):
|
||||
# Create a user for auth
|
||||
User = get_user_model()
|
||||
User.objects.create_user('testuser', 'test@testing.com', 'password')
|
||||
self.client.login(username='testuser', password='password')
|
||||
|
||||
super().setUp()
|
||||
# Create some stock locations
|
||||
top = StockLocation.objects.create(name='A', description='top')
|
||||
|
||||
@ -65,30 +82,11 @@ class StockItemTest(APITestCase):
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
|
||||
class StocktakeTest(APITestCase):
|
||||
class StocktakeTest(StockAPITestCase):
|
||||
"""
|
||||
Series of tests for the Stocktake API
|
||||
"""
|
||||
|
||||
fixtures = [
|
||||
'category',
|
||||
'part',
|
||||
'company',
|
||||
'location',
|
||||
'supplier_part',
|
||||
'stock',
|
||||
]
|
||||
|
||||
def setUp(self):
|
||||
User = get_user_model()
|
||||
User.objects.create_user('testuser', 'test@testing.com', 'password')
|
||||
self.client.login(username='testuser', password='password')
|
||||
|
||||
def doPost(self, url, data={}):
|
||||
response = self.client.post(url, data=data, format='json')
|
||||
|
||||
return response
|
||||
|
||||
def test_action(self):
|
||||
"""
|
||||
Test each stocktake action endpoint,
|
||||
@ -179,3 +177,82 @@ class StocktakeTest(APITestCase):
|
||||
|
||||
response = self.doPost(url, data)
|
||||
self.assertContains(response, 'Valid location must be specified', status_code=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
class StockTestResultTest(StockAPITestCase):
|
||||
|
||||
def get_url(self):
|
||||
return reverse('api-stock-test-result-list')
|
||||
|
||||
def test_list(self):
|
||||
|
||||
url = self.get_url()
|
||||
response = self.client.get(url)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertGreaterEqual(len(response.data), 4)
|
||||
|
||||
response = self.client.get(url, data={'stock_item': 105})
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertGreaterEqual(len(response.data), 4)
|
||||
|
||||
def test_post_fail(self):
|
||||
# Attempt to post a new test result without specifying required data
|
||||
|
||||
url = self.get_url()
|
||||
|
||||
response = self.client.post(
|
||||
url,
|
||||
data={
|
||||
'test': 'A test',
|
||||
'result': True,
|
||||
},
|
||||
format='json'
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# This one should pass!
|
||||
response = self.client.post(
|
||||
url,
|
||||
data={
|
||||
'test': 'A test',
|
||||
'stock_item': 105,
|
||||
'result': True,
|
||||
},
|
||||
format='json'
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
|
||||
def test_post(self):
|
||||
# Test creation of a new test result
|
||||
|
||||
url = self.get_url()
|
||||
|
||||
response = self.client.get(url)
|
||||
n = len(response.data)
|
||||
|
||||
data = {
|
||||
'stock_item': 105,
|
||||
'test': 'Checked Steam Valve',
|
||||
'result': False,
|
||||
'value': '150kPa',
|
||||
'notes': 'I guess there was just too much pressure?',
|
||||
}
|
||||
|
||||
response = self.client.post(url, data, format='json')
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(len(response.data), n + 1)
|
||||
|
||||
# And read out again
|
||||
response = self.client.get(url, data={'test': 'Checked Steam Valve'})
|
||||
|
||||
self.assertEqual(len(response.data), 1)
|
||||
|
||||
test = response.data[0]
|
||||
self.assertEqual(test['value'], '150kPa')
|
||||
self.assertEqual(test['user'], 1)
|
||||
|
@ -17,6 +17,7 @@ class StockTest(TestCase):
|
||||
'part',
|
||||
'location',
|
||||
'stock',
|
||||
'stock_tests',
|
||||
]
|
||||
|
||||
def setUp(self):
|
||||
@ -95,8 +96,8 @@ class StockTest(TestCase):
|
||||
self.assertFalse(self.drawer2.has_items())
|
||||
|
||||
# Drawer 3 should have three stock items
|
||||
self.assertEqual(self.drawer3.stock_items.count(), 15)
|
||||
self.assertEqual(self.drawer3.item_count, 15)
|
||||
self.assertEqual(self.drawer3.stock_items.count(), 16)
|
||||
self.assertEqual(self.drawer3.item_count, 16)
|
||||
|
||||
def test_stock_count(self):
|
||||
part = Part.objects.get(pk=1)
|
||||
@ -108,7 +109,7 @@ class StockTest(TestCase):
|
||||
self.assertEqual(part.total_stock, 9000)
|
||||
|
||||
# There should be 18 widgets in stock
|
||||
self.assertEqual(StockItem.objects.filter(part=25).aggregate(Sum('quantity'))['quantity__sum'], 18)
|
||||
self.assertEqual(StockItem.objects.filter(part=25).aggregate(Sum('quantity'))['quantity__sum'], 19)
|
||||
|
||||
def test_delete_location(self):
|
||||
|
||||
@ -168,12 +169,12 @@ class StockTest(TestCase):
|
||||
self.assertEqual(w1.quantity, 4)
|
||||
|
||||
# There should also be a new object still in drawer3
|
||||
self.assertEqual(StockItem.objects.filter(part=25).count(), 4)
|
||||
self.assertEqual(StockItem.objects.filter(part=25).count(), 5)
|
||||
widget = StockItem.objects.get(location=self.drawer3.id, part=25, quantity=4)
|
||||
|
||||
# Try to move negative units
|
||||
self.assertFalse(widget.move(self.bathroom, 'Test', None, quantity=-100))
|
||||
self.assertEqual(StockItem.objects.filter(part=25).count(), 4)
|
||||
self.assertEqual(StockItem.objects.filter(part=25).count(), 5)
|
||||
|
||||
# Try to move to a blank location
|
||||
self.assertFalse(widget.move(None, 'null', None))
|
||||
@ -404,3 +405,29 @@ class VariantTest(StockTest):
|
||||
|
||||
item.serial += 1
|
||||
item.save()
|
||||
|
||||
|
||||
class TestResultTest(StockTest):
|
||||
"""
|
||||
Tests for the StockItemTestResult model.
|
||||
"""
|
||||
|
||||
def test_test_count(self):
|
||||
item = StockItem.objects.get(pk=105)
|
||||
tests = item.test_results
|
||||
self.assertEqual(tests.count(), 4)
|
||||
|
||||
results = item.getTestResults(test="Temperature Test")
|
||||
self.assertEqual(results.count(), 2)
|
||||
|
||||
# Passing tests
|
||||
self.assertEqual(item.getTestResults(result=True).count(), 3)
|
||||
self.assertEqual(item.getTestResults(result=False).count(), 1)
|
||||
|
||||
# Result map
|
||||
result_map = item.testResultMap()
|
||||
|
||||
self.assertEqual(len(result_map), 3)
|
||||
|
||||
for test in ['Firmware Version', 'Settings Checksum', 'Temperature Test']:
|
||||
self.assertIn(test, result_map.keys())
|
||||
|
@ -24,6 +24,7 @@ stock_item_detail_urls = [
|
||||
|
||||
url(r'^add_tracking/', views.StockItemTrackingCreate.as_view(), name='stock-tracking-create'),
|
||||
|
||||
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'),
|
||||
url(r'^notes/', views.StockItemNotes.as_view(), name='stock-item-notes'),
|
||||
@ -51,12 +52,20 @@ stock_urls = [
|
||||
|
||||
url(r'^item/new/?', views.StockItemCreate.as_view(), name='stock-item-create'),
|
||||
|
||||
# URLs for StockItem attachments
|
||||
url(r'^item/attachment/', include([
|
||||
url(r'^new/', views.StockItemAttachmentCreate.as_view(), name='stock-item-attachment-create'),
|
||||
url(r'^(?P<pk>\d+)/edit/', views.StockItemAttachmentEdit.as_view(), name='stock-item-attachment-edit'),
|
||||
url(r'^(?P<pk>\d+)/delete/', views.StockItemAttachmentDelete.as_view(), name='stock-item-attachment-delete'),
|
||||
])),
|
||||
|
||||
# URLs for StockItem tests
|
||||
url(r'^item/test/', include([
|
||||
url(r'^new/', views.StockItemTestResultCreate.as_view(), name='stock-item-test-create'),
|
||||
url(r'^(?P<pk>\d+)/edit/', views.StockItemTestResultEdit.as_view(), name='stock-item-test-edit'),
|
||||
url(r'^(?P<pk>\d+)/delete/', views.StockItemTestResultDelete.as_view(), name='stock-item-test-delete'),
|
||||
])),
|
||||
|
||||
url(r'^track/', include(stock_tracking_urls)),
|
||||
|
||||
url(r'^adjust/?', views.StockAdjust.as_view(), name='stock-adjust'),
|
||||
|
@ -26,7 +26,7 @@ from datetime import datetime
|
||||
|
||||
from company.models import Company, SupplierPart
|
||||
from part.models import Part
|
||||
from .models import StockItem, StockLocation, StockItemTracking, StockItemAttachment
|
||||
from .models import StockItem, StockLocation, StockItemTracking, StockItemAttachment, StockItemTestResult
|
||||
|
||||
from .admin import StockItemResource
|
||||
|
||||
@ -38,6 +38,7 @@ from .forms import TrackingEntryForm
|
||||
from .forms import SerializeStockForm
|
||||
from .forms import ExportOptionsForm
|
||||
from .forms import EditStockItemAttachmentForm
|
||||
from .forms import EditStockItemTestResultForm
|
||||
|
||||
|
||||
class StockIndex(ListView):
|
||||
@ -228,6 +229,69 @@ class StockItemAttachmentDelete(AjaxDeleteView):
|
||||
}
|
||||
|
||||
|
||||
class StockItemTestResultCreate(AjaxCreateView):
|
||||
"""
|
||||
View for adding a new StockItemTestResult
|
||||
"""
|
||||
|
||||
model = StockItemTestResult
|
||||
form_class = EditStockItemTestResultForm
|
||||
ajax_form_title = _("Add Test Result")
|
||||
|
||||
def post_save(self, **kwargs):
|
||||
""" Record the user that uploaded the test result """
|
||||
|
||||
self.object.user = self.request.user
|
||||
self.object.save()
|
||||
|
||||
def get_initial(self):
|
||||
|
||||
initials = super().get_initial()
|
||||
|
||||
try:
|
||||
stock_id = self.request.GET.get('stock_item', None)
|
||||
initials['stock_item'] = StockItem.objects.get(pk=stock_id)
|
||||
except (ValueError, StockItem.DoesNotExist):
|
||||
pass
|
||||
|
||||
return initials
|
||||
|
||||
def get_form(self):
|
||||
|
||||
form = super().get_form()
|
||||
form.fields['stock_item'].widget = HiddenInput()
|
||||
|
||||
return form
|
||||
|
||||
|
||||
class StockItemTestResultEdit(AjaxUpdateView):
|
||||
"""
|
||||
View for editing a StockItemTestResult
|
||||
"""
|
||||
|
||||
model = StockItemTestResult
|
||||
form_class = EditStockItemTestResultForm
|
||||
ajax_form_title = _("Edit Test Result")
|
||||
|
||||
def get_form(self):
|
||||
|
||||
form = super().get_form()
|
||||
|
||||
form.fields['stock_item'].widget = HiddenInput()
|
||||
|
||||
return form
|
||||
|
||||
|
||||
class StockItemTestResultDelete(AjaxDeleteView):
|
||||
"""
|
||||
View for deleting a StockItemTestResult
|
||||
"""
|
||||
|
||||
model = StockItemTestResult
|
||||
ajax_form_title = _("Delete Test Result")
|
||||
context_object_name = "result"
|
||||
|
||||
|
||||
class StockExportOptions(AjaxView):
|
||||
""" Form for selecting StockExport options """
|
||||
|
||||
|
@ -28,7 +28,7 @@
|
||||
<td>
|
||||
<div class='btn-group' style='float: right;'>
|
||||
<button type='button' class='btn btn-default btn-glyph attachment-edit-button' pk="{{ attachment.id }}" data-toggle='tooltip' title='{% trans "Edit attachment" %}'>
|
||||
<span class='fas fa-edit'/>
|
||||
<span class='fas fa-edit icon-blue'/>
|
||||
</button>
|
||||
<button type='button' class='btn btn-default btn-glyph attachment-delete-button' pk="{{ attachment.id }}" data-toggle='tooltip' title='{% trans "Delete attachment" %}'>
|
||||
<span class='fas fa-trash-alt icon-red'/>
|
||||
|
@ -114,6 +114,7 @@ InvenTree
|
||||
<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>
|
||||
<script type='text/javascript' src="{% url 'table_filters.js' %}"></script>
|
||||
|
||||
<script type='text/javascript' src="{% static 'fontawesome/js/solid.js' %}"></script>
|
||||
<script type='text/javascript' src="{% static 'fontawesome/js/brands.js' %}"></script>
|
||||
@ -122,8 +123,6 @@ InvenTree
|
||||
{% block js_load %}
|
||||
{% endblock %}
|
||||
|
||||
{% include "table_filters.html" %}
|
||||
|
||||
<script type='text/javascript'>
|
||||
|
||||
$(document).ready(function () {
|
||||
|
@ -18,6 +18,123 @@ function removeStockRow(e) {
|
||||
$('#' + row).remove();
|
||||
}
|
||||
|
||||
|
||||
function passFailBadge(result) {
|
||||
|
||||
if (result) {
|
||||
return `<span class='label label-green'>{% trans "PASS" %}</span>`;
|
||||
} else {
|
||||
return `<span class='label label-red'>{% trans "FAIL" %}</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
function loadStockTestResultsTable(table, options) {
|
||||
/*
|
||||
* Load StockItemTestResult table
|
||||
*/
|
||||
|
||||
var params = options.params || {};
|
||||
|
||||
// HTML element to setup the filtering
|
||||
var filterListElement = options.filterList || '#filter-list-stocktests';
|
||||
|
||||
var filters = {};
|
||||
|
||||
filters = loadTableFilters("stocktests");
|
||||
|
||||
var original = {};
|
||||
|
||||
for (var key in params) {
|
||||
original[key] = params[key];
|
||||
}
|
||||
|
||||
setupFilterList("stocktests", table, filterListElement);
|
||||
|
||||
// Override the default values, or add new ones
|
||||
for (var key in params) {
|
||||
filters[key] = params[key];
|
||||
}
|
||||
|
||||
table.inventreeTable({
|
||||
method: 'get',
|
||||
formatNoMatches: function() {
|
||||
return '{% trans "No test results matching query" %}';
|
||||
},
|
||||
url: "{% url 'api-stock-test-result-list' %}",
|
||||
queryParams: filters,
|
||||
original: original,
|
||||
columns: [
|
||||
{
|
||||
field: 'pk',
|
||||
title: 'ID',
|
||||
visible: false
|
||||
},
|
||||
{
|
||||
field: 'test',
|
||||
title: '{% trans "Test" %}',
|
||||
sortable: true,
|
||||
formatter: function(value, row) {
|
||||
var html = value;
|
||||
|
||||
if (row.attachment_detail) {
|
||||
html += `<a href='${row.attachment_detail.attachment}'><span class='fas fa-file-alt label-right'></span></a>`;
|
||||
}
|
||||
|
||||
return html;
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'result',
|
||||
title: "{% trans "Result" %}",
|
||||
sortable: true,
|
||||
formatter: function(value) {
|
||||
return passFailBadge(value);
|
||||
}
|
||||
},
|
||||
{
|
||||
field: 'value',
|
||||
title: "{% trans "Value" %}",
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
field: 'notes',
|
||||
title: '{% trans "Notes" %}',
|
||||
},
|
||||
{
|
||||
field: 'date',
|
||||
title: '{% trans "Uploaded" %}',
|
||||
sortable: true,
|
||||
formatter: function(value, row) {
|
||||
var html = value;
|
||||
|
||||
if (row.user_detail) {
|
||||
html += `<span class='badge'>${row.user_detail.username}</span>`;
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
||||
},
|
||||
{
|
||||
field: 'buttons',
|
||||
formatter: function(value, row) {
|
||||
|
||||
var pk = row.pk;
|
||||
|
||||
var html = `<div class='btn-group float-right' role='group'>`;
|
||||
|
||||
html += makeIconButton('fa-edit icon-blue', 'button-test-edit', pk, '{% trans "Edit test result" %}');
|
||||
html += makeIconButton('fa-trash-alt icon-red', 'button-test-delete', pk, '{% trans "Delete test result" %}');
|
||||
|
||||
html += `</div>`;
|
||||
|
||||
return html;
|
||||
}
|
||||
},
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function loadStockTable(table, options) {
|
||||
/* Load data into a stock table with adjustable options.
|
||||
* Fetches data (via AJAX) and loads into a bootstrap table.
|
||||
|
@ -1,8 +1,6 @@
|
||||
{% load i18n %}
|
||||
{% load status_codes %}
|
||||
|
||||
<script type='text/javascript'>
|
||||
|
||||
{% include "status_codes.html" with label='stock' options=StockStatus.list %}
|
||||
{% include "status_codes.html" with label='build' options=BuildStatus.list %}
|
||||
{% include "status_codes.html" with label='purchaseOrder' options=PurchaseOrderStatus.list %}
|
||||
@ -39,6 +37,16 @@ function getAvailableTableFilters(tableKey) {
|
||||
};
|
||||
}
|
||||
|
||||
// Filters for the 'stock test' table
|
||||
if (tableKey == 'stocktests') {
|
||||
return {
|
||||
result: {
|
||||
type: 'bool',
|
||||
title: "{% trans 'Test result' %}",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Filters for the "Build" table
|
||||
if (tableKey == 'build') {
|
||||
return {
|
||||
@ -124,4 +132,3 @@ function getAvailableTableFilters(tableKey) {
|
||||
// Finally, no matching key
|
||||
return {};
|
||||
}
|
||||
</script>
|
Loading…
Reference in New Issue
Block a user