Merge remote-tracking branch 'inventree/master'

This commit is contained in:
Oliver Walters 2020-05-12 22:14:20 +10:00
commit dcf00d816d
22 changed files with 306 additions and 200 deletions

View File

@ -8,6 +8,9 @@ from __future__ import unicode_literals
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.http import JsonResponse from django.http import JsonResponse
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import filters
from rest_framework import permissions from rest_framework import permissions
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.views import APIView from rest_framework.views import APIView
@ -41,6 +44,28 @@ class InfoView(AjaxView):
return JsonResponse(data) return JsonResponse(data)
class AttachmentMixin:
"""
Mixin for creating attachment objects,
and ensuring the user information is saved correctly.
"""
permission_classes = [permissions.IsAuthenticated]
filter_backends = [
DjangoFilterBackend,
filters.OrderingFilter,
filters.SearchFilter,
]
def perform_create(self, serializer):
""" Save the user information when a file is uploaded """
attachment = serializer.save()
attachment.user = self.request.user
attachment.save()
class ActionPluginView(APIView): class ActionPluginView(APIView):
""" """
Endpoint for running custom action plugins. Endpoint for running custom action plugins.

View File

@ -7,6 +7,7 @@ from __future__ import unicode_literals
import os import os
from django.db import models from django.db import models
from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@ -41,6 +42,8 @@ class InvenTreeAttachment(models.Model):
Attributes: Attributes:
attachment: File attachment: File
comment: String descriptor for the attachment comment: String descriptor for the attachment
user: User associated with file upload
upload_date: Date the file was uploaded
""" """
def getSubdir(self): def getSubdir(self):
""" """
@ -55,6 +58,15 @@ class InvenTreeAttachment(models.Model):
comment = models.CharField(max_length=100, help_text=_('File comment')) comment = models.CharField(max_length=100, help_text=_('File comment'))
user = models.ForeignKey(
User,
on_delete=models.SET_NULL,
blank=True, null=True,
help_text=_('User'),
)
upload_date = models.DateField(auto_now_add=True, null=True, blank=True)
@property @property
def basename(self): def basename(self):
return os.path.basename(self.attachment.name) return os.path.basename(self.attachment.name)

View File

@ -12,6 +12,7 @@ from rest_framework import filters
from django.conf.urls import url, include from django.conf.urls import url, include
from InvenTree.helpers import str2bool from InvenTree.helpers import str2bool
from InvenTree.api import AttachmentMixin
from part.models import Part from part.models import Part
from company.models import SupplierPart from company.models import SupplierPart
@ -200,7 +201,7 @@ class POLineItemDetail(generics.RetrieveUpdateAPIView):
] ]
class SOAttachmentList(generics.ListCreateAPIView): class SOAttachmentList(generics.ListCreateAPIView, AttachmentMixin):
""" """
API endpoint for listing (and creating) a SalesOrderAttachment (file upload) API endpoint for listing (and creating) a SalesOrderAttachment (file upload)
""" """
@ -208,12 +209,6 @@ class SOAttachmentList(generics.ListCreateAPIView):
queryset = SalesOrderAttachment.objects.all() queryset = SalesOrderAttachment.objects.all()
serializer_class = SOAttachmentSerializer serializer_class = SOAttachmentSerializer
filter_backends = [
DjangoFilterBackend,
filters.OrderingFilter,
filters.SearchFilter,
]
filter_fields = [ filter_fields = [
'order', 'order',
] ]
@ -399,7 +394,7 @@ class SOLineItemDetail(generics.RetrieveUpdateAPIView):
permission_classes = [permissions.IsAuthenticated] permission_classes = [permissions.IsAuthenticated]
class POAttachmentList(generics.ListCreateAPIView): class POAttachmentList(generics.ListCreateAPIView, AttachmentMixin):
""" """
API endpoint for listing (and creating) a PurchaseOrderAttachment (file upload) API endpoint for listing (and creating) a PurchaseOrderAttachment (file upload)
""" """
@ -407,12 +402,6 @@ class POAttachmentList(generics.ListCreateAPIView):
queryset = PurchaseOrderAttachment.objects.all() queryset = PurchaseOrderAttachment.objects.all()
serializer_class = POAttachmentSerializer serializer_class = POAttachmentSerializer
filter_backends = [
DjangoFilterBackend,
filters.OrderingFilter,
filters.SearchFilter,
]
filter_fields = [ filter_fields = [
'order', 'order',
] ]

View File

@ -0,0 +1,26 @@
# Generated by Django 3.0.5 on 2020-05-12 10:33
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),
('order', '0032_auto_20200427_0044'),
]
operations = [
migrations.AddField(
model_name='purchaseorderattachment',
name='user',
field=models.ForeignKey(blank=True, help_text='User', null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='salesorderattachment',
name='user',
field=models.ForeignKey(blank=True, help_text='User', null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL),
),
]

View File

@ -0,0 +1,23 @@
# Generated by Django 3.0.5 on 2020-05-12 10:54
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('order', '0033_auto_20200512_1033'),
]
operations = [
migrations.AddField(
model_name='purchaseorderattachment',
name='upload_date',
field=models.DateField(auto_now_add=True, null=True),
),
migrations.AddField(
model_name='salesorderattachment',
name='upload_date',
field=models.DateField(auto_now_add=True, null=True),
),
]

View File

@ -12,39 +12,8 @@
<hr> <hr>
<div id='attachment-buttons'> {% include "attachment_table.html" with attachments=order.attachments.all %}
<div class='btn-group'>
<button type='button' class='btn btn-success' id='new-attachment'>{% trans "Add Attachment" %}</button>
</div>
</div>
<table class='table table-striped table-condensed' data-toolbar='#attachment-buttons' id='attachment-table'>
<thead>
<tr>
<th data-field='file' data-searchable='true'>{% trans "File" %}</th>
<th data-field='comment' data-searchable='true'>{% trans "Comment" %}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for attachment in order.attachments.all %}
<tr>
<td><a href='/media/{{ attachment.attachment }}'>{{ attachment.basename }}</a></td>
<td>{{ attachment.comment }}</td>
<td>
<div class='btn-group' style='float: right;'>
<button type='button' class='btn btn-default btn-glyph attachment-edit-button' url="{% url 'po-attachment-edit' attachment.id %}" data-toggle='tooltip' title='{% trans "Edit attachment" %}'>
<span class='fas fa-edit'/>
</button>
<button type='button' class='btn btn-default btn-glyph attachment-delete-button' url="{% url 'po-attachment-delete' attachment.id %}" data-toggle='tooltip' title='{% trans "Delete attachment" %}'>
<span class='fas fa-trash-alt icon-red'/>
</button>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %} {% endblock %}
@ -61,8 +30,10 @@ $("#new-attachment").click(function() {
$("#attachment-table").on('click', '.attachment-edit-button', function() { $("#attachment-table").on('click', '.attachment-edit-button', function() {
var button = $(this); var button = $(this);
var url = `/order/purchase-order/attachment/${button.attr('pk')}/edit/`;
launchModalForm(button.attr('url'), { launchModalForm(url, {
reload: true, reload: true,
}); });
}); });
@ -70,7 +41,11 @@ $("#attachment-table").on('click', '.attachment-edit-button', function() {
$("#attachment-table").on('click', '.attachment-delete-button', function() { $("#attachment-table").on('click', '.attachment-delete-button', function() {
var button = $(this); var button = $(this);
launchModalForm(button.attr('url'), { var url = `/order/purchase-order/attachment/${button.attr('pk')}/delete/`;
console.log("url: " + url);
launchModalForm(url, {
reload: true, reload: true,
}); });
}); });

View File

@ -8,43 +8,11 @@
{% include 'order/so_tabs.html' with tab='attachments' %} {% include 'order/so_tabs.html' with tab='attachments' %}
<h4>{% trans "Sales Order Attachments" %} <h4>{% trans "Sales Order Attachments" %}</h4>
<hr> <hr>
<div id='attachment-buttons'> {% include "attachment_table.html" with attachments=order.attachments.all %}
<div class='btn-group'>
<button type='button' class='btn btn-success' id='new-attachment'>{% trans "Add Attachment" %}</button>
</div>
</div>
<table class='table table-striped table-condensed' data-toolbar='#attachment-buttons' id='attachment-table'>
<thead>
<tr>
<th data-field='file' data-searchable='true'>{% trans "File" %}</th>
<th data-field='comment' data-searchable='true'>{% trans "Comment" %}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for attachment in order.attachments.all %}
<tr>
<td><a href='/media/{{ attachment.attachment }}'>{{ attachment.basename }}</a></td>
<td>{{ attachment.comment }}</td>
<td>
<div class='btn-group' style='float: right;'>
<button type='button' class='btn btn-default btn-glyph attachment-edit-button' url="{% url 'so-attachment-edit' attachment.id %}" data-toggle='tooltip' title='{% trans "Edit attachment" %}'>
<span class='fas fa-edit'/>
</button>
<button type='button' class='btn btn-default btn-glyph attachment-delete-button' url="{% url 'so-attachment-delete' attachment.id %}" data-toggle='tooltip' title='{% trans "Delete attachment" %}'>
<span class='fas fa-trash-alt icon-red'/>
</button>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %} {% endblock %}
@ -62,7 +30,9 @@ $("#new-attachment").click(function() {
$("#attachment-table").on('click', '.attachment-edit-button', function() { $("#attachment-table").on('click', '.attachment-edit-button', function() {
var button = $(this); var button = $(this);
launchModalForm(button.attr('url'), { var url = `/order/sales-order/attachment/${button.attr('pk')}/edit/`;
launchModalForm(url, {
reload: true, reload: true,
}); });
}); });
@ -70,7 +40,9 @@ $("#attachment-table").on('click', '.attachment-edit-button', function() {
$("#attachment-table").on('click', '.attachment-delete-button', function() { $("#attachment-table").on('click', '.attachment-delete-button', function() {
var button = $(this); var button = $(this);
launchModalForm(button.attr('url'), { var url = `/order/sales-order/attachment/${button.attr('pk')}/delete/`;
launchModalForm(url, {
reload: true, reload: true,
}); });
}); });

View File

@ -42,7 +42,7 @@ purchase_order_urls = [
])), ])),
])), ])),
url(r'^attachments/', include([ url(r'^attachment/', include([
url(r'^new/', views.PurchaseOrderAttachmentCreate.as_view(), name='po-attachment-create'), url(r'^new/', views.PurchaseOrderAttachmentCreate.as_view(), name='po-attachment-create'),
url(r'^(?P<pk>\d+)/edit/', views.PurchaseOrderAttachmentEdit.as_view(), name='po-attachment-edit'), url(r'^(?P<pk>\d+)/edit/', views.PurchaseOrderAttachmentEdit.as_view(), name='po-attachment-edit'),
url(r'^(?P<pk>\d+)/delete/', views.PurchaseOrderAttachmentDelete.as_view(), name='po-attachment-delete'), url(r'^(?P<pk>\d+)/delete/', views.PurchaseOrderAttachmentDelete.as_view(), name='po-attachment-delete'),
@ -86,7 +86,7 @@ sales_order_urls = [
])), ])),
])), ])),
url(r'^attachments/', include([ url(r'^attachment/', include([
url(r'^new/', views.SalesOrderAttachmentCreate.as_view(), name='so-attachment-create'), url(r'^new/', views.SalesOrderAttachmentCreate.as_view(), name='so-attachment-create'),
url(r'^(?P<pk>\d+)/edit/', views.SalesOrderAttachmentEdit.as_view(), name='so-attachment-edit'), url(r'^(?P<pk>\d+)/edit/', views.SalesOrderAttachmentEdit.as_view(), name='so-attachment-edit'),
url(r'^(?P<pk>\d+)/delete/', views.SalesOrderAttachmentDelete.as_view(), name='so-attachment-delete'), url(r'^(?P<pk>\d+)/delete/', views.SalesOrderAttachmentDelete.as_view(), name='so-attachment-delete'),

View File

@ -93,6 +93,10 @@ class PurchaseOrderAttachmentCreate(AjaxCreateView):
ajax_form_title = _("Add Purchase Order Attachment") ajax_form_title = _("Add Purchase Order Attachment")
ajax_template_name = "modal_form.html" ajax_template_name = "modal_form.html"
def post_save(self, **kwargs):
self.object.user = self.request.user
self.object.save()
def get_data(self): def get_data(self):
return { return {
"success": _("Added attachment") "success": _("Added attachment")
@ -133,6 +137,10 @@ class SalesOrderAttachmentCreate(AjaxCreateView):
form_class = order_forms.EditSalesOrderAttachmentForm form_class = order_forms.EditSalesOrderAttachmentForm
ajax_form_title = _('Add Sales Order Attachment') ajax_form_title = _('Add Sales Order Attachment')
def post_save(self, **kwargs):
self.object.user = self.request.user
self.object.save()
def get_data(self): def get_data(self):
return { return {
'success': _('Added attachment') 'success': _('Added attachment')

View File

@ -25,6 +25,7 @@ from . import serializers as part_serializers
from InvenTree.views import TreeSerializer from InvenTree.views import TreeSerializer
from InvenTree.helpers import str2bool, isNull from InvenTree.helpers import str2bool, isNull
from InvenTree.api import AttachmentMixin
class PartCategoryTree(TreeSerializer): class PartCategoryTree(TreeSerializer):
@ -106,7 +107,7 @@ class CategoryDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = PartCategory.objects.all() queryset = PartCategory.objects.all()
class PartAttachmentList(generics.ListCreateAPIView): class PartAttachmentList(generics.ListCreateAPIView, AttachmentMixin):
""" """
API endpoint for listing (and creating) a PartAttachment (file upload). API endpoint for listing (and creating) a PartAttachment (file upload).
""" """
@ -114,14 +115,6 @@ class PartAttachmentList(generics.ListCreateAPIView):
queryset = PartAttachment.objects.all() queryset = PartAttachment.objects.all()
serializer_class = part_serializers.PartAttachmentSerializer serializer_class = part_serializers.PartAttachmentSerializer
permission_classes = [permissions.IsAuthenticated]
filter_backends = [
DjangoFilterBackend,
filters.OrderingFilter,
filters.SearchFilter,
]
filter_fields = [ filter_fields = [
'part', 'part',
] ]
@ -296,24 +289,17 @@ class PartList(generics.ListCreateAPIView):
else: else:
return Response(data) return Response(data)
def create(self, request, *args, **kwargs): def perform_create(self, serializer):
""" Override the default 'create' behaviour: """
We wish to save the user who created this part! We wish to save the user who created this part!
Note: Implementation coped from DRF class CreateModelMixin Note: Implementation copied from DRF class CreateModelMixin
""" """
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
# Record the user who created this Part object
part = serializer.save() part = serializer.save()
part.creation_user = request.user part.creation_user = self.request.user
part.save() part.save()
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
def get_queryset(self, *args, **kwargs): def get_queryset(self, *args, **kwargs):
queryset = super().get_queryset(*args, **kwargs) queryset = super().get_queryset(*args, **kwargs)

View File

@ -0,0 +1,21 @@
# Generated by Django 3.0.5 on 2020-05-12 10:33
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),
('part', '0035_auto_20200406_0045'),
]
operations = [
migrations.AddField(
model_name='partattachment',
name='user',
field=models.ForeignKey(blank=True, help_text='User', null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 3.0.5 on 2020-05-12 10:54
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('part', '0036_partattachment_user'),
]
operations = [
migrations.AddField(
model_name='partattachment',
name='upload_date',
field=models.DateField(auto_now_add=True, null=True),
),
]

View File

@ -9,54 +9,7 @@
<hr> <hr>
<div id='attachment-buttons'> {% include "attachment_table.html" with attachments=part.attachments.all %}
<div class="btn-group">
<button type='button' class='btn btn-success' id='new-attachment'>{% trans "Add Attachment" %}</button>
</div>
</div>
<table class='table table-striped table-condensed' data-toolbar='#attachment-buttons' id='attachment-table'>
<thead>
<tr>
<th data-field='file' data-searchable='true'>{% trans "File" %}</th>
<th data-field='comment' data-searchable='true'>{% trans "Comment" %}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for attachment in part.attachments.all %}
<tr>
<td><a href='/media/{{ attachment.attachment }}'>{{ attachment.basename }}</a></td>
<td>{{ attachment.comment }}</td>
<td>
<div class='btn-group' style='float: right;'>
<button type='button' class='btn btn-default btn-glyph attachment-edit-button' url="{% url 'part-attachment-edit' attachment.id %}" data-toggle='tooltip' title='{% trans "Edit attachment" %}'>
<span class='fas fa-edit'/>
</button>
<button type='button' class='btn btn-default btn-glyph attachment-delete-button' url="{% url 'part-attachment-delete' attachment.id %}" data-toggle='tooltip' title='{% trans "Delete attachment" %}'>
<span class='fas fa-trash-alt icon-red'/>
</button>
</div>
</td>
</tr>
{% endfor %}
</tbody>
{% if part.variant_of and part.variant_of.attachments.count > 0 %}
<tr>
<td colspan='3'>
Attachments for template part <b><i>{{ part.variant_of.full_name }}</i></b>
</td>
</tr>
{% for attachment in part.variant_of.attachments.all %}
<tr>
<td><a href='/media/{{ attachment.attachment }}'>{{ attachment.basename }}</a></td>
<td>{{ attachment.comment }}</td>
<td></td>
</tr>
{% endfor %}
{% endif %}
</table>
{% endblock %} {% endblock %}
@ -73,7 +26,9 @@
$("#attachment-table").on('click', '.attachment-edit-button', function() { $("#attachment-table").on('click', '.attachment-edit-button', function() {
var button = $(this); var button = $(this);
launchModalForm(button.attr('url'), var url = `/part/attachment/${button.attr('pk')}/edit/`;
launchModalForm(url,
{ {
reload: true, reload: true,
}); });
@ -82,7 +37,9 @@
$("#attachment-table").on('click', '.attachment-delete-button', function() { $("#attachment-table").on('click', '.attachment-delete-button', function() {
var button = $(this); var button = $(this);
launchModalForm(button.attr('url'), { var url = `/part/attachment/${button.attr('pk')}/delete/`;
launchModalForm(url, {
success: function() { success: function() {
location.reload(); location.reload();
} }

View File

@ -74,6 +74,11 @@ class PartAttachmentCreate(AjaxCreateView):
ajax_form_title = _("Add part attachment") ajax_form_title = _("Add part attachment")
ajax_template_name = "modal_form.html" ajax_template_name = "modal_form.html"
def post_save(self):
""" Record the user that uploaded the attachment """
self.object.user = self.request.user
self.object.save()
def get_data(self): def get_data(self):
return { return {
'success': _('Added attachment') 'success': _('Added attachment')

View File

@ -29,6 +29,7 @@ from .serializers import StockItemAttachmentSerializer
from InvenTree.views import TreeSerializer from InvenTree.views import TreeSerializer
from InvenTree.helpers import str2bool, isNull from InvenTree.helpers import str2bool, isNull
from InvenTree.api import AttachmentMixin
from decimal import Decimal, InvalidOperation from decimal import Decimal, InvalidOperation
@ -478,6 +479,23 @@ class StockList(generics.ListCreateAPIView):
if sales_order: if sales_order:
queryset = queryset.filter(sales_order=sales_order) queryset = queryset.filter(sales_order=sales_order)
# Filter by "serialized" status?
serialized = params.get('serialized', None)
if serialized is not None:
serialized = str2bool(serialized)
if serialized:
queryset = queryset.exclude(serial=None)
else:
queryset = queryset.filter(serial=None)
# Filter by serial number?
serial_number = params.get('serial', None)
if serial_number is not None:
queryset = queryset.filter(serial=serial_number)
in_stock = self.request.query_params.get('in_stock', None) in_stock = self.request.query_params.get('in_stock', None)
@ -628,7 +646,7 @@ class StockList(generics.ListCreateAPIView):
] ]
class StockAttachmentList(generics.ListCreateAPIView): class StockAttachmentList(generics.ListCreateAPIView, AttachmentMixin):
""" """
API endpoint for listing (and creating) a StockItemAttachment (file upload) API endpoint for listing (and creating) a StockItemAttachment (file upload)
""" """
@ -636,12 +654,6 @@ class StockAttachmentList(generics.ListCreateAPIView):
queryset = StockItemAttachment.objects.all() queryset = StockItemAttachment.objects.all()
serializer_class = StockItemAttachmentSerializer serializer_class = StockItemAttachmentSerializer
filter_backends = [
DjangoFilterBackend,
filters.OrderingFilter,
filters.SearchFilter,
]
filter_fields = [ filter_fields = [
'stock_item', 'stock_item',
] ]

View File

@ -0,0 +1,21 @@
# Generated by Django 3.0.5 on 2020-05-12 10:33
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', '0036_stockitemattachment'),
]
operations = [
migrations.AddField(
model_name='stockitemattachment',
name='user',
field=models.ForeignKey(blank=True, help_text='User', null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 3.0.5 on 2020-05-12 10:54
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('stock', '0037_stockitemattachment_user'),
]
operations = [
migrations.AddField(
model_name='stockitemattachment',
name='upload_date',
field=models.DateField(auto_now_add=True, null=True),
),
]

View File

@ -193,6 +193,16 @@ class LocationSerializer(InvenTreeModelSerializer):
class StockItemAttachmentSerializer(InvenTreeModelSerializer): class StockItemAttachmentSerializer(InvenTreeModelSerializer):
""" Serializer for StockItemAttachment model """ """ Serializer for StockItemAttachment model """
def __init_(self, *args, **kwargs):
user_detail = kwargs.pop('user_detail', False)
super().__init__(*args, **kwargs)
if user_detail is not True:
self.fields.pop('user_detail')
user_detail = UserSerializerBrief(source='user', read_only=True)
class Meta: class Meta:
model = StockItemAttachment model = StockItemAttachment
@ -200,7 +210,9 @@ class StockItemAttachmentSerializer(InvenTreeModelSerializer):
'pk', 'pk',
'stock_item', 'stock_item',
'attachment', 'attachment',
'comment' 'comment',
'user',
'user_detail',
] ]

View File

@ -10,40 +10,7 @@
<hr> <hr>
<h4>{% trans "Stock Item Attachments" %}</h4> <h4>{% trans "Stock Item Attachments" %}</h4>
{% include "attachment_table.html" with attachments=item.attachments.all %}
<div id='attachment-buttons'>
<div class="btn-group">
<button type='button' class='btn btn-success' id='new-attachment'>{% trans "Add Attachment" %}</button>
</div>
</div>
<table class='table table-striped table-condensed' data-toolbar='#attachment-buttons' id='attachment-table'>
<thead>
<tr>
<th data-field='file' data-searchable='true'>{% trans "File" %}</th>
<th data-field='comment' data-searchable='true'>{% trans "Comment" %}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for attachment in item.attachments.all %}
<tr>
<td><a href='/media/{{ attachment.attachment }}'>{{ attachment.basename }}</a></td>
<td>{{ attachment.comment }}</td>
<td>
<div class='btn-group' style='float: right;'>
<button type='button' class='btn btn-default btn-glyph attachment-edit-button' url="{% url 'stock-item-attachment-edit' attachment.id %}" data-toggle='tooltip' title='{% trans "Edit attachment" %}'>
<span class='fas fa-edit'/>
</button>
<button type='button' class='btn btn-default btn-glyph attachment-delete-button' url="{% url 'stock-item-attachment-delete' attachment.id %}" data-toggle='tooltip' title='{% trans "Delete attachment" %}'>
<span class='fas fa-trash-alt icon-red'/>
</button>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %} {% endblock %}
@ -60,7 +27,9 @@ $("#new-attachment").click(function() {
$("#attachment-table").on('click', '.attachment-edit-button', function() { $("#attachment-table").on('click', '.attachment-edit-button', function() {
var button = $(this); var button = $(this);
launchModalForm(button.attr('url'), var url = `/stock/item/attachment/${button.attr('pk')}/edit/`;
launchModalForm(url,
{ {
reload: true, reload: true,
}); });
@ -69,7 +38,9 @@ $("#attachment-table").on('click', '.attachment-edit-button', function() {
$("#attachment-table").on('click', '.attachment-delete-button', function() { $("#attachment-table").on('click', '.attachment-delete-button', function() {
var button = $(this); var button = $(this);
launchModalForm(button.attr('url'), { var url = `/stock/item/attachment/${button.attr('pk')}/delete/`;
launchModalForm(url, {
success: function() { success: function() {
location.reload(); location.reload();
} }

View File

@ -160,6 +160,12 @@ class StockItemAttachmentCreate(AjaxCreateView):
ajax_form_title = _("Add Stock Item Attachment") ajax_form_title = _("Add Stock Item Attachment")
ajax_template_name = "modal_form.html" ajax_template_name = "modal_form.html"
def post_save(self, **kwargs):
""" Record the user that uploaded the attachment """
self.object.user = self.request.user
self.object.save()
def get_data(self): def get_data(self):
return { return {
'success': _("Added attachment") 'success': _("Added attachment")

View File

@ -0,0 +1,40 @@
{% load i18n %}
<div id='attachment-buttons'>
<div class='btn-group'>
<button type='button' class='btn btn-success' id='new-attachment'>{% trans "Add Attachment" %}</button>
</div>
</div>
<table class='table table-striped table-condensed' data-toolbar='#attachment-buttons' id='attachment-table'>
<thead>
<tr>
<th data-field='file' data-sortable='true' data-searchable='true'>{% trans "File" %}</th>
<th data-field='comment' data-sortable='true' data-searchable='true'>{% trans "Comment" %}</th>
<th data-field='user' data-sortable='true' data-searchable='true'>{% trans "Uploaded" %}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for attachment in attachments %}
<tr>
<td><a href='/media/{{ attachment.attachment }}'>{{ attachment.basename }}</a></td>
<td>{{ attachment.comment }}</td>
<td>
{% if attachment.upload_date %}{{ attachment.upload_date }}{% endif %}
{% if attachment.user %}<span class='badge'>{{ attachment.user.username }}</div>{% endif %}
</td>
<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'/>
</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'/>
</button>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>

View File

@ -17,6 +17,15 @@ Refer to the [getting started guide](https://inventree.github.io/docs/start/inst
For InvenTree documentation, refer to the [InvenTre documentation website](https://inventree.github.io). For InvenTree documentation, refer to the [InvenTre documentation website](https://inventree.github.io).
## Integration
InvenTree is designed to be extensible, and provides multiple options for integration with external applications or addition of custom plugins:
* [InvenTree API](https://inventree.github.io/docs/extend/api)
* [Python module](https://inventree.github.io/docs/extend/python)
* [Plugin interface](https://inventree.github.io/docs/extend/plugins)
* [Third party](https://inventree.github.io/docs/extend/integrate)
## Developer Documentation ## Developer Documentation
For code documentation, refer to the [developer documentation](http://inventree.readthedocs.io/en/latest/). For code documentation, refer to the [developer documentation](http://inventree.readthedocs.io/en/latest/).