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.models import MPTTModel, TreeForeignKey | ||||||
| from mptt.exceptions import InvalidMove | 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') | logger = logging.getLogger('inventree') | ||||||
| @@ -89,12 +90,15 @@ class ReferenceIndexingMixin(models.Model): | |||||||
| class InvenTreeAttachment(models.Model): | class InvenTreeAttachment(models.Model): | ||||||
|     """ Provides an abstracted class for managing file attachments. |     """ Provides an abstracted class for managing file attachments. | ||||||
|  |  | ||||||
|  |     An attachment can be either an uploaded file, or an external URL | ||||||
|  |  | ||||||
|     Attributes: |     Attributes: | ||||||
|         attachment: File |         attachment: File | ||||||
|         comment: String descriptor for the attachment |         comment: String descriptor for the attachment | ||||||
|         user: User associated with file upload |         user: User associated with file upload | ||||||
|         upload_date: Date the file was uploaded |         upload_date: Date the file was uploaded | ||||||
|     """ |     """ | ||||||
|  |  | ||||||
|     def getSubdir(self): |     def getSubdir(self): | ||||||
|         """ |         """ | ||||||
|         Return the subdirectory under which attachments should be stored. |         Return the subdirectory under which attachments should be stored. | ||||||
| @@ -103,11 +107,32 @@ class InvenTreeAttachment(models.Model): | |||||||
|  |  | ||||||
|         return "attachments" |         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): |     def __str__(self): | ||||||
|  |         if self.attachment is not None: | ||||||
|             return os.path.basename(self.attachment.name) |             return os.path.basename(self.attachment.name) | ||||||
|  |         else: | ||||||
|  |             return str(self.link) | ||||||
|  |  | ||||||
|     attachment = models.FileField(upload_to=rename_attachment, verbose_name=_('Attachment'), |     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')) |     comment = models.CharField(blank=True, max_length=100, verbose_name=_('Comment'), help_text=_('File comment')) | ||||||
|  |  | ||||||
| @@ -123,7 +148,10 @@ class InvenTreeAttachment(models.Model): | |||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def basename(self): |     def basename(self): | ||||||
|  |         if self.attachment: | ||||||
|             return os.path.basename(self.attachment.name) |             return os.path.basename(self.attachment.name) | ||||||
|  |         else: | ||||||
|  |             return None | ||||||
|  |  | ||||||
|     @basename.setter |     @basename.setter | ||||||
|     def basename(self, fn): |     def basename(self, fn): | ||||||
|   | |||||||
| @@ -239,22 +239,6 @@ class InvenTreeModelSerializer(serializers.ModelSerializer): | |||||||
|         return data |         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): | class InvenTreeAttachmentSerializerField(serializers.FileField): | ||||||
|     """ |     """ | ||||||
|     Override the DRF native FileField serializer, |     Override the DRF native FileField serializer, | ||||||
| @@ -284,6 +268,27 @@ class InvenTreeAttachmentSerializerField(serializers.FileField): | |||||||
|         return os.path.join(str(settings.MEDIA_URL), str(value)) |         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): | class InvenTreeImageSerializerField(serializers.ImageField): | ||||||
|     """ |     """ | ||||||
|     Custom image serializer. |     Custom image serializer. | ||||||
|   | |||||||
| @@ -257,7 +257,7 @@ INSTALLED_APPS = [ | |||||||
|     'django.contrib.admin', |     'django.contrib.admin', | ||||||
|     'django.contrib.auth', |     'django.contrib.auth', | ||||||
|     'django.contrib.contenttypes', |     'django.contrib.contenttypes', | ||||||
|     'django.contrib.sessions', |     'user_sessions',                # db user sessions | ||||||
|     'django.contrib.messages', |     'django.contrib.messages', | ||||||
|     'django.contrib.staticfiles', |     'django.contrib.staticfiles', | ||||||
|     'django.contrib.sites', |     'django.contrib.sites', | ||||||
| @@ -299,7 +299,7 @@ INSTALLED_APPS = [ | |||||||
|  |  | ||||||
| MIDDLEWARE = CONFIG.get('middleware', [ | MIDDLEWARE = CONFIG.get('middleware', [ | ||||||
|     'django.middleware.security.SecurityMiddleware', |     'django.middleware.security.SecurityMiddleware', | ||||||
|     'django.contrib.sessions.middleware.SessionMiddleware', |     'user_sessions.middleware.SessionMiddleware',                   # db user sessions | ||||||
|     'django.middleware.locale.LocaleMiddleware', |     'django.middleware.locale.LocaleMiddleware', | ||||||
|     'django.middleware.common.CommonMiddleware', |     'django.middleware.common.CommonMiddleware', | ||||||
|     'django.middleware.csrf.CsrfViewMiddleware', |     'django.middleware.csrf.CsrfViewMiddleware', | ||||||
| @@ -626,6 +626,12 @@ if _cache_host: | |||||||
|     # as well |     # as well | ||||||
|     Q_CLUSTER["django_redis"] = "worker" |     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 | # Password validation | ||||||
| # https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators | # 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 auth_request | ||||||
| from .views import IndexView, SearchView, DatabaseStatsView | from .views import IndexView, SearchView, DatabaseStatsView | ||||||
| from .views import SettingsView, EditUserView, SetPasswordView, CustomEmailView, CustomConnectionsView, CustomPasswordResetFromKeyView | from .views import SettingsView, EditUserView, SetPasswordView, CustomEmailView, CustomConnectionsView, CustomPasswordResetFromKeyView | ||||||
|  | from .views import CustomSessionDeleteView, CustomSessionDeleteOtherView | ||||||
| from .views import CurrencyRefreshView | from .views import CurrencyRefreshView | ||||||
| from .views import AppearanceSelectView, SettingCategorySelectView | from .views import AppearanceSelectView, SettingCategorySelectView | ||||||
| from .views import DynamicJsView | from .views import DynamicJsView | ||||||
| @@ -156,6 +157,10 @@ urlpatterns = [ | |||||||
|  |  | ||||||
|     url(r'^markdownx/', include('markdownx.urls')), |     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 |     # Single Sign On / allauth | ||||||
|     # overrides of urlpatterns |     # overrides of urlpatterns | ||||||
|     url(r'^accounts/email/', CustomEmailView.as_view(), name='account_email'), |     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.template.loader import render_to_string | ||||||
| from django.http import HttpResponse, JsonResponse, HttpResponseRedirect | from django.http import HttpResponse, JsonResponse, HttpResponseRedirect | ||||||
| from django.urls import reverse_lazy | from django.urls import reverse_lazy | ||||||
|  | from django.utils.timezone import now | ||||||
| from django.shortcuts import redirect | from django.shortcuts import redirect | ||||||
| from django.conf import settings | from django.conf import settings | ||||||
|  |  | ||||||
| @@ -29,6 +30,7 @@ from allauth.socialaccount.forms import DisconnectForm | |||||||
| from allauth.account.models import EmailAddress | from allauth.account.models import EmailAddress | ||||||
| from allauth.account.views import EmailView, PasswordResetFromKeyView | from allauth.account.views import EmailView, PasswordResetFromKeyView | ||||||
| from allauth.socialaccount.views import ConnectionsView | from allauth.socialaccount.views import ConnectionsView | ||||||
|  | from user_sessions.views import SessionDeleteView, SessionDeleteOtherView | ||||||
|  |  | ||||||
| from common.settings import currency_code_default, currency_codes | from common.settings import currency_code_default, currency_codes | ||||||
|  |  | ||||||
| @@ -733,6 +735,10 @@ class SettingsView(TemplateView): | |||||||
|         ctx["request"] = self.request |         ctx["request"] = self.request | ||||||
|         ctx['social_form'] = DisconnectForm(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 |         return ctx | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -766,6 +772,20 @@ class CustomPasswordResetFromKeyView(PasswordResetFromKeyView): | |||||||
|     success_url = reverse_lazy("account_login") |     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): | class CurrencyRefreshView(RedirectView): | ||||||
|     """ |     """ | ||||||
|     POST endpoint to refresh / update exchange rates |     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 rest_framework.serializers import ValidationError | ||||||
|  |  | ||||||
| from InvenTree.serializers import InvenTreeModelSerializer, InvenTreeAttachmentSerializer | from InvenTree.serializers import InvenTreeModelSerializer, InvenTreeAttachmentSerializer | ||||||
| from InvenTree.serializers import InvenTreeAttachmentSerializerField, UserSerializerBrief | from InvenTree.serializers import UserSerializerBrief | ||||||
|  |  | ||||||
| import InvenTree.helpers | import InvenTree.helpers | ||||||
| from InvenTree.serializers import InvenTreeDecimalField | from InvenTree.serializers import InvenTreeDecimalField | ||||||
| @@ -516,8 +516,6 @@ class BuildAttachmentSerializer(InvenTreeAttachmentSerializer): | |||||||
|     Serializer for a BuildAttachment |     Serializer for a BuildAttachment | ||||||
|     """ |     """ | ||||||
|  |  | ||||||
|     attachment = InvenTreeAttachmentSerializerField(required=True) |  | ||||||
|  |  | ||||||
|     class Meta: |     class Meta: | ||||||
|         model = BuildOrderAttachment |         model = BuildOrderAttachment | ||||||
|  |  | ||||||
| @@ -525,6 +523,7 @@ class BuildAttachmentSerializer(InvenTreeAttachmentSerializer): | |||||||
|             'pk', |             'pk', | ||||||
|             'build', |             'build', | ||||||
|             'attachment', |             'attachment', | ||||||
|  |             'link', | ||||||
|             'filename', |             'filename', | ||||||
|             'comment', |             'comment', | ||||||
|             'upload_date', |             'upload_date', | ||||||
|   | |||||||
| @@ -431,53 +431,17 @@ enableDragAndDrop( | |||||||
|     } |     } | ||||||
| ); | ); | ||||||
|  |  | ||||||
| // Callback for creating a new attachment | loadAttachmentTable('{% url "api-build-attachment-list" %}', { | ||||||
| $('#new-attachment').click(function() { |     filters: { | ||||||
|  |         build: {{ build.pk }}, | ||||||
|     constructForm('{% url "api-build-attachment-list" %}', { |     }, | ||||||
|     fields: { |     fields: { | ||||||
|             attachment: {}, |  | ||||||
|             comment: {}, |  | ||||||
|         build: { |         build: { | ||||||
|             value: {{ build.pk }}, |             value: {{ build.pk }}, | ||||||
|             hidden: true, |             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, |  | ||||||
|             }); |  | ||||||
|     } |     } | ||||||
|     } | }); | ||||||
| ); |  | ||||||
|  |  | ||||||
| $('#edit-notes').click(function() { | $('#edit-notes').click(function() { | ||||||
|     constructForm('{% url "api-build-detail" build.pk %}', { |     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 = [ |     dependencies = [ | ||||||
|         migrations.swappable_dependency(settings.AUTH_USER_MODEL), |         migrations.swappable_dependency(settings.AUTH_USER_MODEL), | ||||||
|         ('order', '0052_auto_20211014_0631'), |         ('order', '0053_auto_20211128_0151'), | ||||||
|     ] |     ] | ||||||
|  |  | ||||||
|     operations = [ |     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'), |         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): | class SalesOrderAllocation(models.Model): | ||||||
|     """ |     """ | ||||||
|   | |||||||
| @@ -18,8 +18,8 @@ from rest_framework.serializers import ValidationError | |||||||
| from sql_util.utils import SubqueryCount | from sql_util.utils import SubqueryCount | ||||||
|  |  | ||||||
| from common.settings import currency_code_mappings | from common.settings import currency_code_mappings | ||||||
|  |  | ||||||
| from company.serializers import CompanyBriefSerializer, SupplierPartSerializer | from company.serializers import CompanyBriefSerializer, SupplierPartSerializer | ||||||
|  |  | ||||||
| from InvenTree.serializers import InvenTreeAttachmentSerializer | from InvenTree.serializers import InvenTreeAttachmentSerializer | ||||||
| from InvenTree.helpers import normalize | from InvenTree.helpers import normalize | ||||||
| from InvenTree.serializers import InvenTreeModelSerializer | from InvenTree.serializers import InvenTreeModelSerializer | ||||||
| @@ -35,6 +35,8 @@ from part.serializers import PartBriefSerializer | |||||||
| import stock.models | import stock.models | ||||||
| import stock.serializers | import stock.serializers | ||||||
|  |  | ||||||
|  | from users.serializers import OwnerSerializer | ||||||
|  |  | ||||||
|  |  | ||||||
| class POSerializer(InvenTreeModelSerializer): | class POSerializer(InvenTreeModelSerializer): | ||||||
|     """ |     """ | ||||||
| @@ -84,6 +86,8 @@ class POSerializer(InvenTreeModelSerializer): | |||||||
|  |  | ||||||
|     reference = serializers.CharField(required=True) |     reference = serializers.CharField(required=True) | ||||||
|  |  | ||||||
|  |     responsible_detail = OwnerSerializer(source='responsible', read_only=True, many=False) | ||||||
|  |  | ||||||
|     class Meta: |     class Meta: | ||||||
|         model = order.models.PurchaseOrder |         model = order.models.PurchaseOrder | ||||||
|  |  | ||||||
| @@ -98,6 +102,7 @@ class POSerializer(InvenTreeModelSerializer): | |||||||
|             'overdue', |             'overdue', | ||||||
|             'reference', |             'reference', | ||||||
|             'responsible', |             'responsible', | ||||||
|  |             'responsible_detail', | ||||||
|             'supplier', |             'supplier', | ||||||
|             'supplier_detail', |             'supplier_detail', | ||||||
|             'supplier_reference', |             'supplier_reference', | ||||||
| @@ -372,8 +377,6 @@ class POAttachmentSerializer(InvenTreeAttachmentSerializer): | |||||||
|     Serializers for the PurchaseOrderAttachment model |     Serializers for the PurchaseOrderAttachment model | ||||||
|     """ |     """ | ||||||
|  |  | ||||||
|     attachment = InvenTreeAttachmentSerializerField(required=True) |  | ||||||
|  |  | ||||||
|     class Meta: |     class Meta: | ||||||
|         model = order.models.PurchaseOrderAttachment |         model = order.models.PurchaseOrderAttachment | ||||||
|  |  | ||||||
| @@ -381,6 +384,7 @@ class POAttachmentSerializer(InvenTreeAttachmentSerializer): | |||||||
|             'pk', |             'pk', | ||||||
|             'order', |             'order', | ||||||
|             'attachment', |             'attachment', | ||||||
|  |             'link', | ||||||
|             'filename', |             'filename', | ||||||
|             'comment', |             'comment', | ||||||
|             'upload_date', |             'upload_date', | ||||||
| @@ -608,6 +612,7 @@ class SalesOrderShipmentSerializer(InvenTreeModelSerializer): | |||||||
|             'shipment_date', |             'shipment_date', | ||||||
|             'checked_by', |             'checked_by', | ||||||
|             'reference', |             'reference', | ||||||
|  |             'tracking_number', | ||||||
|             'notes', |             'notes', | ||||||
|         ] |         ] | ||||||
|  |  | ||||||
| @@ -771,8 +776,6 @@ class SOAttachmentSerializer(InvenTreeAttachmentSerializer): | |||||||
|     Serializers for the SalesOrderAttachment model |     Serializers for the SalesOrderAttachment model | ||||||
|     """ |     """ | ||||||
|  |  | ||||||
|     attachment = InvenTreeAttachmentSerializerField(required=True) |  | ||||||
|  |  | ||||||
|     class Meta: |     class Meta: | ||||||
|         model = order.models.SalesOrderAttachment |         model = order.models.SalesOrderAttachment | ||||||
|  |  | ||||||
| @@ -781,6 +784,7 @@ class SOAttachmentSerializer(InvenTreeAttachmentSerializer): | |||||||
|             'order', |             'order', | ||||||
|             'attachment', |             'attachment', | ||||||
|             'filename', |             'filename', | ||||||
|  |             'link', | ||||||
|             'comment', |             'comment', | ||||||
|             'upload_date', |             'upload_date', | ||||||
|         ] |         ] | ||||||
|   | |||||||
| @@ -124,51 +124,16 @@ | |||||||
|         } |         } | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     loadAttachmentTable( |     loadAttachmentTable('{% url "api-po-attachment-list" %}', { | ||||||
|         '{% url "api-po-attachment-list" %}', |  | ||||||
|         { |  | ||||||
|         filters: { |         filters: { | ||||||
|             order: {{ order.pk }}, |             order: {{ order.pk }}, | ||||||
|         }, |         }, | ||||||
|             onEdit: function(pk) { |  | ||||||
|                 var url = `/api/order/po/attachment/${pk}/`; |  | ||||||
|  |  | ||||||
|                 constructForm(url, { |  | ||||||
|         fields: { |         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, |  | ||||||
|                 }); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     ); |  | ||||||
|  |  | ||||||
|     $("#new-attachment").click(function() { |  | ||||||
|  |  | ||||||
|         constructForm('{% url "api-po-attachment-list" %}', { |  | ||||||
|             method: 'POST', |  | ||||||
|             fields: { |  | ||||||
|                 attachment: {}, |  | ||||||
|                 comment: {}, |  | ||||||
|             order: { |             order: { | ||||||
|                 value: {{ order.pk }}, |                 value: {{ order.pk }}, | ||||||
|                 hidden: true, |                 hidden: true, | ||||||
|                 }, |             } | ||||||
|             }, |         } | ||||||
|             reload: true, |  | ||||||
|             title: '{% trans "Add Attachment" %}', |  | ||||||
|         }); |  | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     loadStockTable($("#stock-table"), { |     loadStockTable($("#stock-table"), { | ||||||
|   | |||||||
| @@ -194,55 +194,21 @@ | |||||||
|             }, |             }, | ||||||
|             label: 'attachment', |             label: 'attachment', | ||||||
|             success: function(data, status, xhr) { |             success: function(data, status, xhr) { | ||||||
|                 location.reload(); |                 reloadAttachmentTable(); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     loadAttachmentTable( |     loadAttachmentTable('{% url "api-so-attachment-list" %}', { | ||||||
|         '{% url "api-so-attachment-list" %}', |  | ||||||
|         { |  | ||||||
|         filters: { |         filters: { | ||||||
|             order: {{ order.pk }}, |             order: {{ order.pk }}, | ||||||
|         }, |         }, | ||||||
|             onEdit: function(pk) { |  | ||||||
|                 var url = `/api/order/so/attachment/${pk}/`; |  | ||||||
|  |  | ||||||
|                 constructForm(url, { |  | ||||||
|         fields: { |         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: { |             order: { | ||||||
|                 value: {{ order.pk }}, |                 value: {{ order.pk }}, | ||||||
|                     hidden: true |                 hidden: true, | ||||||
|                 } |  | ||||||
|             }, |             }, | ||||||
|             onSuccess: reloadAttachmentTable, |         } | ||||||
|             title: '{% trans "Add Attachment" %}' |  | ||||||
|         }); |  | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     loadBuildTable($("#builds-table"), { |     loadBuildTable($("#builds-table"), { | ||||||
|   | |||||||
| @@ -42,7 +42,7 @@ from build.models import Build | |||||||
|  |  | ||||||
| from . import serializers as part_serializers | 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.api import AttachmentMixin | ||||||
|  |  | ||||||
| from InvenTree.status_codes import BuildStatus | 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): | class PartDetail(generics.RetrieveUpdateDestroyAPIView): | ||||||
|     """ API endpoint for detail view of a single Part object """ |     """ 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+)/?', 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'), |     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 |     Serializer for the PartAttachment class | ||||||
|     """ |     """ | ||||||
|  |  | ||||||
|     attachment = InvenTreeAttachmentSerializerField(required=True) |  | ||||||
|  |  | ||||||
|     class Meta: |     class Meta: | ||||||
|         model = PartAttachment |         model = PartAttachment | ||||||
|  |  | ||||||
| @@ -85,6 +83,7 @@ class PartAttachmentSerializer(InvenTreeAttachmentSerializer): | |||||||
|             'part', |             'part', | ||||||
|             'attachment', |             'attachment', | ||||||
|             'filename', |             'filename', | ||||||
|  |             'link', | ||||||
|             'comment', |             'comment', | ||||||
|             'upload_date', |             'upload_date', | ||||||
|         ] |         ] | ||||||
|   | |||||||
| @@ -999,36 +999,17 @@ | |||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     onPanelLoad("part-attachments", function() { |     onPanelLoad("part-attachments", function() { | ||||||
|         loadAttachmentTable( |         loadAttachmentTable('{% url "api-part-attachment-list" %}', { | ||||||
|             '{% url "api-part-attachment-list" %}', |  | ||||||
|             { |  | ||||||
|             filters: { |             filters: { | ||||||
|                 part: {{ part.pk }}, |                 part: {{ part.pk }}, | ||||||
|             }, |             }, | ||||||
|                 onEdit: function(pk) { |  | ||||||
|                     var url = `/api/part/attachment/${pk}/`; |  | ||||||
|      |  | ||||||
|                     constructForm(url, { |  | ||||||
|             fields: { |             fields: { | ||||||
|                             filename: {}, |                 part: { | ||||||
|                             comment: {}, |                     value: {{ part.pk }}, | ||||||
|                         }, |                     hidden: true | ||||||
|                         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, |  | ||||||
|                     }); |  | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         ); |         }); | ||||||
|      |      | ||||||
|         enableDragAndDrop( |         enableDragAndDrop( | ||||||
|             '#attachment-dropzone', |             '#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) |     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! |     # TODO: Record the uploading user when creating or updating an attachment! | ||||||
|  |  | ||||||
|     class Meta: |     class Meta: | ||||||
| @@ -432,6 +430,7 @@ class StockItemAttachmentSerializer(InvenTree.serializers.InvenTreeAttachmentSer | |||||||
|             'stock_item', |             'stock_item', | ||||||
|             'attachment', |             'attachment', | ||||||
|             'filename', |             'filename', | ||||||
|  |             'link', | ||||||
|             'comment', |             'comment', | ||||||
|             'upload_date', |             'upload_date', | ||||||
|             'user', |             'user', | ||||||
|   | |||||||
| @@ -221,55 +221,16 @@ | |||||||
|             } |             } | ||||||
|         ); |         ); | ||||||
|  |  | ||||||
|     loadAttachmentTable( |     loadAttachmentTable('{% url "api-stock-attachment-list" %}', { | ||||||
|         '{% url "api-stock-attachment-list" %}', |  | ||||||
|         { |  | ||||||
|         filters: { |         filters: { | ||||||
|             stock_item: {{ item.pk }}, |             stock_item: {{ item.pk }}, | ||||||
|         }, |         }, | ||||||
|             onEdit: function(pk) { |  | ||||||
|                 var url = `/api/stock/attachment/${pk}/`; |  | ||||||
|  |  | ||||||
|                 constructForm(url, { |  | ||||||
|         fields: { |         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, |  | ||||||
|                 }); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     ); |  | ||||||
|  |  | ||||||
|     $("#new-attachment").click(function() { |  | ||||||
|  |  | ||||||
|         constructForm( |  | ||||||
|             '{% url "api-stock-attachment-list" %}', |  | ||||||
|             { |  | ||||||
|                 method: 'POST', |  | ||||||
|                 fields: { |  | ||||||
|                     attachment: {}, |  | ||||||
|                     comment: {}, |  | ||||||
|             stock_item: { |             stock_item: { | ||||||
|                 value: {{ item.pk }}, |                 value: {{ item.pk }}, | ||||||
|                 hidden: true, |                 hidden: true, | ||||||
|                     }, |  | ||||||
|                 }, |  | ||||||
|                 reload: true, |  | ||||||
|                 title: '{% trans "Add Attachment" %}', |  | ||||||
|             } |             } | ||||||
|         ); |         } | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     loadStockTestResultsTable( |     loadStockTestResultsTable( | ||||||
|   | |||||||
| @@ -442,6 +442,7 @@ | |||||||
| $("#stock-serialize").click(function() { | $("#stock-serialize").click(function() { | ||||||
|  |  | ||||||
|     serializeStockItem({{ item.pk }}, { |     serializeStockItem({{ item.pk }}, { | ||||||
|  |         part: {{ item.part.pk }}, | ||||||
|         reload: true, |         reload: true, | ||||||
|         data: { |         data: { | ||||||
|             quantity: {{ item.quantity }}, |             quantity: {{ item.quantity }}, | ||||||
|   | |||||||
| @@ -4,6 +4,7 @@ | |||||||
| {% load inventree_extras %} | {% load inventree_extras %} | ||||||
| {% load socialaccount %} | {% load socialaccount %} | ||||||
| {% load crispy_forms_tags %} | {% load crispy_forms_tags %} | ||||||
|  | {% load user_sessions i18n %} | ||||||
|  |  | ||||||
| {% block label %}account{% endblock %} | {% block label %}account{% endblock %} | ||||||
|  |  | ||||||
| @@ -14,12 +15,12 @@ | |||||||
| {% block actions %} | {% block actions %} | ||||||
| {% inventree_demo_mode as demo %} | {% inventree_demo_mode as demo %} | ||||||
| {% if not 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" %}'> | <div class='btn btn-primary' type='button' id='edit-user' title='{% trans "Edit User Information" %}'> | ||||||
|     <span class='fas fa-user-cog'></span> {% trans "Edit" %} |     <span class='fas fa-user-cog'></span> {% trans "Edit" %} | ||||||
| </div> | </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 %} | {% endif %} | ||||||
| {% endblock %} | {% endblock %} | ||||||
|  |  | ||||||
| @@ -174,58 +175,48 @@ | |||||||
| </div> | </div> | ||||||
|  |  | ||||||
| <div class='panel-heading'> | <div class='panel-heading'> | ||||||
|     <h4>{% trans "Language Settings" %}</h4> |     <div class='d-flex flex-wrap'> | ||||||
| </div> |         <h4>{% trans "Active Sessions" %}</h4> | ||||||
|  |         {% include "spacer.html" %} | ||||||
| <div class="row"> |         <div class='btn-group' role='group'> | ||||||
|     <div class="col"> |             {% if session_list.count > 1 %} | ||||||
|         <form action="{% url 'set_language' %}" method="post"> |             <form method="post" action="{% url 'session_delete_other' %}"> | ||||||
|                 {% csrf_token %} |                 {% csrf_token %} | ||||||
|             <input name="next" type="hidden" value="{% url 'settings' %}"> |                 <button type="submit" class="btn btn-sm btn-default btn-danger" title='{% trans "Log out active sessions (except this one)" %}'> | ||||||
|             <label for='language' class=' requiredField'> |                     {% trans "Log Out Active Sessions" %} | ||||||
|                 {% trans "Select language" %} |                 </button> | ||||||
|             </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> |             </form> | ||||||
|  |             {% endif %} | ||||||
|         </div> |         </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> | ||||||
|  |  | ||||||
|  | <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 %} | {% endblock %} | ||||||
|  |  | ||||||
| {% block js_ready %} | {% block js_ready %} | ||||||
|   | |||||||
| @@ -50,4 +50,57 @@ | |||||||
|     </div> |     </div> | ||||||
| </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 %} | {% endblock %} | ||||||
| @@ -1,5 +1,8 @@ | |||||||
| {% load i18n %} | {% 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'> | <button type='button' class='btn btn-success' id='new-attachment'> | ||||||
|     <span class='fas fa-plus-circle'></span> {% trans "Add Attachment" %} |     <span class='fas fa-plus-circle'></span> {% trans "Add Attachment" %} | ||||||
| </button> | </button> | ||||||
| @@ -54,6 +54,7 @@ function inventreeGet(url, filters={}, options={}) { | |||||||
|         data: filters, |         data: filters, | ||||||
|         dataType: 'json', |         dataType: 'json', | ||||||
|         contentType: 'application/json', |         contentType: 'application/json', | ||||||
|  |         async: (options.async == false) ? false : true, | ||||||
|         success: function(response) { |         success: function(response) { | ||||||
|             if (options.success) { |             if (options.success) { | ||||||
|                 options.success(response); |                 options.success(response); | ||||||
|   | |||||||
| @@ -6,10 +6,57 @@ | |||||||
| */ | */ | ||||||
|  |  | ||||||
| /* exported | /* exported | ||||||
|  |     addAttachmentButtonCallbacks, | ||||||
|     loadAttachmentTable, |     loadAttachmentTable, | ||||||
|     reloadAttachmentTable, |     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() { | function reloadAttachmentTable() { | ||||||
|  |  | ||||||
|     $('#attachment-table').bootstrapTable('refresh'); |     $('#attachment-table').bootstrapTable('refresh'); | ||||||
| @@ -20,6 +67,8 @@ function loadAttachmentTable(url, options) { | |||||||
|  |  | ||||||
|     var table = options.table || '#attachment-table'; |     var table = options.table || '#attachment-table'; | ||||||
|  |  | ||||||
|  |     addAttachmentButtonCallbacks(url, options.fields || {}); | ||||||
|  |  | ||||||
|     $(table).inventreeTable({ |     $(table).inventreeTable({ | ||||||
|         url: url, |         url: url, | ||||||
|         name: options.name || 'attachments', |         name: options.name || 'attachments', | ||||||
| @@ -34,26 +83,41 @@ function loadAttachmentTable(url, options) { | |||||||
|             $(table).find('.button-attachment-edit').click(function() { |             $(table).find('.button-attachment-edit').click(function() { | ||||||
|                 var pk = $(this).attr('pk'); |                 var pk = $(this).attr('pk'); | ||||||
|  |  | ||||||
|                 if (options.onEdit) { |                 constructForm(`${url}${pk}/`, { | ||||||
|                     options.onEdit(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 |             // Add callback for 'delete' button | ||||||
|             $(table).find('.button-attachment-delete').click(function() { |             $(table).find('.button-attachment-delete').click(function() { | ||||||
|                 var pk = $(this).attr('pk'); |                 var pk = $(this).attr('pk'); | ||||||
|  |  | ||||||
|                 if (options.onDelete) { |                 constructForm(`${url}${pk}/`, { | ||||||
|                     options.onDelete(pk); |                     method: 'DELETE', | ||||||
|                 } |                     confirmMessage: '{% trans "Confirm Delete" %}', | ||||||
|  |                     title: '{% trans "Delete Attachment" %}', | ||||||
|  |                     onSuccess: reloadAttachmentTable, | ||||||
|  |                 }); | ||||||
|             }); |             }); | ||||||
|         }, |         }, | ||||||
|         columns: [ |         columns: [ | ||||||
|             { |             { | ||||||
|                 field: 'attachment', |                 field: 'attachment', | ||||||
|                 title: '{% trans "File" %}', |                 title: '{% trans "Attachment" %}', | ||||||
|                 formatter: function(value) { |                 formatter: function(value, row) { | ||||||
|  |  | ||||||
|  |                     if (row.attachment) { | ||||||
|                         var icon = 'fa-file-alt'; |                         var icon = 'fa-file-alt'; | ||||||
|  |  | ||||||
|                         var fn = value.toLowerCase(); |                         var fn = value.toLowerCase(); | ||||||
| @@ -84,6 +148,12 @@ function loadAttachmentTable(url, options) { | |||||||
|                         var html = `<span class='fas ${icon}'></span> ${filename}`; |                         var html = `<span class='fas ${icon}'></span> ${filename}`; | ||||||
|  |  | ||||||
|                         return renderLink(html, value); |                         return renderLink(html, value); | ||||||
|  |                     } else if (row.link) { | ||||||
|  |                         var html = `<span class='fas fa-link'></span> ${row.link}`; | ||||||
|  |                         return renderLink(html, row.link); | ||||||
|  |                     } else { | ||||||
|  |                         return '-'; | ||||||
|  |                     } | ||||||
|                 } |                 } | ||||||
|             }, |             }, | ||||||
|             { |             { | ||||||
|   | |||||||
| @@ -28,6 +28,7 @@ | |||||||
|     disableFormInput, |     disableFormInput, | ||||||
|     enableFormInput, |     enableFormInput, | ||||||
|     hideFormInput, |     hideFormInput, | ||||||
|  |     setFormInputPlaceholder, | ||||||
|     setFormGroupVisibility, |     setFormGroupVisibility, | ||||||
|     showFormInput, |     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 | // Clear a form input | ||||||
| function clearFormInput(name, options) { | function clearFormInput(name, options) { | ||||||
|     updateFieldValue(name, null, {}, options); |     updateFieldValue(name, null, {}, options); | ||||||
|   | |||||||
| @@ -729,6 +729,23 @@ function loadPurchaseOrderTable(table, options) { | |||||||
|                 title: '{% trans "Items" %}', |                 title: '{% trans "Items" %}', | ||||||
|                 sortable: true, |                 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: {}, |         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); |     constructForm(url, options); | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -144,10 +158,26 @@ function stockItemFields(options={}) { | |||||||
|                     // If a "trackable" part is selected, enable serial number field |                     // If a "trackable" part is selected, enable serial number field | ||||||
|                     if (data.trackable) { |                     if (data.trackable) { | ||||||
|                         enableFormInput('serial_numbers', opts); |                         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 { |                     } else { | ||||||
|                         clearFormInput('serial_numbers', opts); |                         clearFormInput('serial_numbers', opts); | ||||||
|                         disableFormInput('serial_numbers', opts); |                         disableFormInput('serial_numbers', opts); | ||||||
|  |  | ||||||
|  |                         setFormInputPlaceholder('serial_numbers', '{% trans "This part cannot be serialized" %}', opts); | ||||||
|                     } |                     } | ||||||
|  |  | ||||||
|                     // Enable / disable fields based on purchaseable status |                     // Enable / disable fields based on purchaseable status | ||||||
|   | |||||||
| @@ -146,7 +146,6 @@ class RuleSet(models.Model): | |||||||
|         # Core django models (not user configurable) |         # Core django models (not user configurable) | ||||||
|         'admin_logentry', |         'admin_logentry', | ||||||
|         'contenttypes_contenttype', |         'contenttypes_contenttype', | ||||||
|         'sessions_session', |  | ||||||
|  |  | ||||||
|         # Models which currently do not require permissions |         # Models which currently do not require permissions | ||||||
|         'common_colortheme', |         'common_colortheme', | ||||||
| @@ -160,6 +159,7 @@ class RuleSet(models.Model): | |||||||
|         'error_report_error', |         'error_report_error', | ||||||
|         'exchange_rate', |         'exchange_rate', | ||||||
|         'exchange_exchangebackend', |         'exchange_exchangebackend', | ||||||
|  |         'user_sessions_session', | ||||||
|  |  | ||||||
|         # Django-q |         # Django-q | ||||||
|         'django_q_ormq', |         '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-sql-utils==0.5.0         # Advanced query annotation / aggregation | ||||||
| django-stdimage==5.1.1          # Advanced ImageField management | django-stdimage==5.1.1          # Advanced ImageField management | ||||||
| django-test-migrations==1.1.0   # Unit testing for database migrations | 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 | django-weasyprint==1.0.1        # django weasyprint integration | ||||||
| djangorestframework==3.12.4     # DRF framework | djangorestframework==3.12.4     # DRF framework | ||||||
| flake8==3.8.3                   # PEP checking | flake8==3.8.3                   # PEP checking | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								tasks.py
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								tasks.py
									
									
									
									
									
								
							| @@ -279,7 +279,6 @@ def content_excludes(): | |||||||
|  |  | ||||||
|     excludes = [ |     excludes = [ | ||||||
|         "contenttypes", |         "contenttypes", | ||||||
|         "sessions.session", |  | ||||||
|         "auth.permission", |         "auth.permission", | ||||||
|         "authtoken.token", |         "authtoken.token", | ||||||
|         "error_report.error", |         "error_report.error", | ||||||
| @@ -291,6 +290,7 @@ def content_excludes(): | |||||||
|         "exchange.rate", |         "exchange.rate", | ||||||
|         "exchange.exchangebackend", |         "exchange.exchangebackend", | ||||||
|         "common.notificationentry", |         "common.notificationentry", | ||||||
|  |         "user_sessions.session", | ||||||
|     ] |     ] | ||||||
|  |  | ||||||
|     output = "" |     output = "" | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user