mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-10-30 20:55:42 +00:00 
			
		
		
		
	Merge branch 'master' into partial-shipment
# Conflicts: # InvenTree/order/serializers.py
This commit is contained in:
		| @@ -21,7 +21,8 @@ from django.dispatch import receiver | ||||
| from mptt.models import MPTTModel, TreeForeignKey | ||||
| from mptt.exceptions import InvalidMove | ||||
|  | ||||
| from .validators import validate_tree_name | ||||
| from InvenTree.fields import InvenTreeURLField | ||||
| from InvenTree.validators import validate_tree_name | ||||
|  | ||||
|  | ||||
| logger = logging.getLogger('inventree') | ||||
| @@ -89,12 +90,15 @@ class ReferenceIndexingMixin(models.Model): | ||||
| class InvenTreeAttachment(models.Model): | ||||
|     """ Provides an abstracted class for managing file attachments. | ||||
|  | ||||
|     An attachment can be either an uploaded file, or an external URL | ||||
|  | ||||
|     Attributes: | ||||
|         attachment: File | ||||
|         comment: String descriptor for the attachment | ||||
|         user: User associated with file upload | ||||
|         upload_date: Date the file was uploaded | ||||
|     """ | ||||
|  | ||||
|     def getSubdir(self): | ||||
|         """ | ||||
|         Return the subdirectory under which attachments should be stored. | ||||
| @@ -103,11 +107,32 @@ class InvenTreeAttachment(models.Model): | ||||
|  | ||||
|         return "attachments" | ||||
|  | ||||
|     def save(self, *args, **kwargs): | ||||
|         # Either 'attachment' or 'link' must be specified! | ||||
|         if not self.attachment and not self.link: | ||||
|             raise ValidationError({ | ||||
|                 'attachment': _('Missing file'), | ||||
|                 'link': _('Missing external link'), | ||||
|             }) | ||||
|  | ||||
|         super().save(*args, **kwargs) | ||||
|  | ||||
|     def __str__(self): | ||||
|         return os.path.basename(self.attachment.name) | ||||
|         if self.attachment is not None: | ||||
|             return os.path.basename(self.attachment.name) | ||||
|         else: | ||||
|             return str(self.link) | ||||
|  | ||||
|     attachment = models.FileField(upload_to=rename_attachment, verbose_name=_('Attachment'), | ||||
|                                   help_text=_('Select file to attach')) | ||||
|                                   help_text=_('Select file to attach'), | ||||
|                                   blank=True, null=True | ||||
|                                   ) | ||||
|  | ||||
|     link = InvenTreeURLField( | ||||
|         blank=True, null=True, | ||||
|         verbose_name=_('Link'), | ||||
|         help_text=_('Link to external URL') | ||||
|     ) | ||||
|  | ||||
|     comment = models.CharField(blank=True, max_length=100, verbose_name=_('Comment'), help_text=_('File comment')) | ||||
|  | ||||
| @@ -123,7 +148,10 @@ class InvenTreeAttachment(models.Model): | ||||
|  | ||||
|     @property | ||||
|     def basename(self): | ||||
|         return os.path.basename(self.attachment.name) | ||||
|         if self.attachment: | ||||
|             return os.path.basename(self.attachment.name) | ||||
|         else: | ||||
|             return None | ||||
|  | ||||
|     @basename.setter | ||||
|     def basename(self, fn): | ||||
|   | ||||
| @@ -239,22 +239,6 @@ class InvenTreeModelSerializer(serializers.ModelSerializer): | ||||
|         return data | ||||
|  | ||||
|  | ||||
| class InvenTreeAttachmentSerializer(InvenTreeModelSerializer): | ||||
|     """ | ||||
|     Special case of an InvenTreeModelSerializer, which handles an "attachment" model. | ||||
|  | ||||
|     The only real addition here is that we support "renaming" of the attachment file. | ||||
|     """ | ||||
|  | ||||
|     # The 'filename' field must be present in the serializer | ||||
|     filename = serializers.CharField( | ||||
|         label=_('Filename'), | ||||
|         required=False, | ||||
|         source='basename', | ||||
|         allow_blank=False, | ||||
|     ) | ||||
|  | ||||
|  | ||||
| class InvenTreeAttachmentSerializerField(serializers.FileField): | ||||
|     """ | ||||
|     Override the DRF native FileField serializer, | ||||
| @@ -284,6 +268,27 @@ class InvenTreeAttachmentSerializerField(serializers.FileField): | ||||
|         return os.path.join(str(settings.MEDIA_URL), str(value)) | ||||
|  | ||||
|  | ||||
| class InvenTreeAttachmentSerializer(InvenTreeModelSerializer): | ||||
|     """ | ||||
|     Special case of an InvenTreeModelSerializer, which handles an "attachment" model. | ||||
|  | ||||
|     The only real addition here is that we support "renaming" of the attachment file. | ||||
|     """ | ||||
|  | ||||
|     attachment = InvenTreeAttachmentSerializerField( | ||||
|         required=False, | ||||
|         allow_null=False, | ||||
|     ) | ||||
|  | ||||
|     # The 'filename' field must be present in the serializer | ||||
|     filename = serializers.CharField( | ||||
|         label=_('Filename'), | ||||
|         required=False, | ||||
|         source='basename', | ||||
|         allow_blank=False, | ||||
|     ) | ||||
|  | ||||
|  | ||||
| class InvenTreeImageSerializerField(serializers.ImageField): | ||||
|     """ | ||||
|     Custom image serializer. | ||||
|   | ||||
| @@ -257,7 +257,7 @@ INSTALLED_APPS = [ | ||||
|     'django.contrib.admin', | ||||
|     'django.contrib.auth', | ||||
|     'django.contrib.contenttypes', | ||||
|     'django.contrib.sessions', | ||||
|     'user_sessions',                # db user sessions | ||||
|     'django.contrib.messages', | ||||
|     'django.contrib.staticfiles', | ||||
|     'django.contrib.sites', | ||||
| @@ -299,7 +299,7 @@ INSTALLED_APPS = [ | ||||
|  | ||||
| MIDDLEWARE = CONFIG.get('middleware', [ | ||||
|     'django.middleware.security.SecurityMiddleware', | ||||
|     'django.contrib.sessions.middleware.SessionMiddleware', | ||||
|     'user_sessions.middleware.SessionMiddleware',                   # db user sessions | ||||
|     'django.middleware.locale.LocaleMiddleware', | ||||
|     'django.middleware.common.CommonMiddleware', | ||||
|     'django.middleware.csrf.CsrfViewMiddleware', | ||||
| @@ -626,6 +626,12 @@ if _cache_host: | ||||
|     # as well | ||||
|     Q_CLUSTER["django_redis"] = "worker" | ||||
|  | ||||
| # database user sessions | ||||
| SESSION_ENGINE = 'user_sessions.backends.db' | ||||
| LOGOUT_REDIRECT_URL = 'index' | ||||
| SILENCED_SYSTEM_CHECKS = [ | ||||
|     'admin.E410', | ||||
| ] | ||||
|  | ||||
| # Password validation | ||||
| # https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators | ||||
|   | ||||
| @@ -38,6 +38,7 @@ from rest_framework.documentation import include_docs_urls | ||||
| from .views import auth_request | ||||
| from .views import IndexView, SearchView, DatabaseStatsView | ||||
| from .views import SettingsView, EditUserView, SetPasswordView, CustomEmailView, CustomConnectionsView, CustomPasswordResetFromKeyView | ||||
| from .views import CustomSessionDeleteView, CustomSessionDeleteOtherView | ||||
| from .views import CurrencyRefreshView | ||||
| from .views import AppearanceSelectView, SettingCategorySelectView | ||||
| from .views import DynamicJsView | ||||
| @@ -156,6 +157,10 @@ urlpatterns = [ | ||||
|  | ||||
|     url(r'^markdownx/', include('markdownx.urls')), | ||||
|  | ||||
|     # DB user sessions | ||||
|     url(r'^accounts/sessions/other/delete/$', view=CustomSessionDeleteOtherView.as_view(), name='session_delete_other', ), | ||||
|     url(r'^accounts/sessions/(?P<pk>\w+)/delete/$', view=CustomSessionDeleteView.as_view(), name='session_delete', ), | ||||
|  | ||||
|     # Single Sign On / allauth | ||||
|     # overrides of urlpatterns | ||||
|     url(r'^accounts/email/', CustomEmailView.as_view(), name='account_email'), | ||||
|   | ||||
| @@ -14,6 +14,7 @@ from django.utils.translation import gettext_lazy as _ | ||||
| from django.template.loader import render_to_string | ||||
| from django.http import HttpResponse, JsonResponse, HttpResponseRedirect | ||||
| from django.urls import reverse_lazy | ||||
| from django.utils.timezone import now | ||||
| from django.shortcuts import redirect | ||||
| from django.conf import settings | ||||
|  | ||||
| @@ -29,6 +30,7 @@ from allauth.socialaccount.forms import DisconnectForm | ||||
| from allauth.account.models import EmailAddress | ||||
| from allauth.account.views import EmailView, PasswordResetFromKeyView | ||||
| from allauth.socialaccount.views import ConnectionsView | ||||
| from user_sessions.views import SessionDeleteView, SessionDeleteOtherView | ||||
|  | ||||
| from common.settings import currency_code_default, currency_codes | ||||
|  | ||||
| @@ -733,6 +735,10 @@ class SettingsView(TemplateView): | ||||
|         ctx["request"] = self.request | ||||
|         ctx['social_form'] = DisconnectForm(request=self.request) | ||||
|  | ||||
|         # user db sessions | ||||
|         ctx['session_key'] = self.request.session.session_key | ||||
|         ctx['session_list'] = self.request.user.session_set.filter(expire_date__gt=now()).order_by('-last_activity') | ||||
|  | ||||
|         return ctx | ||||
|  | ||||
|  | ||||
| @@ -766,6 +772,20 @@ class CustomPasswordResetFromKeyView(PasswordResetFromKeyView): | ||||
|     success_url = reverse_lazy("account_login") | ||||
|  | ||||
|  | ||||
| class UserSessionOverride(): | ||||
|     """overrides sucessurl to lead to settings""" | ||||
|     def get_success_url(self): | ||||
|         return str(reverse_lazy('settings')) | ||||
|  | ||||
|  | ||||
| class CustomSessionDeleteView(UserSessionOverride, SessionDeleteView): | ||||
|     pass | ||||
|  | ||||
|  | ||||
| class CustomSessionDeleteOtherView(UserSessionOverride, SessionDeleteOtherView): | ||||
|     pass | ||||
|  | ||||
|  | ||||
| class CurrencyRefreshView(RedirectView): | ||||
|     """ | ||||
|     POST endpoint to refresh / update exchange rates | ||||
|   | ||||
							
								
								
									
										25
									
								
								InvenTree/build/migrations/0033_auto_20211128_0151.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								InvenTree/build/migrations/0033_auto_20211128_0151.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| # Generated by Django 3.2.5 on 2021-11-28 01:51 | ||||
|  | ||||
| import InvenTree.fields | ||||
| import InvenTree.models | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('build', '0032_auto_20211014_0632'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name='buildorderattachment', | ||||
|             name='link', | ||||
|             field=InvenTree.fields.InvenTreeURLField(blank=True, help_text='Link to external URL', null=True, verbose_name='Link'), | ||||
|         ), | ||||
|         migrations.AlterField( | ||||
|             model_name='buildorderattachment', | ||||
|             name='attachment', | ||||
|             field=models.FileField(blank=True, help_text='Select file to attach', null=True, upload_to=InvenTree.models.rename_attachment, verbose_name='Attachment'), | ||||
|         ), | ||||
|     ] | ||||
| @@ -16,7 +16,7 @@ from rest_framework import serializers | ||||
| from rest_framework.serializers import ValidationError | ||||
|  | ||||
| from InvenTree.serializers import InvenTreeModelSerializer, InvenTreeAttachmentSerializer | ||||
| from InvenTree.serializers import InvenTreeAttachmentSerializerField, UserSerializerBrief | ||||
| from InvenTree.serializers import UserSerializerBrief | ||||
|  | ||||
| import InvenTree.helpers | ||||
| from InvenTree.serializers import InvenTreeDecimalField | ||||
| @@ -516,8 +516,6 @@ class BuildAttachmentSerializer(InvenTreeAttachmentSerializer): | ||||
|     Serializer for a BuildAttachment | ||||
|     """ | ||||
|  | ||||
|     attachment = InvenTreeAttachmentSerializerField(required=True) | ||||
|  | ||||
|     class Meta: | ||||
|         model = BuildOrderAttachment | ||||
|  | ||||
| @@ -525,6 +523,7 @@ class BuildAttachmentSerializer(InvenTreeAttachmentSerializer): | ||||
|             'pk', | ||||
|             'build', | ||||
|             'attachment', | ||||
|             'link', | ||||
|             'filename', | ||||
|             'comment', | ||||
|             'upload_date', | ||||
|   | ||||
| @@ -431,53 +431,17 @@ enableDragAndDrop( | ||||
|     } | ||||
| ); | ||||
|  | ||||
| // Callback for creating a new attachment | ||||
| $('#new-attachment').click(function() { | ||||
|  | ||||
|     constructForm('{% url "api-build-attachment-list" %}', { | ||||
|         fields: { | ||||
|             attachment: {}, | ||||
|             comment: {}, | ||||
|             build: { | ||||
|                 value: {{ build.pk }}, | ||||
|                 hidden: true, | ||||
|             } | ||||
|         }, | ||||
|         method: 'POST', | ||||
|         onSuccess: reloadAttachmentTable, | ||||
|         title: '{% trans "Add Attachment" %}', | ||||
|     }); | ||||
| }); | ||||
|  | ||||
| loadAttachmentTable( | ||||
|     '{% url "api-build-attachment-list" %}', | ||||
|     { | ||||
|         filters: { | ||||
|             build: {{ build.pk }}, | ||||
|         }, | ||||
|         onEdit: function(pk) { | ||||
|             var url = `/api/build/attachment/${pk}/`; | ||||
|  | ||||
|             constructForm(url, { | ||||
|                 fields: { | ||||
|                     filename: {}, | ||||
|                     comment: {}, | ||||
|                 }, | ||||
|                 onSuccess: reloadAttachmentTable, | ||||
|                 title: '{% trans "Edit Attachment" %}', | ||||
|             }); | ||||
|         }, | ||||
|         onDelete: function(pk) { | ||||
|  | ||||
|             constructForm(`/api/build/attachment/${pk}/`, { | ||||
|                 method: 'DELETE', | ||||
|                 confirmMessage: '{% trans "Confirm Delete Operation" %}', | ||||
|                 title: '{% trans "Delete Attachment" %}', | ||||
|                 onSuccess: reloadAttachmentTable, | ||||
|             }); | ||||
| loadAttachmentTable('{% url "api-build-attachment-list" %}', { | ||||
|     filters: { | ||||
|         build: {{ build.pk }}, | ||||
|     }, | ||||
|     fields: { | ||||
|         build: { | ||||
|             value: {{ build.pk }}, | ||||
|             hidden: true, | ||||
|         } | ||||
|     } | ||||
| ); | ||||
| }); | ||||
|  | ||||
| $('#edit-notes').click(function() { | ||||
|     constructForm('{% url "api-build-detail" build.pk %}', { | ||||
|   | ||||
							
								
								
									
										35
									
								
								InvenTree/order/migrations/0053_auto_20211128_0151.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								InvenTree/order/migrations/0053_auto_20211128_0151.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | ||||
| # Generated by Django 3.2.5 on 2021-11-28 01:51 | ||||
|  | ||||
| import InvenTree.fields | ||||
| import InvenTree.models | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('order', '0052_auto_20211014_0631'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name='purchaseorderattachment', | ||||
|             name='link', | ||||
|             field=InvenTree.fields.InvenTreeURLField(blank=True, help_text='Link to external URL', null=True, verbose_name='Link'), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='salesorderattachment', | ||||
|             name='link', | ||||
|             field=InvenTree.fields.InvenTreeURLField(blank=True, help_text='Link to external URL', null=True, verbose_name='Link'), | ||||
|         ), | ||||
|         migrations.AlterField( | ||||
|             model_name='purchaseorderattachment', | ||||
|             name='attachment', | ||||
|             field=models.FileField(blank=True, help_text='Select file to attach', null=True, upload_to=InvenTree.models.rename_attachment, verbose_name='Attachment'), | ||||
|         ), | ||||
|         migrations.AlterField( | ||||
|             model_name='salesorderattachment', | ||||
|             name='attachment', | ||||
|             field=models.FileField(blank=True, help_text='Select file to attach', null=True, upload_to=InvenTree.models.rename_attachment, verbose_name='Attachment'), | ||||
|         ), | ||||
|     ] | ||||
| @@ -13,7 +13,7 @@ class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         migrations.swappable_dependency(settings.AUTH_USER_MODEL), | ||||
|         ('order', '0052_auto_20211014_0631'), | ||||
|         ('order', '0053_auto_20211128_0151'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|   | ||||
| @@ -0,0 +1,18 @@ | ||||
| # Generated by Django 3.2.5 on 2021-11-29 11:58 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('order', '0058_auto_20211126_1210'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name='salesordershipment', | ||||
|             name='tracking_number', | ||||
|             field=models.CharField(blank=True, help_text='Shipment tracking information', max_length=100, verbose_name='Tracking Number'), | ||||
|         ), | ||||
|     ] | ||||
| @@ -971,6 +971,35 @@ class SalesOrderShipment(models.Model): | ||||
|         help_text=_('Shipment notes'), | ||||
|     ) | ||||
|  | ||||
|     tracking_number = models.CharField( | ||||
|         max_length=100, | ||||
|         blank=True, | ||||
|         unique=False, | ||||
|         verbose_name=_('Tracking Number'), | ||||
|         help_text=_('Shipment tracking information'), | ||||
|     ) | ||||
|  | ||||
|     @transaction.atomic | ||||
|     def complete_shipment(self): | ||||
|         """ | ||||
|         Complete this particular shipment: | ||||
|  | ||||
|         1. Update any stock items associated with this shipment | ||||
|         2. Update the "shipped" quantity of all associated line items | ||||
|         3. Set the "shipment_date" to now | ||||
|         """ | ||||
|  | ||||
|         # Iterate through each stock item assigned to this shipment | ||||
|         for allocation in self.allocations.all(): | ||||
|             pass | ||||
|  | ||||
|  | ||||
|  | ||||
|         # Update the "shipment" date  | ||||
|         self.shipment_date = datetime.now() | ||||
|         self.save() | ||||
|  | ||||
|  | ||||
|  | ||||
| class SalesOrderAllocation(models.Model): | ||||
|     """ | ||||
|   | ||||
| @@ -18,8 +18,8 @@ from rest_framework.serializers import ValidationError | ||||
| from sql_util.utils import SubqueryCount | ||||
|  | ||||
| from common.settings import currency_code_mappings | ||||
|  | ||||
| from company.serializers import CompanyBriefSerializer, SupplierPartSerializer | ||||
|  | ||||
| from InvenTree.serializers import InvenTreeAttachmentSerializer | ||||
| from InvenTree.helpers import normalize | ||||
| from InvenTree.serializers import InvenTreeModelSerializer | ||||
| @@ -35,6 +35,8 @@ from part.serializers import PartBriefSerializer | ||||
| import stock.models | ||||
| import stock.serializers | ||||
|  | ||||
| from users.serializers import OwnerSerializer | ||||
|  | ||||
|  | ||||
| class POSerializer(InvenTreeModelSerializer): | ||||
|     """ | ||||
| @@ -84,6 +86,8 @@ class POSerializer(InvenTreeModelSerializer): | ||||
|  | ||||
|     reference = serializers.CharField(required=True) | ||||
|  | ||||
|     responsible_detail = OwnerSerializer(source='responsible', read_only=True, many=False) | ||||
|  | ||||
|     class Meta: | ||||
|         model = order.models.PurchaseOrder | ||||
|  | ||||
| @@ -98,6 +102,7 @@ class POSerializer(InvenTreeModelSerializer): | ||||
|             'overdue', | ||||
|             'reference', | ||||
|             'responsible', | ||||
|             'responsible_detail', | ||||
|             'supplier', | ||||
|             'supplier_detail', | ||||
|             'supplier_reference', | ||||
| @@ -372,8 +377,6 @@ class POAttachmentSerializer(InvenTreeAttachmentSerializer): | ||||
|     Serializers for the PurchaseOrderAttachment model | ||||
|     """ | ||||
|  | ||||
|     attachment = InvenTreeAttachmentSerializerField(required=True) | ||||
|  | ||||
|     class Meta: | ||||
|         model = order.models.PurchaseOrderAttachment | ||||
|  | ||||
| @@ -381,6 +384,7 @@ class POAttachmentSerializer(InvenTreeAttachmentSerializer): | ||||
|             'pk', | ||||
|             'order', | ||||
|             'attachment', | ||||
|             'link', | ||||
|             'filename', | ||||
|             'comment', | ||||
|             'upload_date', | ||||
| @@ -608,6 +612,7 @@ class SalesOrderShipmentSerializer(InvenTreeModelSerializer): | ||||
|             'shipment_date', | ||||
|             'checked_by', | ||||
|             'reference', | ||||
|             'tracking_number', | ||||
|             'notes', | ||||
|         ] | ||||
|  | ||||
| @@ -771,8 +776,6 @@ class SOAttachmentSerializer(InvenTreeAttachmentSerializer): | ||||
|     Serializers for the SalesOrderAttachment model | ||||
|     """ | ||||
|  | ||||
|     attachment = InvenTreeAttachmentSerializerField(required=True) | ||||
|  | ||||
|     class Meta: | ||||
|         model = order.models.SalesOrderAttachment | ||||
|  | ||||
| @@ -781,6 +784,7 @@ class SOAttachmentSerializer(InvenTreeAttachmentSerializer): | ||||
|             'order', | ||||
|             'attachment', | ||||
|             'filename', | ||||
|             'link', | ||||
|             'comment', | ||||
|             'upload_date', | ||||
|         ] | ||||
|   | ||||
| @@ -124,51 +124,16 @@ | ||||
|         } | ||||
|     ); | ||||
|  | ||||
|     loadAttachmentTable( | ||||
|         '{% url "api-po-attachment-list" %}', | ||||
|         { | ||||
|             filters: { | ||||
|                 order: {{ order.pk }}, | ||||
|             }, | ||||
|             onEdit: function(pk) { | ||||
|                 var url = `/api/order/po/attachment/${pk}/`; | ||||
|  | ||||
|                 constructForm(url, { | ||||
|                     fields: { | ||||
|                         filename: {}, | ||||
|                         comment: {}, | ||||
|                     }, | ||||
|                     onSuccess: reloadAttachmentTable, | ||||
|                     title: '{% trans "Edit Attachment" %}', | ||||
|                 }); | ||||
|             }, | ||||
|             onDelete: function(pk) { | ||||
|  | ||||
|                 constructForm(`/api/order/po/attachment/${pk}/`, { | ||||
|                     method: 'DELETE', | ||||
|                     confirmMessage: '{% trans "Confirm Delete Operation" %}', | ||||
|                     title: '{% trans "Delete Attachment" %}', | ||||
|                     onSuccess: reloadAttachmentTable, | ||||
|                 }); | ||||
|     loadAttachmentTable('{% url "api-po-attachment-list" %}', { | ||||
|         filters: { | ||||
|             order: {{ order.pk }}, | ||||
|         }, | ||||
|         fields: { | ||||
|             order: { | ||||
|                 value: {{ order.pk }}, | ||||
|                 hidden: true, | ||||
|             } | ||||
|         } | ||||
|     ); | ||||
|  | ||||
|     $("#new-attachment").click(function() { | ||||
|  | ||||
|         constructForm('{% url "api-po-attachment-list" %}', { | ||||
|             method: 'POST', | ||||
|             fields: { | ||||
|                 attachment: {}, | ||||
|                 comment: {}, | ||||
|                 order: { | ||||
|                     value: {{ order.pk }}, | ||||
|                     hidden: true, | ||||
|                 }, | ||||
|             }, | ||||
|             reload: true, | ||||
|             title: '{% trans "Add Attachment" %}', | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     loadStockTable($("#stock-table"), { | ||||
|   | ||||
| @@ -194,55 +194,21 @@ | ||||
|             }, | ||||
|             label: 'attachment', | ||||
|             success: function(data, status, xhr) { | ||||
|                 location.reload(); | ||||
|                 reloadAttachmentTable(); | ||||
|             } | ||||
|         } | ||||
|     ); | ||||
|  | ||||
|     loadAttachmentTable( | ||||
|         '{% url "api-so-attachment-list" %}', | ||||
|         { | ||||
|             filters: { | ||||
|                 order: {{ order.pk }}, | ||||
|     loadAttachmentTable('{% url "api-so-attachment-list" %}', { | ||||
|         filters: { | ||||
|             order: {{ order.pk }}, | ||||
|         }, | ||||
|         fields: { | ||||
|             order: { | ||||
|                 value: {{ order.pk }}, | ||||
|                 hidden: true, | ||||
|             }, | ||||
|             onEdit: function(pk) { | ||||
|                 var url = `/api/order/so/attachment/${pk}/`; | ||||
|  | ||||
|                 constructForm(url, { | ||||
|                     fields: { | ||||
|                         filename: {}, | ||||
|                         comment: {}, | ||||
|                     }, | ||||
|                     onSuccess: reloadAttachmentTable, | ||||
|                     title: '{% trans "Edit Attachment" %}', | ||||
|                 }); | ||||
|             }, | ||||
|             onDelete: function(pk) { | ||||
|                 constructForm(`/api/order/so/attachment/${pk}/`, { | ||||
|                     method: 'DELETE', | ||||
|                     confirmMessage: '{% trans "Confirm Delete Operation" %}', | ||||
|                     title: '{% trans "Delete Attachment" %}', | ||||
|                     onSuccess: reloadAttachmentTable, | ||||
|                 }); | ||||
|             } | ||||
|         } | ||||
|     ); | ||||
|  | ||||
|     $("#new-attachment").click(function() { | ||||
|  | ||||
|         constructForm('{% url "api-so-attachment-list" %}', { | ||||
|             method: 'POST', | ||||
|             fields: { | ||||
|                 attachment: {}, | ||||
|                 comment: {}, | ||||
|                 order: { | ||||
|                     value: {{ order.pk }}, | ||||
|                     hidden: true | ||||
|                 } | ||||
|             }, | ||||
|             onSuccess: reloadAttachmentTable, | ||||
|             title: '{% trans "Add Attachment" %}' | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     loadBuildTable($("#builds-table"), { | ||||
|   | ||||
| @@ -42,7 +42,7 @@ from build.models import Build | ||||
|  | ||||
| from . import serializers as part_serializers | ||||
|  | ||||
| from InvenTree.helpers import str2bool, isNull | ||||
| from InvenTree.helpers import str2bool, isNull, increment | ||||
| from InvenTree.api import AttachmentMixin | ||||
|  | ||||
| from InvenTree.status_codes import BuildStatus | ||||
| @@ -410,6 +410,33 @@ class PartThumbsUpdate(generics.RetrieveUpdateAPIView): | ||||
|     ] | ||||
|  | ||||
|  | ||||
| class PartSerialNumberDetail(generics.RetrieveAPIView): | ||||
|     """ | ||||
|     API endpoint for returning extra serial number information about a particular part | ||||
|     """ | ||||
|  | ||||
|     queryset = Part.objects.all() | ||||
|  | ||||
|     def retrieve(self, request, *args, **kwargs): | ||||
|  | ||||
|         part = self.get_object() | ||||
|  | ||||
|         # Calculate the "latest" serial number | ||||
|         latest = part.getLatestSerialNumber() | ||||
|  | ||||
|         data = { | ||||
|             'latest': latest, | ||||
|         } | ||||
|  | ||||
|         if latest is not None: | ||||
|             next = increment(latest) | ||||
|  | ||||
|             if next != increment: | ||||
|                 data['next'] = next | ||||
|  | ||||
|         return Response(data) | ||||
|  | ||||
|  | ||||
| class PartDetail(generics.RetrieveUpdateDestroyAPIView): | ||||
|     """ API endpoint for detail view of a single Part object """ | ||||
|  | ||||
| @@ -1532,7 +1559,14 @@ part_api_urls = [ | ||||
|         url(r'^(?P<pk>\d+)/?', PartThumbsUpdate.as_view(), name='api-part-thumbs-update'), | ||||
|     ])), | ||||
|  | ||||
|     url(r'^(?P<pk>\d+)/', PartDetail.as_view(), name='api-part-detail'), | ||||
|     url(r'^(?P<pk>\d+)/', include([ | ||||
|  | ||||
|         # Endpoint for extra serial number information | ||||
|         url(r'^serial-numbers/', PartSerialNumberDetail.as_view(), name='api-part-serial-number-detail'), | ||||
|  | ||||
|         # Part detail endpoint | ||||
|         url(r'^.*$', PartDetail.as_view(), name='api-part-detail'), | ||||
|     ])), | ||||
|  | ||||
|     url(r'^.*$', PartList.as_view(), name='api-part-list'), | ||||
| ] | ||||
|   | ||||
							
								
								
									
										25
									
								
								InvenTree/part/migrations/0075_auto_20211128_0151.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								InvenTree/part/migrations/0075_auto_20211128_0151.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| # Generated by Django 3.2.5 on 2021-11-28 01:51 | ||||
|  | ||||
| import InvenTree.fields | ||||
| import InvenTree.models | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('part', '0074_partcategorystar'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name='partattachment', | ||||
|             name='link', | ||||
|             field=InvenTree.fields.InvenTreeURLField(blank=True, help_text='Link to external URL', null=True, verbose_name='Link'), | ||||
|         ), | ||||
|         migrations.AlterField( | ||||
|             model_name='partattachment', | ||||
|             name='attachment', | ||||
|             field=models.FileField(blank=True, help_text='Select file to attach', null=True, upload_to=InvenTree.models.rename_attachment, verbose_name='Attachment'), | ||||
|         ), | ||||
|     ] | ||||
| @@ -75,8 +75,6 @@ class PartAttachmentSerializer(InvenTreeAttachmentSerializer): | ||||
|     Serializer for the PartAttachment class | ||||
|     """ | ||||
|  | ||||
|     attachment = InvenTreeAttachmentSerializerField(required=True) | ||||
|  | ||||
|     class Meta: | ||||
|         model = PartAttachment | ||||
|  | ||||
| @@ -85,6 +83,7 @@ class PartAttachmentSerializer(InvenTreeAttachmentSerializer): | ||||
|             'part', | ||||
|             'attachment', | ||||
|             'filename', | ||||
|             'link', | ||||
|             'comment', | ||||
|             'upload_date', | ||||
|         ] | ||||
|   | ||||
| @@ -999,36 +999,17 @@ | ||||
|     }); | ||||
|  | ||||
|     onPanelLoad("part-attachments", function() { | ||||
|         loadAttachmentTable( | ||||
|             '{% url "api-part-attachment-list" %}', | ||||
|             { | ||||
|                 filters: { | ||||
|                     part: {{ part.pk }}, | ||||
|                 }, | ||||
|                 onEdit: function(pk) { | ||||
|                     var url = `/api/part/attachment/${pk}/`; | ||||
|      | ||||
|                     constructForm(url, { | ||||
|                         fields: { | ||||
|                             filename: {}, | ||||
|                             comment: {}, | ||||
|                         }, | ||||
|                         title: '{% trans "Edit Attachment" %}', | ||||
|                         onSuccess: reloadAttachmentTable, | ||||
|                     }); | ||||
|                 }, | ||||
|                 onDelete: function(pk) { | ||||
|                     var url = `/api/part/attachment/${pk}/`; | ||||
|      | ||||
|                     constructForm(url, { | ||||
|                         method: 'DELETE', | ||||
|                         confirmMessage: '{% trans "Confirm Delete Operation" %}', | ||||
|                         title: '{% trans "Delete Attachment" %}', | ||||
|                         onSuccess: reloadAttachmentTable, | ||||
|                     }); | ||||
|         loadAttachmentTable('{% url "api-part-attachment-list" %}', { | ||||
|             filters: { | ||||
|                 part: {{ part.pk }}, | ||||
|             }, | ||||
|             fields: { | ||||
|                 part: { | ||||
|                     value: {{ part.pk }}, | ||||
|                     hidden: true | ||||
|                 } | ||||
|             } | ||||
|         ); | ||||
|         }); | ||||
|      | ||||
|         enableDragAndDrop( | ||||
|             '#attachment-dropzone', | ||||
| @@ -1043,26 +1024,6 @@ | ||||
|                 } | ||||
|             } | ||||
|         ); | ||||
|      | ||||
|         $("#new-attachment").click(function() { | ||||
|      | ||||
|             constructForm( | ||||
|                 '{% url "api-part-attachment-list" %}', | ||||
|                 { | ||||
|                     method: 'POST', | ||||
|                     fields: { | ||||
|                         attachment: {}, | ||||
|                         comment: {}, | ||||
|                         part: { | ||||
|                             value: {{ part.pk }}, | ||||
|                             hidden: true, | ||||
|                         } | ||||
|                     }, | ||||
|                     onSuccess: reloadAttachmentTable, | ||||
|                     title: '{% trans "Add Attachment" %}', | ||||
|                 } | ||||
|             ) | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|  | ||||
|   | ||||
							
								
								
									
										25
									
								
								InvenTree/stock/migrations/0070_auto_20211128_0151.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								InvenTree/stock/migrations/0070_auto_20211128_0151.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| # Generated by Django 3.2.5 on 2021-11-28 01:51 | ||||
|  | ||||
| import InvenTree.fields | ||||
| import InvenTree.models | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('stock', '0069_auto_20211109_2347'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name='stockitemattachment', | ||||
|             name='link', | ||||
|             field=InvenTree.fields.InvenTreeURLField(blank=True, help_text='Link to external URL', null=True, verbose_name='Link'), | ||||
|         ), | ||||
|         migrations.AlterField( | ||||
|             model_name='stockitemattachment', | ||||
|             name='attachment', | ||||
|             field=models.FileField(blank=True, help_text='Select file to attach', null=True, upload_to=InvenTree.models.rename_attachment, verbose_name='Attachment'), | ||||
|         ), | ||||
|     ] | ||||
| @@ -420,8 +420,6 @@ class StockItemAttachmentSerializer(InvenTree.serializers.InvenTreeAttachmentSer | ||||
|  | ||||
|     user_detail = InvenTree.serializers.UserSerializerBrief(source='user', read_only=True) | ||||
|  | ||||
|     attachment = InvenTree.serializers.InvenTreeAttachmentSerializerField(required=True) | ||||
|  | ||||
|     # TODO: Record the uploading user when creating or updating an attachment! | ||||
|  | ||||
|     class Meta: | ||||
| @@ -432,6 +430,7 @@ class StockItemAttachmentSerializer(InvenTree.serializers.InvenTreeAttachmentSer | ||||
|             'stock_item', | ||||
|             'attachment', | ||||
|             'filename', | ||||
|             'link', | ||||
|             'comment', | ||||
|             'upload_date', | ||||
|             'user', | ||||
|   | ||||
| @@ -221,55 +221,16 @@ | ||||
|             } | ||||
|         ); | ||||
|  | ||||
|     loadAttachmentTable( | ||||
|         '{% url "api-stock-attachment-list" %}', | ||||
|         { | ||||
|             filters: { | ||||
|                 stock_item: {{ item.pk }}, | ||||
|             }, | ||||
|             onEdit: function(pk) { | ||||
|                 var url = `/api/stock/attachment/${pk}/`; | ||||
|  | ||||
|                 constructForm(url, { | ||||
|                     fields: { | ||||
|                         filename: {}, | ||||
|                         comment: {}, | ||||
|                     }, | ||||
|                     title: '{% trans "Edit Attachment" %}', | ||||
|                     onSuccess: reloadAttachmentTable  | ||||
|                 }); | ||||
|             }, | ||||
|             onDelete: function(pk) { | ||||
|                 var url = `/api/stock/attachment/${pk}/`; | ||||
|  | ||||
|                 constructForm(url, { | ||||
|                     method: 'DELETE', | ||||
|                     confirmMessage: '{% trans "Confirm Delete Operation" %}', | ||||
|                     title: '{% trans "Delete Attachment" %}', | ||||
|                     onSuccess: reloadAttachmentTable, | ||||
|                 }); | ||||
|     loadAttachmentTable('{% url "api-stock-attachment-list" %}', { | ||||
|         filters: { | ||||
|             stock_item: {{ item.pk }}, | ||||
|         }, | ||||
|         fields: { | ||||
|             stock_item: { | ||||
|                 value: {{ item.pk }}, | ||||
|                 hidden: true, | ||||
|             } | ||||
|         } | ||||
|     ); | ||||
|  | ||||
|     $("#new-attachment").click(function() { | ||||
|  | ||||
|         constructForm( | ||||
|             '{% url "api-stock-attachment-list" %}', | ||||
|             { | ||||
|                 method: 'POST', | ||||
|                 fields: { | ||||
|                     attachment: {}, | ||||
|                     comment: {}, | ||||
|                     stock_item: { | ||||
|                         value: {{ item.pk }}, | ||||
|                         hidden: true, | ||||
|                     }, | ||||
|                 }, | ||||
|                 reload: true, | ||||
|                 title: '{% trans "Add Attachment" %}', | ||||
|             } | ||||
|         ); | ||||
|     }); | ||||
|  | ||||
|     loadStockTestResultsTable( | ||||
|   | ||||
| @@ -442,6 +442,7 @@ | ||||
| $("#stock-serialize").click(function() { | ||||
|  | ||||
|     serializeStockItem({{ item.pk }}, { | ||||
|         part: {{ item.part.pk }}, | ||||
|         reload: true, | ||||
|         data: { | ||||
|             quantity: {{ item.quantity }}, | ||||
|   | ||||
| @@ -4,6 +4,7 @@ | ||||
| {% load inventree_extras %} | ||||
| {% load socialaccount %} | ||||
| {% load crispy_forms_tags %} | ||||
| {% load user_sessions i18n %} | ||||
|  | ||||
| {% block label %}account{% endblock %} | ||||
|  | ||||
| @@ -14,12 +15,12 @@ | ||||
| {% block actions %} | ||||
| {% inventree_demo_mode as demo %} | ||||
| {% if not demo %} | ||||
| <div class='btn btn-outline-primary' type='button' id='edit-password' title='{% trans "Change Password" %}'> | ||||
|     <span class='fas fa-key'></span> {% trans "Set Password" %} | ||||
| </div> | ||||
| <div class='btn btn-primary' type='button' id='edit-user' title='{% trans "Edit User Information" %}'> | ||||
|     <span class='fas fa-user-cog'></span> {% trans "Edit" %} | ||||
| </div> | ||||
| <div class='btn btn-primary' type='button' id='edit-password' title='{% trans "Change Password" %}'> | ||||
|     <span class='fas fa-key'></span> {% trans "Set Password" %} | ||||
| </div> | ||||
| {% endif %} | ||||
| {% endblock %} | ||||
|  | ||||
| @@ -174,58 +175,48 @@ | ||||
| </div> | ||||
|  | ||||
| <div class='panel-heading'> | ||||
|     <h4>{% trans "Language Settings" %}</h4> | ||||
| </div> | ||||
|  | ||||
| <div class="row"> | ||||
|     <div class="col"> | ||||
|         <form action="{% url 'set_language' %}" method="post"> | ||||
|             {% csrf_token %} | ||||
|             <input name="next" type="hidden" value="{% url 'settings' %}"> | ||||
|             <label for='language' class=' requiredField'> | ||||
|                 {% trans "Select language" %} | ||||
|             </label> | ||||
|             <div class='form-group input-group mb-3'> | ||||
|                 <select name="language" class="select form-control w-25"> | ||||
|                     {% get_current_language as LANGUAGE_CODE %} | ||||
|                     {% get_available_languages as LANGUAGES %} | ||||
|                     {% get_language_info_list for LANGUAGES as languages %} | ||||
|                     {% if 'alllang' in request.GET %}{% define True as ALL_LANG %}{% endif %} | ||||
|                     {% for language in languages %} | ||||
|                         {% define language.code as lang_code %} | ||||
|                         {% define locale_stats|keyvalue:lang_code as lang_translated %} | ||||
|                         {% if lang_translated > 10 or lang_code == 'en' or lang_code == LANGUAGE_CODE %}{% define True as use_lang %}{% else %}{% define False as use_lang %}{% endif %} | ||||
|                         {% if ALL_LANG or use_lang  %} | ||||
|                         <option value="{{ lang_code }}"{% if lang_code == LANGUAGE_CODE %} selected{% endif %}> | ||||
|                             {{ language.name_local }} ({{ lang_code }})  | ||||
|                             {% if lang_translated %} | ||||
|                                 {% blocktrans %}{{ lang_translated }}% translated{% endblocktrans %} | ||||
|                             {% else %} | ||||
|                                 {% if lang_code == 'en' %}-{% else %}{% trans 'No translations available' %}{% endif %} | ||||
|                             {% endif %} | ||||
|                         </option> | ||||
|                         {% endif %} | ||||
|                     {% endfor %} | ||||
|                 </select> | ||||
|                 <div class='input-group-append'> | ||||
|                     <input type="submit" value="{% trans 'Set Language' %}" class="btn btn btn-primary"> | ||||
|                 </div> | ||||
|             </div> | ||||
|             <p>{% trans "Some languages are not complete" %} | ||||
|             {% if ALL_LANG %} | ||||
|             . <a href="{% url 'settings' %}">{% trans "Show only sufficent" %}</a> | ||||
|             {% else %} | ||||
|             {% trans "and hidden." %} <a href="?alllang">{% trans "Show them too" %}</a> | ||||
|     <div class='d-flex flex-wrap'> | ||||
|         <h4>{% trans "Active Sessions" %}</h4> | ||||
|         {% include "spacer.html" %} | ||||
|         <div class='btn-group' role='group'> | ||||
|             {% if session_list.count > 1 %} | ||||
|             <form method="post" action="{% url 'session_delete_other' %}"> | ||||
|                 {% csrf_token %} | ||||
|                 <button type="submit" class="btn btn-sm btn-default btn-danger" title='{% trans "Log out active sessions (except this one)" %}'> | ||||
|                     {% trans "Log Out Active Sessions" %} | ||||
|                 </button> | ||||
|             </form> | ||||
|             {% endif %} | ||||
|             </p> | ||||
|     </form> | ||||
|     </div> | ||||
|     <div class="col-sm-6"> | ||||
|         <h4>{% trans "Help the translation efforts!" %}</h4> | ||||
|         <p>{% blocktrans with link="https://crowdin.com/project/inventree" %}Native language translation of the InvenTree web application is <a href="{{link}}">community contributed via crowdin</a>. Contributions are welcomed and encouraged.{% endblocktrans %}</p> | ||||
|         </div> | ||||
|     </div> | ||||
| </div> | ||||
|  | ||||
| <div> | ||||
|     {% trans "<em>unknown on unknown</em>" as unknown_on_unknown %} | ||||
|     {% trans "<em>unknown</em>" as unknown %} | ||||
|     <table class="table table-striped table-condensed"> | ||||
|     <thead> | ||||
|         <tr> | ||||
|         <th>{% trans "IP Address" %}</th> | ||||
|         <th>{% trans "Device" %}</th> | ||||
|         <th>{% trans "Last Activity" %}</th> | ||||
|         </tr> | ||||
|     </thead> | ||||
|     {% for object in session_list %} | ||||
|         <tr {% if object.session_key == session_key %}class="active"{% endif %}> | ||||
|         <td>{{ object.ip }}</td> | ||||
|         <td>{{ object.user_agent|device|default_if_none:unknown_on_unknown|safe }}</td> | ||||
|         <td> | ||||
|             {% if object.session_key == session_key %} | ||||
|             {% blocktrans with time=object.last_activity|timesince %}{{ time }} ago (this session){% endblocktrans %} | ||||
|             {% else %} | ||||
|             {% blocktrans with time=object.last_activity|timesince %}{{ time }} ago{% endblocktrans %} | ||||
|             {% endif %} | ||||
|         </td> | ||||
|         </tr> | ||||
|     {% endfor %} | ||||
|     </table> | ||||
| </div> | ||||
| {% endblock %} | ||||
|  | ||||
| {% block js_ready %} | ||||
|   | ||||
| @@ -50,4 +50,57 @@ | ||||
|     </div> | ||||
| </div> | ||||
|  | ||||
| <div class='panel-heading'> | ||||
|     <h4>{% trans "Language Settings" %}</h4> | ||||
| </div> | ||||
|  | ||||
| <div class="row"> | ||||
|     <div class="col"> | ||||
|         <form action="{% url 'set_language' %}" method="post"> | ||||
|             {% csrf_token %} | ||||
|             <input name="next" type="hidden" value="{% url 'settings' %}"> | ||||
|             <label for='language' class=' requiredField'> | ||||
|                 {% trans "Select language" %} | ||||
|             </label> | ||||
|             <div class='form-group input-group mb-3'> | ||||
|                 <select name="language" class="select form-control w-25"> | ||||
|                     {% get_current_language as LANGUAGE_CODE %} | ||||
|                     {% get_available_languages as LANGUAGES %} | ||||
|                     {% get_language_info_list for LANGUAGES as languages %} | ||||
|                     {% if 'alllang' in request.GET %}{% define True as ALL_LANG %}{% endif %} | ||||
|                     {% for language in languages %} | ||||
|                         {% define language.code as lang_code %} | ||||
|                         {% define locale_stats|keyvalue:lang_code as lang_translated %} | ||||
|                         {% if lang_translated > 10 or lang_code == 'en' or lang_code == LANGUAGE_CODE %}{% define True as use_lang %}{% else %}{% define False as use_lang %}{% endif %} | ||||
|                         {% if ALL_LANG or use_lang  %} | ||||
|                         <option value="{{ lang_code }}"{% if lang_code == LANGUAGE_CODE %} selected{% endif %}> | ||||
|                             {{ language.name_local }} ({{ lang_code }})  | ||||
|                             {% if lang_translated %} | ||||
|                                 {% blocktrans %}{{ lang_translated }}% translated{% endblocktrans %} | ||||
|                             {% else %} | ||||
|                                 {% if lang_code == 'en' %}-{% else %}{% trans 'No translations available' %}{% endif %} | ||||
|                             {% endif %} | ||||
|                         </option> | ||||
|                         {% endif %} | ||||
|                     {% endfor %} | ||||
|                 </select> | ||||
|                 <div class='input-group-append'> | ||||
|                     <input type="submit" value="{% trans 'Set Language' %}" class="btn btn btn-primary"> | ||||
|                 </div> | ||||
|             </div> | ||||
|             <p>{% trans "Some languages are not complete" %} | ||||
|             {% if ALL_LANG %} | ||||
|             . <a href="{% url 'settings' %}">{% trans "Show only sufficent" %}</a> | ||||
|             {% else %} | ||||
|             {% trans "and hidden." %} <a href="?alllang">{% trans "Show them too" %}</a> | ||||
|             {% endif %} | ||||
|             </p> | ||||
|     </form> | ||||
|     </div> | ||||
|     <div class="col-sm-6"> | ||||
|         <h4>{% trans "Help the translation efforts!" %}</h4> | ||||
|         <p>{% blocktrans with link="https://crowdin.com/project/inventree" %}Native language translation of the InvenTree web application is <a href="{{link}}">community contributed via crowdin</a>. Contributions are welcomed and encouraged.{% endblocktrans %}</p> | ||||
|     </div> | ||||
| </div> | ||||
|  | ||||
| {% endblock %} | ||||
| @@ -1,5 +1,8 @@ | ||||
| {% load i18n %} | ||||
|  | ||||
| <button type='button' class='btn btn-outline-success' id='new-attachment-link'> | ||||
|     <span class='fas fa-link'></span> {% trans "Add Link" %} | ||||
| </button> | ||||
| <button type='button' class='btn btn-success' id='new-attachment'> | ||||
|     <span class='fas fa-plus-circle'></span> {% trans "Add Attachment" %} | ||||
| </button> | ||||
| @@ -54,6 +54,7 @@ function inventreeGet(url, filters={}, options={}) { | ||||
|         data: filters, | ||||
|         dataType: 'json', | ||||
|         contentType: 'application/json', | ||||
|         async: (options.async == false) ? false : true, | ||||
|         success: function(response) { | ||||
|             if (options.success) { | ||||
|                 options.success(response); | ||||
|   | ||||
| @@ -6,10 +6,57 @@ | ||||
| */ | ||||
|  | ||||
| /* exported | ||||
|     addAttachmentButtonCallbacks, | ||||
|     loadAttachmentTable, | ||||
|     reloadAttachmentTable, | ||||
| */ | ||||
|  | ||||
|  | ||||
| /* | ||||
|  * Add callbacks to buttons for creating new attachments. | ||||
|  *  | ||||
|  * Note: Attachments can also be external links! | ||||
|  */ | ||||
| function addAttachmentButtonCallbacks(url, fields={}) { | ||||
|  | ||||
|     // Callback for 'new attachment' button | ||||
|     $('#new-attachment').click(function() { | ||||
|  | ||||
|         var file_fields = { | ||||
|             attachment: {}, | ||||
|             comment: {}, | ||||
|         }; | ||||
|  | ||||
|         Object.assign(file_fields, fields); | ||||
|  | ||||
|         constructForm(url, { | ||||
|             fields: file_fields, | ||||
|             method: 'POST', | ||||
|             onSuccess: reloadAttachmentTable, | ||||
|             title: '{% trans "Add Attachment" %}', | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     // Callback for 'new link' button | ||||
|     $('#new-attachment-link').click(function() { | ||||
|  | ||||
|         var link_fields = { | ||||
|             link: {}, | ||||
|             comment: {}, | ||||
|         }; | ||||
|  | ||||
|         Object.assign(link_fields, fields); | ||||
|          | ||||
|         constructForm(url, { | ||||
|             fields: link_fields, | ||||
|             method: 'POST', | ||||
|             onSuccess: reloadAttachmentTable, | ||||
|             title: '{% trans "Add Link" %}', | ||||
|         }); | ||||
|     }); | ||||
| } | ||||
|  | ||||
|  | ||||
| function reloadAttachmentTable() { | ||||
|  | ||||
|     $('#attachment-table').bootstrapTable('refresh'); | ||||
| @@ -20,6 +67,8 @@ function loadAttachmentTable(url, options) { | ||||
|  | ||||
|     var table = options.table || '#attachment-table'; | ||||
|  | ||||
|     addAttachmentButtonCallbacks(url, options.fields || {}); | ||||
|  | ||||
|     $(table).inventreeTable({ | ||||
|         url: url, | ||||
|         name: options.name || 'attachments', | ||||
| @@ -34,56 +83,77 @@ function loadAttachmentTable(url, options) { | ||||
|             $(table).find('.button-attachment-edit').click(function() { | ||||
|                 var pk = $(this).attr('pk'); | ||||
|  | ||||
|                 if (options.onEdit) { | ||||
|                     options.onEdit(pk); | ||||
|                 } | ||||
|                 constructForm(`${url}${pk}/`, { | ||||
|                     fields: { | ||||
|                         link: {}, | ||||
|                         comment: {},  | ||||
|                     }, | ||||
|                     processResults: function(data, fields, opts) { | ||||
|                         // Remove the "link" field if the attachment is a file! | ||||
|                         if (data.attachment) { | ||||
|                             delete opts.fields.link; | ||||
|                         } | ||||
|                     }, | ||||
|                     onSuccess: reloadAttachmentTable, | ||||
|                     title: '{% trans "Edit Attachment" %}', | ||||
|                 }); | ||||
|             }); | ||||
|              | ||||
|             // Add callback for 'delete' button | ||||
|             $(table).find('.button-attachment-delete').click(function() { | ||||
|                 var pk = $(this).attr('pk'); | ||||
|  | ||||
|                 if (options.onDelete) { | ||||
|                     options.onDelete(pk); | ||||
|                 } | ||||
|                 constructForm(`${url}${pk}/`, { | ||||
|                     method: 'DELETE', | ||||
|                     confirmMessage: '{% trans "Confirm Delete" %}', | ||||
|                     title: '{% trans "Delete Attachment" %}', | ||||
|                     onSuccess: reloadAttachmentTable, | ||||
|                 }); | ||||
|             }); | ||||
|         }, | ||||
|         columns: [ | ||||
|             { | ||||
|                 field: 'attachment', | ||||
|                 title: '{% trans "File" %}', | ||||
|                 formatter: function(value) { | ||||
|                 title: '{% trans "Attachment" %}', | ||||
|                 formatter: function(value, row) { | ||||
|  | ||||
|                     var icon = 'fa-file-alt'; | ||||
|                     if (row.attachment) { | ||||
|                         var icon = 'fa-file-alt'; | ||||
|  | ||||
|                     var fn = value.toLowerCase(); | ||||
|                         var fn = value.toLowerCase(); | ||||
|  | ||||
|                     if (fn.endsWith('.csv')) { | ||||
|                         icon = 'fa-file-csv'; | ||||
|                     } else if (fn.endsWith('.pdf')) { | ||||
|                         icon = 'fa-file-pdf'; | ||||
|                     } else if (fn.endsWith('.xls') || fn.endsWith('.xlsx')) { | ||||
|                         icon = 'fa-file-excel'; | ||||
|                     } else if (fn.endsWith('.doc') || fn.endsWith('.docx')) { | ||||
|                         icon = 'fa-file-word'; | ||||
|                     } else if (fn.endsWith('.zip') || fn.endsWith('.7z')) { | ||||
|                         icon = 'fa-file-archive'; | ||||
|                         if (fn.endsWith('.csv')) { | ||||
|                             icon = 'fa-file-csv'; | ||||
|                         } else if (fn.endsWith('.pdf')) { | ||||
|                             icon = 'fa-file-pdf'; | ||||
|                         } else if (fn.endsWith('.xls') || fn.endsWith('.xlsx')) { | ||||
|                             icon = 'fa-file-excel'; | ||||
|                         } else if (fn.endsWith('.doc') || fn.endsWith('.docx')) { | ||||
|                             icon = 'fa-file-word'; | ||||
|                         } else if (fn.endsWith('.zip') || fn.endsWith('.7z')) { | ||||
|                             icon = 'fa-file-archive'; | ||||
|                         } else { | ||||
|                             var images = ['.png', '.jpg', '.bmp', '.gif', '.svg', '.tif']; | ||||
|  | ||||
|                             images.forEach(function(suffix) { | ||||
|                                 if (fn.endsWith(suffix)) { | ||||
|                                     icon = 'fa-file-image'; | ||||
|                                 } | ||||
|                             }); | ||||
|                         } | ||||
|  | ||||
|                         var split = value.split('/'); | ||||
|                         var filename = split[split.length - 1]; | ||||
|  | ||||
|                         var html = `<span class='fas ${icon}'></span> ${filename}`; | ||||
|  | ||||
|                         return renderLink(html, value); | ||||
|                     } else if (row.link) { | ||||
|                         var html = `<span class='fas fa-link'></span> ${row.link}`; | ||||
|                         return renderLink(html, row.link); | ||||
|                     } else { | ||||
|                         var images = ['.png', '.jpg', '.bmp', '.gif', '.svg', '.tif']; | ||||
|  | ||||
|                         images.forEach(function(suffix) { | ||||
|                             if (fn.endsWith(suffix)) { | ||||
|                                 icon = 'fa-file-image'; | ||||
|                             } | ||||
|                         }); | ||||
|                         return '-'; | ||||
|                     } | ||||
|  | ||||
|                     var split = value.split('/'); | ||||
|                     var filename = split[split.length - 1]; | ||||
|  | ||||
|                     var html = `<span class='fas ${icon}'></span> ${filename}`; | ||||
|  | ||||
|                     return renderLink(html, value); | ||||
|                 } | ||||
|             }, | ||||
|             { | ||||
|   | ||||
| @@ -28,6 +28,7 @@ | ||||
|     disableFormInput, | ||||
|     enableFormInput, | ||||
|     hideFormInput, | ||||
|     setFormInputPlaceholder, | ||||
|     setFormGroupVisibility, | ||||
|     showFormInput, | ||||
| */ | ||||
| @@ -1276,6 +1277,11 @@ function initializeGroups(fields, options) { | ||||
|     } | ||||
| } | ||||
|  | ||||
| // Set the placeholder value for a field | ||||
| function setFormInputPlaceholder(name, placeholder, options) { | ||||
|     $(options.modal).find(`#id_${name}`).attr('placeholder', placeholder); | ||||
| } | ||||
|  | ||||
| // Clear a form input | ||||
| function clearFormInput(name, options) { | ||||
|     updateFieldValue(name, null, {}, options); | ||||
|   | ||||
| @@ -729,6 +729,23 @@ function loadPurchaseOrderTable(table, options) { | ||||
|                 title: '{% trans "Items" %}', | ||||
|                 sortable: true, | ||||
|             }, | ||||
|             { | ||||
|                 field: 'responsible', | ||||
|                 title: '{% trans "Responsible" %}', | ||||
|                 switchable: true, | ||||
|                 sortable: false, | ||||
|                 formatter: function(value, row) { | ||||
|                     var html = row.responsible_detail.name; | ||||
|  | ||||
|                     if (row.responsible_detail.label == 'group') { | ||||
|                         html += `<span class='float-right fas fa-users'></span>`; | ||||
|                     } else { | ||||
|                         html += `<span class='float-right fas fa-user'></span>`; | ||||
|                     } | ||||
|  | ||||
|                     return html; | ||||
|                 } | ||||
|             }, | ||||
|         ], | ||||
|     }); | ||||
| } | ||||
|   | ||||
| @@ -80,6 +80,20 @@ function serializeStockItem(pk, options={}) { | ||||
|         notes: {}, | ||||
|     }; | ||||
|  | ||||
|     if (options.part) { | ||||
|         // Work out the next available serial number | ||||
|         inventreeGet(`/api/part/${options.part}/serial-numbers/`, {}, { | ||||
|             success: function(data) { | ||||
|                 if (data.next) { | ||||
|                     options.fields.serial_numbers.placeholder = `{% trans "Next available serial number" %}: ${data.next}`; | ||||
|                 } else if (data.latest) { | ||||
|                     options.fields.serial_numbers.placeholder = `{% trans "Latest serial number" %}: ${data.latest}`; | ||||
|                 } | ||||
|             }, | ||||
|             async: false, | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     constructForm(url, options); | ||||
| } | ||||
|  | ||||
| @@ -144,10 +158,26 @@ function stockItemFields(options={}) { | ||||
|                     // If a "trackable" part is selected, enable serial number field | ||||
|                     if (data.trackable) { | ||||
|                         enableFormInput('serial_numbers', opts); | ||||
|                         // showFormInput('serial_numbers', opts); | ||||
|  | ||||
|                         // Request part serial number information from the server | ||||
|                         inventreeGet(`/api/part/${data.pk}/serial-numbers/`, {}, { | ||||
|                             success: function(data) { | ||||
|                                 var placeholder = ''; | ||||
|                                 if (data.next) { | ||||
|                                     placeholder = `{% trans "Next available serial number" %}: ${data.next}`; | ||||
|                                 } else if (data.latest) { | ||||
|                                     placeholder = `{% trans "Latest serial number" %}: ${data.latest}`; | ||||
|                                 } | ||||
|  | ||||
|                                 setFormInputPlaceholder('serial_numbers', placeholder, opts); | ||||
|                             } | ||||
|                         }); | ||||
|  | ||||
|                     } else { | ||||
|                         clearFormInput('serial_numbers', opts); | ||||
|                         disableFormInput('serial_numbers', opts); | ||||
|  | ||||
|                         setFormInputPlaceholder('serial_numbers', '{% trans "This part cannot be serialized" %}', opts); | ||||
|                     } | ||||
|  | ||||
|                     // Enable / disable fields based on purchaseable status | ||||
|   | ||||
| @@ -146,7 +146,6 @@ class RuleSet(models.Model): | ||||
|         # Core django models (not user configurable) | ||||
|         'admin_logentry', | ||||
|         'contenttypes_contenttype', | ||||
|         'sessions_session', | ||||
|  | ||||
|         # Models which currently do not require permissions | ||||
|         'common_colortheme', | ||||
| @@ -160,6 +159,7 @@ class RuleSet(models.Model): | ||||
|         'error_report_error', | ||||
|         'exchange_rate', | ||||
|         'exchange_exchangebackend', | ||||
|         'user_sessions_session', | ||||
|  | ||||
|         # Django-q | ||||
|         'django_q_ormq', | ||||
|   | ||||
| @@ -23,6 +23,7 @@ django-q==1.3.4                 # Background task scheduling | ||||
| django-sql-utils==0.5.0         # Advanced query annotation / aggregation | ||||
| django-stdimage==5.1.1          # Advanced ImageField management | ||||
| django-test-migrations==1.1.0   # Unit testing for database migrations | ||||
| django-user-sessions==1.7.1     # user sessions in DB | ||||
| django-weasyprint==1.0.1        # django weasyprint integration | ||||
| djangorestframework==3.12.4     # DRF framework | ||||
| flake8==3.8.3                   # PEP checking | ||||
|   | ||||
							
								
								
									
										2
									
								
								tasks.py
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								tasks.py
									
									
									
									
									
								
							| @@ -279,7 +279,6 @@ def content_excludes(): | ||||
|  | ||||
|     excludes = [ | ||||
|         "contenttypes", | ||||
|         "sessions.session", | ||||
|         "auth.permission", | ||||
|         "authtoken.token", | ||||
|         "error_report.error", | ||||
| @@ -291,6 +290,7 @@ def content_excludes(): | ||||
|         "exchange.rate", | ||||
|         "exchange.exchangebackend", | ||||
|         "common.notificationentry", | ||||
|         "user_sessions.session", | ||||
|     ] | ||||
|  | ||||
|     output = "" | ||||
|   | ||||
		Reference in New Issue
	
	Block a user