mirror of
https://github.com/inventree/InvenTree.git
synced 2025-04-30 20:46:47 +00:00
Merge branch 'master' into partial-shipment
# Conflicts: # InvenTree/order/serializers.py
This commit is contained in:
commit
da6c723184
@ -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 = ""
|
||||||
|
Loading…
x
Reference in New Issue
Block a user