Merge remote-tracking branch 'inventree/master'

This commit is contained in:
Oliver Walters 2022-06-17 21:32:02 +10:00
commit 66d072e1c2
103 changed files with 16779 additions and 15296 deletions

View File

@ -6,10 +6,12 @@ from django.http import JsonResponse
from django.utils.translation import gettext_lazy as _
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import filters, generics, permissions
from rest_framework import filters, permissions
from rest_framework.response import Response
from rest_framework.serializers import ValidationError
from InvenTree.mixins import ListCreateAPI
from .status import is_worker_running
from .version import (inventreeApiVersion, inventreeInstanceName,
inventreeVersion)
@ -134,7 +136,7 @@ class BulkDeleteMixin:
)
class ListCreateDestroyAPIView(BulkDeleteMixin, generics.ListCreateAPIView):
class ListCreateDestroyAPIView(BulkDeleteMixin, ListCreateAPI):
"""Custom API endpoint which provides BulkDelete functionality in addition to List and Create"""
...

View File

@ -6,7 +6,7 @@ import InvenTree.status
from InvenTree.status_codes import (BuildStatus, PurchaseOrderStatus,
SalesOrderStatus, StockHistoryCode,
StockStatus)
from users.models import RuleSet
from users.models import RuleSet, check_user_role
def health_status(request):
@ -83,31 +83,13 @@ def user_roles(request):
roles = {
}
if user.is_superuser:
for ruleset in RuleSet.RULESET_MODELS.keys(): # pragma: no cover
roles[ruleset] = {
'view': True,
'add': True,
'change': True,
'delete': True,
}
else:
for group in user.groups.all():
for rule in group.rule_sets.all():
for role in RuleSet.RULESET_MODELS.keys():
# Ensure the role name is in the dict
if rule.name not in roles:
roles[rule.name] = {
'view': user.is_superuser,
'add': user.is_superuser,
'change': user.is_superuser,
'delete': user.is_superuser
}
permissions = {}
# Roles are additive across groups
roles[rule.name]['view'] |= rule.can_view
roles[rule.name]['add'] |= rule.can_add
roles[rule.name]['change'] |= rule.can_change
roles[rule.name]['delete'] |= rule.can_delete
for perm in ['view', 'add', 'change', 'delete']:
permissions[perm] = user.is_superuser or check_user_role(user, role, perm)
roles[role] = permissions
return {'roles': roles}

View File

@ -124,21 +124,31 @@ class EditUserForm(HelperForm):
class SetPasswordForm(HelperForm):
"""Form for setting user password."""
enter_password = forms.CharField(max_length=100,
enter_password = forms.CharField(
max_length=100,
min_length=8,
required=True,
initial='',
widget=forms.PasswordInput(attrs={'autocomplete': 'off'}),
label=_('Enter password'),
help_text=_('Enter new password'))
help_text=_('Enter new password')
)
confirm_password = forms.CharField(max_length=100,
confirm_password = forms.CharField(
max_length=100,
min_length=8,
required=True,
initial='',
widget=forms.PasswordInput(attrs={'autocomplete': 'off'}),
label=_('Confirm password'),
help_text=_('Confirm new password'))
help_text=_('Confirm new password')
)
old_password = forms.CharField(
label=_("Old password"),
strip=False,
widget=forms.PasswordInput(attrs={'autocomplete': 'current-password', 'autofocus': True}),
)
class Meta:
"""Metaclass options."""
@ -146,7 +156,8 @@ class SetPasswordForm(HelperForm):
model = User
fields = [
'enter_password',
'confirm_password'
'confirm_password',
'old_password',
]

View File

@ -0,0 +1,90 @@
"""Mixins for (API) views in the whole project."""
from bleach import clean
from rest_framework import generics, status
from rest_framework.response import Response
class CleanMixin():
"""Model mixin class which cleans inputs."""
# Define a map of fields avaialble for import
SAFE_FIELDS = {}
def create(self, request, *args, **kwargs):
"""Override to clean data before processing it."""
serializer = self.get_serializer(data=self.clean_data(request.data))
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
def update(self, request, *args, **kwargs):
"""Override to clean data before processing it."""
partial = kwargs.pop('partial', False)
instance = self.get_object()
serializer = self.get_serializer(instance, data=self.clean_data(request.data), partial=partial)
serializer.is_valid(raise_exception=True)
self.perform_update(serializer)
if getattr(instance, '_prefetched_objects_cache', None):
# If 'prefetch_related' has been applied to a queryset, we need to
# forcibly invalidate the prefetch cache on the instance.
instance._prefetched_objects_cache = {}
return Response(serializer.data)
def clean_data(self, data: dict) -> dict:
"""Clean / sanitize data.
This uses mozillas bleach under the hood to disable certain html tags by
encoding them - this leads to script tags etc. to not work.
The results can be longer then the input; might make some character combinations
`ugly`. Prevents XSS on the server-level.
Args:
data (dict): Data that should be sanatized.
Returns:
dict: Profided data sanatized; still in the same order.
"""
clean_data = {}
for k, v in data.items():
if isinstance(v, str):
ret = clean(v)
elif isinstance(v, dict):
ret = self.clean_data(v)
else:
ret = v
clean_data[k] = ret
return clean_data
class ListAPI(generics.ListAPIView):
"""View for list API."""
class ListCreateAPI(CleanMixin, generics.ListCreateAPIView):
"""View for list and create API."""
class CreateAPI(CleanMixin, generics.CreateAPIView):
"""View for create API."""
class RetrieveAPI(generics.RetrieveAPIView):
"""View for retreive API."""
pass
class RetrieveUpdateAPI(CleanMixin, generics.RetrieveUpdateAPIView):
"""View for retrieve and update API."""
pass
class RetrieveUpdateDestroyAPI(CleanMixin, generics.RetrieveUpdateDestroyAPIView):
"""View for retrieve, update and destroy API."""
class UpdateAPI(CleanMixin, generics.UpdateAPIView):
"""View for update API."""

View File

@ -309,6 +309,11 @@ if DEBUG_TOOLBAR_ENABLED: # pragma: no cover
INSTALLED_APPS.append('debug_toolbar')
MIDDLEWARE.append('debug_toolbar.middleware.DebugToolbarMiddleware')
DEBUG_TOOLBAR_CONFIG = {
'RESULTS_CACHE_SIZE': 100,
'OBSERVE_REQUEST_CALLBACK': lambda x: False,
}
# Internal IP addresses allowed to see the debug toolbar
INTERNAL_IPS = [
'127.0.0.1',

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -470,8 +470,8 @@ main {
}
.part-thumb {
width: 200px;
height: 200px;
width: 256px;
height: 256px;
margin: 2px;
padding: 3px;
object-fit: contain;

View File

@ -222,6 +222,29 @@
};
var l10 = {
code: 'bn',
week: {
dow: 0, // Sunday is the first day of the week.
doy: 6, // The week that contains Jan 1st is the first week of the year.
},
buttonText: {
prev: 'পেছনে',
next: 'সামনে',
today: 'আজ',
month: 'মাস',
week: 'সপ্তাহ',
day: 'দিন',
list: 'তালিকা',
},
weekText: 'সপ্তাহ',
allDayText: 'সারাদিন',
moreLinkText: function(n) {
return '+অন্যান্য ' + n
},
noEventsText: 'কোনো ইভেন্ট নেই',
};
var l11 = {
code: 'bs',
week: {
dow: 1, // Monday is the first day of the week.
@ -244,7 +267,7 @@
noEventsText: 'Nema događaja za prikazivanje',
};
var l11 = {
var l12 = {
code: 'ca',
week: {
dow: 1, // Monday is the first day of the week.
@ -265,7 +288,7 @@
noEventsText: 'No hi ha esdeveniments per mostrar',
};
var l12 = {
var l13 = {
code: 'cs',
week: {
dow: 1, // Monday is the first day of the week.
@ -288,7 +311,7 @@
noEventsText: 'Žádné akce k zobrazení',
};
var l13 = {
var l14 = {
code: 'cy',
week: {
dow: 1, // Monday is the first day of the week.
@ -310,7 +333,7 @@
noEventsText: 'Dim digwyddiadau',
};
var l14 = {
var l15 = {
code: 'da',
week: {
dow: 1, // Monday is the first day of the week.
@ -331,7 +354,12 @@
noEventsText: 'Ingen arrangementer at vise',
};
var l15 = {
function affix$1(buttonText) {
return (buttonText === 'Tag' || buttonText === 'Monat') ? 'r' :
buttonText === 'Jahr' ? 's' : ''
}
var l16 = {
code: 'de-at',
week: {
dow: 1, // Monday is the first day of the week.
@ -348,14 +376,49 @@
list: 'Terminübersicht',
},
weekText: 'KW',
weekTextLong: 'Woche',
allDayText: 'Ganztägig',
moreLinkText: function(n) {
return '+ weitere ' + n
},
noEventsText: 'Keine Ereignisse anzuzeigen',
buttonHints: {
prev(buttonText) {
return `Vorherige${affix$1(buttonText)} ${buttonText}`
},
next(buttonText) {
return `Nächste${affix$1(buttonText)} ${buttonText}`
},
today(buttonText) {
// → Heute, Diese Woche, Dieser Monat, Dieses Jahr
if (buttonText === 'Tag') {
return 'Heute'
}
return `Diese${affix$1(buttonText)} ${buttonText}`
},
},
viewHint(buttonText) {
// → Tagesansicht, Wochenansicht, Monatsansicht, Jahresansicht
const glue = buttonText === 'Woche' ? 'n' : buttonText === 'Monat' ? 's' : 'es';
return buttonText + glue + 'ansicht'
},
navLinkHint: 'Gehe zu $0',
moreLinkHint(eventCnt) {
return 'Zeige ' + (eventCnt === 1 ?
'ein weiteres Ereignis' :
eventCnt + ' weitere Ereignisse')
},
closeHint: 'Schließen',
timeHint: 'Uhrzeit',
eventHint: 'Ereignis',
};
var l16 = {
function affix(buttonText) {
return (buttonText === 'Tag' || buttonText === 'Monat') ? 'r' :
buttonText === 'Jahr' ? 's' : ''
}
var l17 = {
code: 'de',
week: {
dow: 1, // Monday is the first day of the week.
@ -372,14 +435,44 @@
list: 'Terminübersicht',
},
weekText: 'KW',
weekTextLong: 'Woche',
allDayText: 'Ganztägig',
moreLinkText: function(n) {
return '+ weitere ' + n
},
noEventsText: 'Keine Ereignisse anzuzeigen',
buttonHints: {
prev(buttonText) {
return `Vorherige${affix(buttonText)} ${buttonText}`
},
next(buttonText) {
return `Nächste${affix(buttonText)} ${buttonText}`
},
today(buttonText) {
// → Heute, Diese Woche, Dieser Monat, Dieses Jahr
if (buttonText === 'Tag') {
return 'Heute'
}
return `Diese${affix(buttonText)} ${buttonText}`
},
},
viewHint(buttonText) {
// → Tagesansicht, Wochenansicht, Monatsansicht, Jahresansicht
const glue = buttonText === 'Woche' ? 'n' : buttonText === 'Monat' ? 's' : 'es';
return buttonText + glue + 'ansicht'
},
navLinkHint: 'Gehe zu $0',
moreLinkHint(eventCnt) {
return 'Zeige ' + (eventCnt === 1 ?
'ein weiteres Ereignis' :
eventCnt + ' weitere Ereignisse')
},
closeHint: 'Schließen',
timeHint: 'Uhrzeit',
eventHint: 'Ereignis',
};
var l17 = {
var l18 = {
code: 'el',
week: {
dow: 1, // Monday is the first day of the week.
@ -400,31 +493,61 @@
noEventsText: 'Δεν υπάρχουν γεγονότα προς εμφάνιση',
};
var l18 = {
var l19 = {
code: 'en-au',
week: {
dow: 1, // Monday is the first day of the week.
doy: 4, // The week that contains Jan 4th is the first week of the year.
},
buttonHints: {
prev: 'Previous $0',
next: 'Next $0',
today: 'This $0',
},
viewHint: '$0 view',
navLinkHint: 'Go to $0',
moreLinkHint(eventCnt) {
return `Show ${eventCnt} more event${eventCnt === 1 ? '' : 's'}`
},
};
var l19 = {
var l20 = {
code: 'en-gb',
week: {
dow: 1, // Monday is the first day of the week.
doy: 4, // The week that contains Jan 4th is the first week of the year.
},
buttonHints: {
prev: 'Previous $0',
next: 'Next $0',
today: 'This $0',
},
viewHint: '$0 view',
navLinkHint: 'Go to $0',
moreLinkHint(eventCnt) {
return `Show ${eventCnt} more event${eventCnt === 1 ? '' : 's'}`
},
};
var l20 = {
var l21 = {
code: 'en-nz',
week: {
dow: 1, // Monday is the first day of the week.
doy: 4, // The week that contains Jan 4th is the first week of the year.
},
buttonHints: {
prev: 'Previous $0',
next: 'Next $0',
today: 'This $0',
},
viewHint: '$0 view',
navLinkHint: 'Go to $0',
moreLinkHint(eventCnt) {
return `Show ${eventCnt} more event${eventCnt === 1 ? '' : 's'}`
},
};
var l21 = {
var l22 = {
code: 'eo',
week: {
dow: 1, // Monday is the first day of the week.
@ -445,7 +568,7 @@
noEventsText: 'Neniuj eventoj por montri',
};
var l22 = {
var l23 = {
code: 'es',
week: {
dow: 0, // Sunday is the first day of the week.
@ -466,7 +589,7 @@
noEventsText: 'No hay eventos para mostrar',
};
var l23 = {
var l24 = {
code: 'es',
week: {
dow: 1, // Monday is the first day of the week.
@ -481,13 +604,32 @@
day: 'Día',
list: 'Agenda',
},
buttonHints: {
prev: '$0 antes',
next: '$0 siguiente',
today(buttonText) {
return (buttonText === 'Día') ? 'Hoy' :
((buttonText === 'Semana') ? 'Esta' : 'Este') + ' ' + buttonText.toLocaleLowerCase()
},
},
viewHint(buttonText) {
return 'Vista ' + (buttonText === 'Semana' ? 'de la' : 'del') + ' ' + buttonText.toLocaleLowerCase()
},
weekText: 'Sm',
weekTextLong: 'Semana',
allDayText: 'Todo el día',
moreLinkText: 'más',
moreLinkHint(eventCnt) {
return `Mostrar ${eventCnt} eventos más`
},
noEventsText: 'No hay eventos para mostrar',
navLinkHint: 'Ir al $0',
closeHint: 'Cerrar',
timeHint: 'La hora',
eventHint: 'Evento',
};
var l24 = {
var l25 = {
code: 'et',
week: {
dow: 1, // Monday is the first day of the week.
@ -510,7 +652,7 @@
noEventsText: 'Kuvamiseks puuduvad sündmused',
};
var l25 = {
var l26 = {
code: 'eu',
week: {
dow: 1, // Monday is the first day of the week.
@ -531,7 +673,7 @@
noEventsText: 'Ez dago ekitaldirik erakusteko',
};
var l26 = {
var l27 = {
code: 'fa',
week: {
dow: 6, // Saturday is the first day of the week.
@ -555,7 +697,7 @@
noEventsText: 'هیچ رویدادی به نمایش',
};
var l27 = {
var l28 = {
code: 'fi',
week: {
dow: 1, // Monday is the first day of the week.
@ -576,7 +718,7 @@
noEventsText: 'Ei näytettäviä tapahtumia',
};
var l28 = {
var l29 = {
code: 'fr',
buttonText: {
prev: 'Précédent',
@ -594,7 +736,7 @@
noEventsText: 'Aucun événement à afficher',
};
var l29 = {
var l30 = {
code: 'fr-ch',
week: {
dow: 1, // Monday is the first day of the week.
@ -616,7 +758,7 @@
noEventsText: 'Aucun événement à afficher',
};
var l30 = {
var l31 = {
code: 'fr',
week: {
dow: 1, // Monday is the first day of the week.
@ -638,7 +780,7 @@
noEventsText: 'Aucun événement à afficher',
};
var l31 = {
var l32 = {
code: 'gl',
week: {
dow: 1, // Monday is the first day of the week.
@ -659,7 +801,7 @@
noEventsText: 'Non hai eventos para amosar',
};
var l32 = {
var l33 = {
code: 'he',
direction: 'rtl',
buttonText: {
@ -677,7 +819,7 @@
weekText: 'שבוע',
};
var l33 = {
var l34 = {
code: 'hi',
week: {
dow: 0, // Sunday is the first day of the week.
@ -700,7 +842,7 @@
noEventsText: 'कोई घटनाओं को प्रदर्शित करने के लिए',
};
var l34 = {
var l35 = {
code: 'hr',
week: {
dow: 1, // Monday is the first day of the week.
@ -723,7 +865,7 @@
noEventsText: 'Nema događaja za prikaz',
};
var l35 = {
var l36 = {
code: 'hu',
week: {
dow: 1, // Monday is the first day of the week.
@ -736,7 +878,7 @@
month: 'Hónap',
week: 'Hét',
day: 'Nap',
list: 'Napló',
list: 'Lista',
},
weekText: 'Hét',
allDayText: 'Egész nap',
@ -744,7 +886,7 @@
noEventsText: 'Nincs megjeleníthető esemény',
};
var l36 = {
var l37 = {
code: 'hy-am',
week: {
dow: 1, // Monday is the first day of the week.
@ -767,7 +909,7 @@
noEventsText: 'Բացակայում է իրադարձությունը ցուցադրելու',
};
var l37 = {
var l38 = {
code: 'id',
week: {
dow: 1, // Monday is the first day of the week.
@ -788,7 +930,7 @@
noEventsText: 'Tidak ada acara untuk ditampilkan',
};
var l38 = {
var l39 = {
code: 'is',
week: {
dow: 1, // Monday is the first day of the week.
@ -809,7 +951,7 @@
noEventsText: 'Engir viðburðir til að sýna',
};
var l39 = {
var l40 = {
code: 'it',
week: {
dow: 1, // Monday is the first day of the week.
@ -832,7 +974,7 @@
noEventsText: 'Non ci sono eventi da visualizzare',
};
var l40 = {
var l41 = {
code: 'ja',
buttonText: {
prev: '前',
@ -851,7 +993,7 @@
noEventsText: '表示する予定はありません',
};
var l41 = {
var l42 = {
code: 'ka',
week: {
dow: 1,
@ -874,7 +1016,7 @@
noEventsText: 'ღონისძიებები არ არის',
};
var l42 = {
var l43 = {
code: 'kk',
week: {
dow: 1, // Monday is the first day of the week.
@ -897,7 +1039,29 @@
noEventsText: 'Көрсету үшін оқиғалар жоқ',
};
var l43 = {
var l44 = {
code: 'km',
week: {
dow: 1, // Monday is the first day of the week.
doy: 4, // The week that contains Jan 4th is the first week of the year.
},
buttonText: {
prev: 'មុន',
next: 'បន្ទាប់',
today: 'ថ្ងៃនេះ',
year: 'ឆ្នាំ',
month: 'ខែ',
week: 'សប្តាហ៍',
day: 'ថ្ងៃ',
list: 'បញ្ជី',
},
weekText: 'សប្តាហ៍',
allDayText: 'ពេញមួយថ្ងៃ',
moreLinkText: 'ច្រើនទៀត',
noEventsText: 'គ្មានព្រឹត្តិការណ៍ត្រូវបង្ហាញ',
};
var l45 = {
code: 'ko',
buttonText: {
prev: '이전달',
@ -914,7 +1078,29 @@
noEventsText: '일정이 없습니다',
};
var l44 = {
var l46 = {
code: 'ku',
week: {
dow: 6, // Saturday is the first day of the week.
doy: 12, // The week that contains Jan 1st is the first week of the year.
},
direction: 'rtl',
buttonText: {
prev: 'پێشتر',
next: 'دواتر',
today: 'ئەمڕو',
month: 'مانگ',
week: 'هەفتە',
day: 'ڕۆژ',
list: 'بەرنامە',
},
weekText: 'هەفتە',
allDayText: 'هەموو ڕۆژەکە',
moreLinkText: 'زیاتر',
noEventsText: 'هیچ ڕووداوێك نیە',
};
var l47 = {
code: 'lb',
week: {
dow: 1, // Monday is the first day of the week.
@ -935,7 +1121,7 @@
noEventsText: 'Nee Evenementer ze affichéieren',
};
var l45 = {
var l48 = {
code: 'lt',
week: {
dow: 1, // Monday is the first day of the week.
@ -956,7 +1142,7 @@
noEventsText: 'Nėra įvykių rodyti',
};
var l46 = {
var l49 = {
code: 'lv',
week: {
dow: 1, // Monday is the first day of the week.
@ -979,7 +1165,7 @@
noEventsText: 'Nav notikumu',
};
var l47 = {
var l50 = {
code: 'mk',
buttonText: {
prev: 'претходно',
@ -998,7 +1184,7 @@
noEventsText: 'Нема настани за прикажување',
};
var l48 = {
var l51 = {
code: 'ms',
week: {
dow: 1, // Monday is the first day of the week.
@ -1021,7 +1207,7 @@
noEventsText: 'Tiada peristiwa untuk dipaparkan',
};
var l49 = {
var l52 = {
code: 'nb',
week: {
dow: 1, // Monday is the first day of the week.
@ -1037,12 +1223,23 @@
list: 'Agenda',
},
weekText: 'Uke',
weekTextLong: 'Uke',
allDayText: 'Hele dagen',
moreLinkText: 'til',
noEventsText: 'Ingen hendelser å vise',
buttonHints: {
prev: 'Forrige $0',
next: 'Neste $0',
today: 'Nåværende $0',
},
viewHint: '$0 visning',
navLinkHint: 'Gå til $0',
moreLinkHint(eventCnt) {
return `Vis ${eventCnt} flere hendelse${eventCnt === 1 ? '' : 'r'}`
},
};
var l50 = {
var l53 = {
code: 'ne', // code for nepal
week: {
dow: 7, // Sunday is the first day of the week.
@ -1063,7 +1260,7 @@
noEventsText: 'देखाउनको लागि कुनै घटनाहरू छैनन्',
};
var l51 = {
var l54 = {
code: 'nl',
week: {
dow: 1, // Monday is the first day of the week.
@ -1084,7 +1281,7 @@
noEventsText: 'Geen evenementen om te laten zien',
};
var l52 = {
var l55 = {
code: 'nn',
week: {
dow: 1, // Monday is the first day of the week.
@ -1105,7 +1302,7 @@
noEventsText: 'Ingen hendelser å vise',
};
var l53 = {
var l56 = {
code: 'pl',
week: {
dow: 1, // Monday is the first day of the week.
@ -1126,7 +1323,7 @@
noEventsText: 'Brak wydarzeń do wyświetlenia',
};
var l54 = {
var l57 = {
code: 'pt-br',
buttonText: {
prev: 'Anterior',
@ -1145,7 +1342,7 @@
noEventsText: 'Não há eventos para mostrar',
};
var l55 = {
var l58 = {
code: 'pt',
week: {
dow: 1, // Monday is the first day of the week.
@ -1166,7 +1363,7 @@
noEventsText: 'Não há eventos para mostrar',
};
var l56 = {
var l59 = {
code: 'ro',
week: {
dow: 1, // Monday is the first day of the week.
@ -1189,7 +1386,7 @@
noEventsText: 'Nu există evenimente de afișat',
};
var l57 = {
var l60 = {
code: 'ru',
week: {
dow: 1, // Monday is the first day of the week.
@ -1212,7 +1409,28 @@
noEventsText: 'Нет событий для отображения',
};
var l58 = {
var l61 = {
code: 'si-lk',
week: {
dow: 1, // Monday is the first day of the week.
doy: 4, // The week that contains Jan 4th is the first week of the year.
},
buttonText: {
prev: 'පෙර',
next: 'පසු',
today: 'අද',
month: 'මාසය',
week: 'සතිය',
day: 'දවස',
list: 'ලැයිස්තුව',
},
weekText: 'සති',
allDayText: 'සියලු',
moreLinkText: 'තවත්',
noEventsText: 'මුකුත් නැත',
};
var l62 = {
code: 'sk',
week: {
dow: 1, // Monday is the first day of the week.
@ -1235,7 +1453,7 @@
noEventsText: 'Žiadne akcie na zobrazenie',
};
var l59 = {
var l63 = {
code: 'sl',
week: {
dow: 1, // Monday is the first day of the week.
@ -1256,7 +1474,24 @@
noEventsText: 'Ni dogodkov za prikaz',
};
var l60 = {
var l64 = {
code: 'sm',
buttonText: {
prev: 'Talu ai',
next: 'Mulimuli atu',
today: 'Aso nei',
month: 'Masina',
week: 'Vaiaso',
day: 'Aso',
list: 'Faasologa',
},
weekText: 'Vaiaso',
allDayText: 'Aso atoa',
moreLinkText: 'sili atu',
noEventsText: 'Leai ni mea na tutupu',
};
var l65 = {
code: 'sq',
week: {
dow: 1, // Monday is the first day of the week.
@ -1279,7 +1514,7 @@
noEventsText: 'Nuk ka evente për të shfaqur',
};
var l61 = {
var l66 = {
code: 'sr-cyrl',
week: {
dow: 1, // Monday is the first day of the week.
@ -1302,7 +1537,7 @@
noEventsText: 'Нема догађаја за приказ',
};
var l62 = {
var l67 = {
code: 'sr',
week: {
dow: 1, // Monday is the first day of the week.
@ -1325,7 +1560,7 @@
noEventsText: 'Nеma događaja za prikaz',
};
var l63 = {
var l68 = {
code: 'sv',
week: {
dow: 1, // Monday is the first day of the week.
@ -1340,13 +1575,56 @@
day: 'Dag',
list: 'Program',
},
buttonHints: {
prev(buttonText) {
return `Föregående ${buttonText.toLocaleLowerCase()}`
},
next(buttonText) {
return `Nästa ${buttonText.toLocaleLowerCase()}`
},
today(buttonText) {
return (buttonText === 'Program' ? 'Detta' : 'Denna') + ' ' + buttonText.toLocaleLowerCase()
},
},
viewHint: '$0 vy',
navLinkHint: 'Gå till $0',
moreLinkHint(eventCnt) {
return `Visa ytterligare ${eventCnt} händelse${eventCnt === 1 ? '' : 'r'}`
},
weekText: 'v.',
weekTextLong: 'Vecka',
allDayText: 'Heldag',
moreLinkText: 'till',
noEventsText: 'Inga händelser att visa',
closeHint: 'Stäng',
timeHint: 'Klockan',
eventHint: 'Händelse',
};
var l64 = {
var l69 = {
code: 'ta-in',
week: {
dow: 1, // Monday is the first day of the week.
doy: 4, // The week that contains Jan 4th is the first week of the year.
},
buttonText: {
prev: 'முந்தைய',
next: 'அடுத்தது',
today: 'இன்று',
month: 'மாதம்',
week: 'வாரம்',
day: 'நாள்',
list: 'தினசரி அட்டவணை',
},
weekText: 'வாரம்',
allDayText: 'நாள் முழுவதும்',
moreLinkText: function(n) {
return '+ மேலும் ' + n
},
noEventsText: 'காண்பிக்க நிகழ்வுகள் இல்லை',
};
var l70 = {
code: 'th',
week: {
dow: 1, // Monday is the first day of the week.
@ -1370,7 +1648,7 @@
noEventsText: 'ไม่มีกิจกรรมที่จะแสดง',
};
var l65 = {
var l71 = {
code: 'tr',
week: {
dow: 1, // Monday is the first day of the week.
@ -1391,7 +1669,7 @@
noEventsText: 'Gösterilecek etkinlik yok',
};
var l66 = {
var l72 = {
code: 'ug',
buttonText: {
month: 'ئاي',
@ -1402,7 +1680,7 @@
allDayText: 'پۈتۈن كۈن',
};
var l67 = {
var l73 = {
code: 'uk',
week: {
dow: 1, // Monday is the first day of the week.
@ -1425,7 +1703,7 @@
noEventsText: 'Немає подій для відображення',
};
var l68 = {
var l74 = {
code: 'uz',
buttonText: {
month: 'Oy',
@ -1440,7 +1718,7 @@
noEventsText: "Ko'rsatish uchun voqealar yo'q",
};
var l69 = {
var l75 = {
code: 'vi',
week: {
dow: 1, // Monday is the first day of the week.
@ -1463,7 +1741,7 @@
noEventsText: 'Không có sự kiện để hiển thị',
};
var l70 = {
var l76 = {
code: 'zh-cn',
week: {
// GB/T 7408-1994《数据元和交换格式·信息交换·日期和时间表示法》与ISO 8601:1988等效
@ -1487,7 +1765,7 @@
noEventsText: '没有事件显示',
};
var l71 = {
var l77 = {
code: 'zh-tw',
buttonText: {
prev: '上月',
@ -1507,7 +1785,7 @@
/* eslint max-len: off */
var localesAll = [
l0, l1, l2, l3, l4, l5, l6, l7, l8, l9, l10, l11, l12, l13, l14, l15, l16, l17, l18, l19, l20, l21, l22, l23, l24, l25, l26, l27, l28, l29, l30, l31, l32, l33, l34, l35, l36, l37, l38, l39, l40, l41, l42, l43, l44, l45, l46, l47, l48, l49, l50, l51, l52, l53, l54, l55, l56, l57, l58, l59, l60, l61, l62, l63, l64, l65, l66, l67, l68, l69, l70, l71,
l0, l1, l2, l3, l4, l5, l6, l7, l8, l9, l10, l11, l12, l13, l14, l15, l16, l17, l18, l19, l20, l21, l22, l23, l24, l25, l26, l27, l28, l29, l30, l31, l32, l33, l34, l35, l36, l37, l38, l39, l40, l41, l42, l43, l44, l45, l46, l47, l48, l49, l50, l51, l52, l53, l54, l55, l56, l57, l58, l59, l60, l61, l62, l63, l64, l65, l66, l67, l68, l69, l70, l71, l72, l73, l74, l75, l76, l77,
];
return localesAll;

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,29 @@
FullCalendar.globalLocales.push(function () {
'use strict';
var bn = {
code: 'bn',
week: {
dow: 0, // Sunday is the first day of the week.
doy: 6, // The week that contains Jan 1st is the first week of the year.
},
buttonText: {
prev: 'পেছনে',
next: 'সামনে',
today: 'আজ',
month: 'মাস',
week: 'সপ্তাহ',
day: 'দিন',
list: 'তালিকা',
},
weekText: 'সপ্তাহ',
allDayText: 'সারাদিন',
moreLinkText: function(n) {
return '+অন্যান্য ' + n
},
noEventsText: 'কোনো ইভেন্ট নেই',
};
return bn;
}());

View File

@ -1,6 +1,11 @@
FullCalendar.globalLocales.push(function () {
'use strict';
function affix(buttonText) {
return (buttonText === 'Tag' || buttonText === 'Monat') ? 'r' :
buttonText === 'Jahr' ? 's' : ''
}
var deAt = {
code: 'de-at',
week: {
@ -18,11 +23,41 @@ FullCalendar.globalLocales.push(function () {
list: 'Terminübersicht',
},
weekText: 'KW',
weekTextLong: 'Woche',
allDayText: 'Ganztägig',
moreLinkText: function(n) {
return '+ weitere ' + n
},
noEventsText: 'Keine Ereignisse anzuzeigen',
buttonHints: {
prev(buttonText) {
return `Vorherige${affix(buttonText)} ${buttonText}`
},
next(buttonText) {
return `Nächste${affix(buttonText)} ${buttonText}`
},
today(buttonText) {
// → Heute, Diese Woche, Dieser Monat, Dieses Jahr
if (buttonText === 'Tag') {
return 'Heute'
}
return `Diese${affix(buttonText)} ${buttonText}`
},
},
viewHint(buttonText) {
// → Tagesansicht, Wochenansicht, Monatsansicht, Jahresansicht
const glue = buttonText === 'Woche' ? 'n' : buttonText === 'Monat' ? 's' : 'es';
return buttonText + glue + 'ansicht'
},
navLinkHint: 'Gehe zu $0',
moreLinkHint(eventCnt) {
return 'Zeige ' + (eventCnt === 1 ?
'ein weiteres Ereignis' :
eventCnt + ' weitere Ereignisse')
},
closeHint: 'Schließen',
timeHint: 'Uhrzeit',
eventHint: 'Ereignis',
};
return deAt;

View File

@ -1,6 +1,11 @@
FullCalendar.globalLocales.push(function () {
'use strict';
function affix(buttonText) {
return (buttonText === 'Tag' || buttonText === 'Monat') ? 'r' :
buttonText === 'Jahr' ? 's' : ''
}
var de = {
code: 'de',
week: {
@ -18,11 +23,41 @@ FullCalendar.globalLocales.push(function () {
list: 'Terminübersicht',
},
weekText: 'KW',
weekTextLong: 'Woche',
allDayText: 'Ganztägig',
moreLinkText: function(n) {
return '+ weitere ' + n
},
noEventsText: 'Keine Ereignisse anzuzeigen',
buttonHints: {
prev(buttonText) {
return `Vorherige${affix(buttonText)} ${buttonText}`
},
next(buttonText) {
return `Nächste${affix(buttonText)} ${buttonText}`
},
today(buttonText) {
// → Heute, Diese Woche, Dieser Monat, Dieses Jahr
if (buttonText === 'Tag') {
return 'Heute'
}
return `Diese${affix(buttonText)} ${buttonText}`
},
},
viewHint(buttonText) {
// → Tagesansicht, Wochenansicht, Monatsansicht, Jahresansicht
const glue = buttonText === 'Woche' ? 'n' : buttonText === 'Monat' ? 's' : 'es';
return buttonText + glue + 'ansicht'
},
navLinkHint: 'Gehe zu $0',
moreLinkHint(eventCnt) {
return 'Zeige ' + (eventCnt === 1 ?
'ein weiteres Ereignis' :
eventCnt + ' weitere Ereignisse')
},
closeHint: 'Schließen',
timeHint: 'Uhrzeit',
eventHint: 'Ereignis',
};
return de;

View File

@ -7,6 +7,16 @@ FullCalendar.globalLocales.push(function () {
dow: 1, // Monday is the first day of the week.
doy: 4, // The week that contains Jan 4th is the first week of the year.
},
buttonHints: {
prev: 'Previous $0',
next: 'Next $0',
today: 'This $0',
},
viewHint: '$0 view',
navLinkHint: 'Go to $0',
moreLinkHint(eventCnt) {
return `Show ${eventCnt} more event${eventCnt === 1 ? '' : 's'}`
},
};
return enAu;

View File

@ -7,6 +7,16 @@ FullCalendar.globalLocales.push(function () {
dow: 1, // Monday is the first day of the week.
doy: 4, // The week that contains Jan 4th is the first week of the year.
},
buttonHints: {
prev: 'Previous $0',
next: 'Next $0',
today: 'This $0',
},
viewHint: '$0 view',
navLinkHint: 'Go to $0',
moreLinkHint(eventCnt) {
return `Show ${eventCnt} more event${eventCnt === 1 ? '' : 's'}`
},
};
return enGb;

View File

@ -7,6 +7,16 @@ FullCalendar.globalLocales.push(function () {
dow: 1, // Monday is the first day of the week.
doy: 4, // The week that contains Jan 4th is the first week of the year.
},
buttonHints: {
prev: 'Previous $0',
next: 'Next $0',
today: 'This $0',
},
viewHint: '$0 view',
navLinkHint: 'Go to $0',
moreLinkHint(eventCnt) {
return `Show ${eventCnt} more event${eventCnt === 1 ? '' : 's'}`
},
};
return enNz;

View File

@ -16,10 +16,29 @@ FullCalendar.globalLocales.push(function () {
day: 'Día',
list: 'Agenda',
},
buttonHints: {
prev: '$0 antes',
next: '$0 siguiente',
today(buttonText) {
return (buttonText === 'Día') ? 'Hoy' :
((buttonText === 'Semana') ? 'Esta' : 'Este') + ' ' + buttonText.toLocaleLowerCase()
},
},
viewHint(buttonText) {
return 'Vista ' + (buttonText === 'Semana' ? 'de la' : 'del') + ' ' + buttonText.toLocaleLowerCase()
},
weekText: 'Sm',
weekTextLong: 'Semana',
allDayText: 'Todo el día',
moreLinkText: 'más',
moreLinkHint(eventCnt) {
return `Mostrar ${eventCnt} eventos más`
},
noEventsText: 'No hay eventos para mostrar',
navLinkHint: 'Ir al $0',
closeHint: 'Cerrar',
timeHint: 'La hora',
eventHint: 'Evento',
};
return es;

View File

@ -14,7 +14,7 @@ FullCalendar.globalLocales.push(function () {
month: 'Hónap',
week: 'Hét',
day: 'Nap',
list: 'Napló',
list: 'Lista',
},
weekText: 'Hét',
allDayText: 'Egész nap',

View File

@ -0,0 +1,28 @@
FullCalendar.globalLocales.push(function () {
'use strict';
var km = {
code: 'km',
week: {
dow: 1, // Monday is the first day of the week.
doy: 4, // The week that contains Jan 4th is the first week of the year.
},
buttonText: {
prev: 'មុន',
next: 'បន្ទាប់',
today: 'ថ្ងៃនេះ',
year: 'ឆ្នាំ',
month: 'ខែ',
week: 'សប្តាហ៍',
day: 'ថ្ងៃ',
list: 'បញ្ជី',
},
weekText: 'សប្តាហ៍',
allDayText: 'ពេញមួយថ្ងៃ',
moreLinkText: 'ច្រើនទៀត',
noEventsText: 'គ្មានព្រឹត្តិការណ៍ត្រូវបង្ហាញ',
};
return km;
}());

View File

@ -0,0 +1,28 @@
FullCalendar.globalLocales.push(function () {
'use strict';
var ku = {
code: 'ku',
week: {
dow: 6, // Saturday is the first day of the week.
doy: 12, // The week that contains Jan 1st is the first week of the year.
},
direction: 'rtl',
buttonText: {
prev: 'پێشتر',
next: 'دواتر',
today: 'ئەمڕو',
month: 'مانگ',
week: 'هەفتە',
day: 'ڕۆژ',
list: 'بەرنامە',
},
weekText: 'هەفتە',
allDayText: 'هەموو ڕۆژەکە',
moreLinkText: 'زیاتر',
noEventsText: 'هیچ ڕووداوێك نیە',
};
return ku;
}());

View File

@ -17,9 +17,20 @@ FullCalendar.globalLocales.push(function () {
list: 'Agenda',
},
weekText: 'Uke',
weekTextLong: 'Uke',
allDayText: 'Hele dagen',
moreLinkText: 'til',
noEventsText: 'Ingen hendelser å vise',
buttonHints: {
prev: 'Forrige $0',
next: 'Neste $0',
today: 'Nåværende $0',
},
viewHint: '$0 visning',
navLinkHint: 'Gå til $0',
moreLinkHint(eventCnt) {
return `Vis ${eventCnt} flere hendelse${eventCnt === 1 ? '' : 'r'}`
},
};
return nb;

View File

@ -0,0 +1,27 @@
FullCalendar.globalLocales.push(function () {
'use strict';
var siLk = {
code: 'si-lk',
week: {
dow: 1, // Monday is the first day of the week.
doy: 4, // The week that contains Jan 4th is the first week of the year.
},
buttonText: {
prev: 'පෙර',
next: 'පසු',
today: 'අද',
month: 'මාසය',
week: 'සතිය',
day: 'දවස',
list: 'ලැයිස්තුව',
},
weekText: 'සති',
allDayText: 'සියලු',
moreLinkText: 'තවත්',
noEventsText: 'මුකුත් නැත',
};
return siLk;
}());

View File

@ -0,0 +1,23 @@
FullCalendar.globalLocales.push(function () {
'use strict';
var sm = {
code: 'sm',
buttonText: {
prev: 'Talu ai',
next: 'Mulimuli atu',
today: 'Aso nei',
month: 'Masina',
week: 'Vaiaso',
day: 'Aso',
list: 'Faasologa',
},
weekText: 'Vaiaso',
allDayText: 'Aso atoa',
moreLinkText: 'sili atu',
noEventsText: 'Leai ni mea na tutupu',
};
return sm;
}());

View File

@ -16,10 +16,30 @@ FullCalendar.globalLocales.push(function () {
day: 'Dag',
list: 'Program',
},
buttonHints: {
prev(buttonText) {
return `Föregående ${buttonText.toLocaleLowerCase()}`
},
next(buttonText) {
return `Nästa ${buttonText.toLocaleLowerCase()}`
},
today(buttonText) {
return (buttonText === 'Program' ? 'Detta' : 'Denna') + ' ' + buttonText.toLocaleLowerCase()
},
},
viewHint: '$0 vy',
navLinkHint: 'Gå till $0',
moreLinkHint(eventCnt) {
return `Visa ytterligare ${eventCnt} händelse${eventCnt === 1 ? '' : 'r'}`
},
weekText: 'v.',
weekTextLong: 'Vecka',
allDayText: 'Heldag',
moreLinkText: 'till',
noEventsText: 'Inga händelser att visa',
closeHint: 'Stäng',
timeHint: 'Klockan',
eventHint: 'Händelse',
};
return sv;

View File

@ -0,0 +1,29 @@
FullCalendar.globalLocales.push(function () {
'use strict';
var taIn = {
code: 'ta-in',
week: {
dow: 1, // Monday is the first day of the week.
doy: 4, // The week that contains Jan 4th is the first week of the year.
},
buttonText: {
prev: 'முந்தைய',
next: 'அடுத்தது',
today: 'இன்று',
month: 'மாதம்',
week: 'வாரம்',
day: 'நாள்',
list: 'தினசரி அட்டவணை',
},
weekText: 'வாரம்',
allDayText: 'நாள் முழுவதும்',
moreLinkText: function(n) {
return '+ மேலும் ' + n
},
noEventsText: 'காண்பிக்க நிகழ்வுகள் இல்லை',
};
return taIn;
}());

View File

@ -1,11 +1,12 @@
/* classes attached to <body> */
/* TODO: make fc-event selector work when calender in shadow DOM */
.fc-not-allowed,
.fc-not-allowed .fc-event { /* override events' custom cursors */
cursor: not-allowed;
}
/* TODO: not attached to body. attached to specific els. move */
.fc-unselectable {
-webkit-user-select: none;
-moz-user-select: none;
@ -367,10 +368,6 @@ When it's NOT activated, the fc-button classes won't even be in the DOM.
/* for most browsers, if a height isn't set on the table, can't do liquid-height within cells */
/* serves as a min-height. harmless */
}
.fc .fc-scrollgrid-section-liquid {
height: auto
}
.fc .fc-scrollgrid-section-liquid > td {
height: 100%; /* better than `auto`, for firefox */
}
@ -394,9 +391,8 @@ When it's NOT activated, the fc-button classes won't even be in the DOM.
.fc .fc-scrollgrid-section-sticky > * {
background: #fff;
background: var(--fc-page-bg-color, #fff);
position: -webkit-sticky;
position: sticky;
z-index: 2; /* TODO: var */
z-index: 3; /* TODO: var */
/* TODO: box-shadow when sticking */
}
.fc .fc-scrollgrid-section-header.fc-scrollgrid-section-sticky > * {
@ -411,7 +407,6 @@ When it's NOT activated, the fc-button classes won't even be in the DOM.
margin-bottom: -1px;
}
.fc-sticky { /* no .fc wrap because used as child of body */
position: -webkit-sticky;
position: sticky;
}
.fc .fc-view-harness {
@ -535,14 +530,17 @@ a.fc-event:hover {
bottom: -20px;
}
/* selecting (always TOUCH) */
/* OR, focused by tab-index */
/* (TODO: maybe not the best focus-styling for .fc-daygrid-dot-event) */
/* ---------------------------------------------------------------------------------------------------- */
.fc-event-selected {
.fc-event-selected,
.fc-event:focus {
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2)
/* expand hit area (subclasses should expand) */
}
.fc-event-selected:before {
.fc-event-selected:before, .fc-event:focus:before {
content: "";
position: absolute;
z-index: 3;
@ -551,12 +549,13 @@ a.fc-event:hover {
right: 0;
bottom: 0;
}
.fc-event-selected {
.fc-event-selected,
.fc-event:focus {
/* dimmer effect */
}
.fc-event-selected:after {
.fc-event-selected:after, .fc-event:focus:after {
content: "";
background: rgba(0, 0, 0, 0.25);
background: var(--fc-event-selected-overlay-color, rgba(0, 0, 0, 0.25));
@ -635,38 +634,33 @@ A HORIZONTAL event
.fc-direction-rtl .fc-h-event:not(.fc-event-selected) .fc-event-resizer-end {
cursor: w-resize;
left: -4px;
left: calc(var(--fc-event-resizer-thickness, 8px) / -2);
left: calc(-0.5 * var(--fc-event-resizer-thickness, 8px));
}
.fc-direction-ltr .fc-h-event:not(.fc-event-selected) .fc-event-resizer-end,
.fc-direction-rtl .fc-h-event:not(.fc-event-selected) .fc-event-resizer-start {
cursor: e-resize;
right: -4px;
right: calc(var(--fc-event-resizer-thickness, 8px) / -2);
right: calc(-0.5 * var(--fc-event-resizer-thickness, 8px));
}
/* resizers for TOUCH */
.fc-h-event.fc-event-selected .fc-event-resizer {
top: 50%;
margin-top: -4px;
margin-top: calc(var(--fc-event-resizer-dot-total-width, 8px) / -2);
margin-top: calc(-0.5 * var(--fc-event-resizer-dot-total-width, 8px));
}
.fc-direction-ltr .fc-h-event.fc-event-selected .fc-event-resizer-start,
.fc-direction-rtl .fc-h-event.fc-event-selected .fc-event-resizer-end {
left: -4px;
left: calc(var(--fc-event-resizer-dot-total-width, 8px) / -2);
left: calc(-0.5 * var(--fc-event-resizer-dot-total-width, 8px));
}
.fc-direction-ltr .fc-h-event.fc-event-selected .fc-event-resizer-end,
.fc-direction-rtl .fc-h-event.fc-event-selected .fc-event-resizer-start {
right: -4px;
right: calc(var(--fc-event-resizer-dot-total-width, 8px) / -2);
}
:root {
--fc-daygrid-event-dot-width: 8px;
right: calc(-0.5 * var(--fc-event-resizer-dot-total-width, 8px));
}
.fc .fc-popover {
position: fixed;
top: 0; /* for when not positioned yet */
position: absolute;
z-index: 9999;
box-shadow: 0 2px 6px rgba(0,0,0,.15);
}
.fc .fc-popover-header {
@ -694,6 +688,11 @@ A HORIZONTAL event
background: rgba(208, 208, 208, 0.3);
background: var(--fc-neutral-bg-color, rgba(208, 208, 208, 0.3));
}
:root {
--fc-daygrid-event-dot-width: 8px;
}
/* help things clear margins of inner content */
.fc-daygrid-day-frame,
.fc-daygrid-day-events,
@ -814,8 +813,12 @@ A HORIZONTAL event
}
.fc .fc-daygrid-day-bottom {
font-size: .85em;
margin: 2px 3px 0;
padding: 2px 3px 0
}
.fc .fc-daygrid-day-bottom:before {
content: "";
clear: both;
display: table; }
.fc .fc-daygrid-more-link {
position: relative;
z-index: 4;
@ -843,9 +846,6 @@ A HORIZONTAL event
/* popover */
}
.fc .fc-more-popover {
z-index: 8;
}
.fc .fc-more-popover .fc-popover-body {
min-width: 220px;
padding: 10px;
@ -1139,7 +1139,7 @@ A VERTICAL event
min-height: 100%; /* liquid-hack is below */
position: relative;
}
.fc-liquid-hack .fc-timegrid-col-frame {
.fc-media-screen.fc-liquid-hack .fc-timegrid-col-frame {
height: auto;
position: absolute;
top: 0;
@ -1165,9 +1165,6 @@ A VERTICAL event
left: 0;
right: 0;
}
.fc-media-screen .fc-timegrid-event-harness {
position: absolute; /* top/left/right/bottom will all be set by JS */
}
.fc {
/* bg */
@ -1211,18 +1208,30 @@ A VERTICAL event
.fc-direction-rtl .fc-timegrid-col-events {
margin: 0 2px 0 2.5%;
}
.fc-timegrid-event-harness {
position: absolute /* top/left/right/bottom will all be set by JS */
}
.fc-timegrid-event-harness > .fc-timegrid-event {
position: absolute; /* absolute WITHIN the harness */
top: 0; /* for when not yet positioned */
bottom: 0; /* " */
left: 0;
right: 0;
}
.fc-timegrid-event-harness-inset .fc-timegrid-event,
.fc-timegrid-event.fc-event-mirror {
.fc-timegrid-event.fc-event-mirror,
.fc-timegrid-more-link {
box-shadow: 0px 0px 0px 1px #fff;
box-shadow: 0px 0px 0px 1px var(--fc-page-bg-color, #fff);
}
.fc-timegrid-event { /* events need to be root */
.fc-timegrid-event,
.fc-timegrid-more-link { /* events need to be root */
font-size: .85em;
font-size: var(--fc-small-font-size, .85em);
border-radius: 3px
border-radius: 3px;
}
.fc-timegrid-event { /* events need to be root */
margin-bottom: 1px /* give some space from bottom */
}
.fc-timegrid-event .fc-event-main {
padding: 1px 1px 0;
@ -1233,24 +1242,37 @@ A VERTICAL event
font-size: var(--fc-small-font-size, .85em);
margin-bottom: 1px;
}
.fc-timegrid-event-condensed .fc-event-main-frame {
.fc-timegrid-event-short .fc-event-main-frame {
flex-direction: row;
overflow: hidden;
}
.fc-timegrid-event-condensed .fc-event-time:after {
.fc-timegrid-event-short .fc-event-time:after {
content: '\00a0-\00a0'; /* dash surrounded by non-breaking spaces */
}
.fc-timegrid-event-condensed .fc-event-title {
.fc-timegrid-event-short .fc-event-title {
font-size: .85em;
font-size: var(--fc-small-font-size, .85em)
}
.fc-media-screen .fc-timegrid-event {
position: absolute; /* absolute WITHIN the harness */
.fc-timegrid-more-link { /* does NOT inherit from fc-timegrid-event */
position: absolute;
z-index: 9999; /* hack */
color: inherit;
color: var(--fc-more-link-text-color, inherit);
background: #d0d0d0;
background: var(--fc-more-link-bg-color, #d0d0d0);
cursor: pointer;
margin-bottom: 1px; /* match space below fc-timegrid-event */
}
.fc-timegrid-more-link-inner { /* has fc-sticky */
padding: 3px 2px;
top: 0;
bottom: 1px; /* stay away from bottom slot line */
left: 0;
}
.fc-direction-ltr .fc-timegrid-more-link {
right: 0;
}
.fc-direction-rtl .fc-timegrid-more-link {
left: 0;
}
.fc {
/* line */
@ -1336,12 +1358,28 @@ A VERTICAL event
border-right: 0;
}
.fc .fc-list-sticky .fc-list-day > * { /* the cells */
position: -webkit-sticky;
position: sticky;
top: 0;
background: #fff;
background: var(--fc-page-bg-color, #fff); /* for when headers are styled to be transparent and sticky */
}
.fc {
/* only exists for aria reasons, hide for non-screen-readers */
}
.fc .fc-list-table thead {
position: absolute;
left: -10000px;
}
.fc {
/* the table's border-style:hidden gets confused by hidden thead. force-hide top border of first cell */
}
.fc .fc-list-table tbody > tr:first-child th {
border-top: 0;
}
.fc .fc-list-table th {
padding: 0; /* uses an inner-wrapper instead... */
}
@ -1427,3 +1465,31 @@ A VERTICAL event
color: inherit; /* natural color for navlinks */
}
.fc-theme-bootstrap5 a:not([href]) {
color: inherit;
text-decoration: inherit;
}
.fc-theme-bootstrap5 .fc-list,
.fc-theme-bootstrap5 .fc-scrollgrid,
.fc-theme-bootstrap5 td,
.fc-theme-bootstrap5 th {
border: 1px solid var(--bs-gray-400);
}
.fc-theme-bootstrap5 {
/* HACK: reapply core styles after highe-precedence border statement above */
}
.fc-theme-bootstrap5 .fc-scrollgrid {
border-right-width: 0;
border-bottom-width: 0;
}
.fc-theme-bootstrap5-shaded {
background-color: var(--bs-gray-200);
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -107,14 +107,9 @@ function inventreeDocReady() {
// Callback to launch the 'About' window
$('#launch-about').click(function() {
var modal = $('#modal-about');
modal.modal({
backdrop: 'static',
keyboard: true,
launchModalForm(`/about/`, {
no_post: true,
});
modal.modal('show');
});
// Callback to launch the 'Database Stats' window
@ -126,8 +121,6 @@ function inventreeDocReady() {
// Initialize clipboard-buttons
attachClipboard('.clip-btn');
attachClipboard('.clip-btn', 'modal-about');
attachClipboard('.clip-btn-version', 'modal-about', 'about-copy-text');
// Generate brand-icons
$('.brand-icon').each(function(i, obj) {

View File

@ -1,7 +1,6 @@
"""Unit tests for the main web views."""
import os
import re
from django.urls import reverse
@ -42,17 +41,3 @@ class ViewTests(InvenTreeTestCase):
self.assertIn("<div id='detail-panels'>", content)
# TODO: In future, run the javascript and ensure that the panels get created!
def test_js_load(self):
"""Test that the required javascript files are loaded correctly."""
# Change this number as more javascript files are added to the index page
N_SCRIPT_FILES = 40
content = self.get_index_page()
# Extract all required javascript files from the index page content
script_files = re.findall("<script type='text\\/javascript' src=\"([^\"]*)\"><\\/script>", content)
self.assertEqual(len(script_files), N_SCRIPT_FILES)
# TODO: Request the javascript files from the server, and ensure they are correcty loaded

View File

@ -31,7 +31,7 @@ from stock.urls import stock_urls
from users.api import user_urls
from .api import InfoView, NotFoundView
from .views import (AppearanceSelectView, CurrencyRefreshView,
from .views import (AboutView, AppearanceSelectView, CurrencyRefreshView,
CustomConnectionsView, CustomEmailView,
CustomPasswordResetFromKeyView,
CustomSessionDeleteOtherView, CustomSessionDeleteView,
@ -150,6 +150,7 @@ frontendpatterns = [
re_path(r'^notifications/', include(notifications_urls)),
re_path(r'^search/', SearchView.as_view(), name='search'),
re_path(r'^settings/', include(settings_urls)),
re_path(r'^about/', AboutView.as_view(), name='about'),
re_path(r'^stats/', DatabaseStatsView.as_view(), name='stats'),
# admin sites

View File

@ -8,8 +8,10 @@ import json
import os
from django.conf import settings
from django.contrib.auth import password_validation
from django.contrib.auth.mixins import (LoginRequiredMixin,
PermissionRequiredMixin)
from django.core.exceptions import ValidationError
from django.http import HttpResponse, HttpResponseRedirect, JsonResponse
from django.shortcuts import redirect
from django.template.loader import render_to_string
@ -540,6 +542,8 @@ class SetPasswordView(AjaxUpdateView):
p1 = request.POST.get('enter_password', '')
p2 = request.POST.get('confirm_password', '')
old_password = request.POST.get('old_password', '')
user = self.request.user
if valid:
# Passwords must match
@ -548,20 +552,28 @@ class SetPasswordView(AjaxUpdateView):
error = _('Password fields must match')
form.add_error('enter_password', error)
form.add_error('confirm_password', error)
valid = False
data = {
'form_valid': valid
}
if valid:
# Old password must be correct
if not user.check_password(old_password):
form.add_error('old_password', _('Wrong password provided'))
valid = False
if valid:
user = self.request.user
try:
# Validate password
password_validation.validate_password(p1, user)
# Update the user
user.set_password(p1)
user.save()
except ValidationError as error:
form.add_error('confirm_password', str(error))
valid = False
return self.renderJsonResponse(request, form, data=data)
return self.renderJsonResponse(request, form, data={'form_valid': valid})
class IndexView(TemplateView):
@ -738,6 +750,13 @@ class DatabaseStatsView(AjaxView):
ajax_form_title = _("System Information")
class AboutView(AjaxView):
"""A view for displaying InvenTree version information"""
ajax_template_name = "about.html"
ajax_form_title = _("About InvenTree")
class NotificationsView(TemplateView):
"""View for showing notifications."""

View File

@ -3,7 +3,7 @@
from django.urls import include, re_path
from django.utils.translation import gettext_lazy as _
from rest_framework import filters, generics
from rest_framework import filters
from rest_framework.exceptions import ValidationError
from django_filters.rest_framework import DjangoFilterBackend
@ -13,6 +13,7 @@ from InvenTree.api import AttachmentMixin, APIDownloadMixin, ListCreateDestroyAP
from InvenTree.helpers import str2bool, isNull, DownloadFile
from InvenTree.filters import InvenTreeOrderingFilter
from InvenTree.status_codes import BuildStatus
from InvenTree.mixins import CreateAPI, RetrieveUpdateDestroyAPI, ListCreateAPI
import build.admin
import build.serializers
@ -65,7 +66,7 @@ class BuildFilter(rest_filters.FilterSet):
return queryset
class BuildList(APIDownloadMixin, generics.ListCreateAPIView):
class BuildList(APIDownloadMixin, ListCreateAPI):
"""API endpoint for accessing a list of Build objects.
- GET: Return list of objects (with filters)
@ -200,7 +201,7 @@ class BuildList(APIDownloadMixin, generics.ListCreateAPIView):
return self.serializer_class(*args, **kwargs)
class BuildDetail(generics.RetrieveUpdateDestroyAPIView):
class BuildDetail(RetrieveUpdateDestroyAPI):
"""API endpoint for detail view of a Build object."""
queryset = Build.objects.all()
@ -219,7 +220,7 @@ class BuildDetail(generics.RetrieveUpdateDestroyAPIView):
return super().destroy(request, *args, **kwargs)
class BuildUnallocate(generics.CreateAPIView):
class BuildUnallocate(CreateAPI):
"""API endpoint for unallocating stock items from a build order.
- The BuildOrder object is specified by the URL
@ -263,7 +264,7 @@ class BuildOrderContextMixin:
return ctx
class BuildOutputCreate(BuildOrderContextMixin, generics.CreateAPIView):
class BuildOutputCreate(BuildOrderContextMixin, CreateAPI):
"""API endpoint for creating new build output(s)."""
queryset = Build.objects.none()
@ -271,7 +272,7 @@ class BuildOutputCreate(BuildOrderContextMixin, generics.CreateAPIView):
serializer_class = build.serializers.BuildOutputCreateSerializer
class BuildOutputComplete(BuildOrderContextMixin, generics.CreateAPIView):
class BuildOutputComplete(BuildOrderContextMixin, CreateAPI):
"""API endpoint for completing build outputs."""
queryset = Build.objects.none()
@ -279,7 +280,7 @@ class BuildOutputComplete(BuildOrderContextMixin, generics.CreateAPIView):
serializer_class = build.serializers.BuildOutputCompleteSerializer
class BuildOutputDelete(BuildOrderContextMixin, generics.CreateAPIView):
class BuildOutputDelete(BuildOrderContextMixin, CreateAPI):
"""API endpoint for deleting multiple build outputs."""
def get_serializer_context(self):
@ -295,7 +296,7 @@ class BuildOutputDelete(BuildOrderContextMixin, generics.CreateAPIView):
serializer_class = build.serializers.BuildOutputDeleteSerializer
class BuildFinish(BuildOrderContextMixin, generics.CreateAPIView):
class BuildFinish(BuildOrderContextMixin, CreateAPI):
"""API endpoint for marking a build as finished (completed)."""
queryset = Build.objects.none()
@ -303,7 +304,7 @@ class BuildFinish(BuildOrderContextMixin, generics.CreateAPIView):
serializer_class = build.serializers.BuildCompleteSerializer
class BuildAutoAllocate(BuildOrderContextMixin, generics.CreateAPIView):
class BuildAutoAllocate(BuildOrderContextMixin, CreateAPI):
"""API endpoint for 'automatically' allocating stock against a build order.
- Only looks at 'untracked' parts
@ -317,7 +318,7 @@ class BuildAutoAllocate(BuildOrderContextMixin, generics.CreateAPIView):
serializer_class = build.serializers.BuildAutoAllocationSerializer
class BuildAllocate(BuildOrderContextMixin, generics.CreateAPIView):
class BuildAllocate(BuildOrderContextMixin, CreateAPI):
"""API endpoint to allocate stock items to a build order.
- The BuildOrder object is specified by the URL
@ -333,21 +334,21 @@ class BuildAllocate(BuildOrderContextMixin, generics.CreateAPIView):
serializer_class = build.serializers.BuildAllocationSerializer
class BuildCancel(BuildOrderContextMixin, generics.CreateAPIView):
class BuildCancel(BuildOrderContextMixin, CreateAPI):
"""API endpoint for cancelling a BuildOrder."""
queryset = Build.objects.all()
serializer_class = build.serializers.BuildCancelSerializer
class BuildItemDetail(generics.RetrieveUpdateDestroyAPIView):
class BuildItemDetail(RetrieveUpdateDestroyAPI):
"""API endpoint for detail view of a BuildItem object."""
queryset = BuildItem.objects.all()
serializer_class = build.serializers.BuildItemSerializer
class BuildItemList(generics.ListCreateAPIView):
class BuildItemList(ListCreateAPI):
"""API endpoint for accessing a list of BuildItem objects.
- GET: Return list of objects
@ -442,7 +443,7 @@ class BuildAttachmentList(AttachmentMixin, ListCreateDestroyAPIView):
]
class BuildAttachmentDetail(AttachmentMixin, generics.RetrieveUpdateDestroyAPIView):
class BuildAttachmentDetail(AttachmentMixin, RetrieveUpdateDestroyAPI):
"""Detail endpoint for a BuildOrderAttachment object."""
queryset = BuildOrderAttachment.objects.all()

View File

@ -17,7 +17,7 @@
{% block thumbnail %}
<img class="part-thumb"
{% if build.part.image %}
src="{{ build.part.image.url }}"
src="{{ build.part.image.preview.url }}"
{% else %}
src="{% static 'img/blank_image.png' %}"
{% endif %}/>

View File

@ -9,7 +9,7 @@ from django.views.decorators.csrf import csrf_exempt
from django_filters.rest_framework import DjangoFilterBackend
from django_q.tasks import async_task
from rest_framework import filters, generics, permissions, serializers
from rest_framework import filters, permissions, serializers
from rest_framework.exceptions import NotAcceptable, NotFound
from rest_framework.response import Response
from rest_framework.views import APIView
@ -18,6 +18,8 @@ import common.models
import common.serializers
from InvenTree.api import BulkDeleteMixin
from InvenTree.helpers import inheritors
from InvenTree.mixins import (CreateAPI, ListAPI, RetrieveAPI,
RetrieveUpdateAPI, RetrieveUpdateDestroyAPI)
from plugin.models import NotificationUserSetting
from plugin.serializers import NotificationUserSettingSerializer
@ -97,7 +99,7 @@ class WebhookView(CsrfExemptMixin, APIView):
raise NotFound()
class SettingsList(generics.ListAPIView):
class SettingsList(ListAPI):
"""Generic ListView for settings.
This is inheritted by all list views for settings.
@ -145,7 +147,7 @@ class GlobalSettingsPermissions(permissions.BasePermission):
return False
class GlobalSettingsDetail(generics.RetrieveUpdateAPIView):
class GlobalSettingsDetail(RetrieveUpdateAPI):
"""Detail view for an individual "global setting" object.
- User must have 'staff' status to view / edit
@ -203,7 +205,7 @@ class UserSettingsPermissions(permissions.BasePermission):
return user == obj.user
class UserSettingsDetail(generics.RetrieveUpdateAPIView):
class UserSettingsDetail(RetrieveUpdateAPI):
"""Detail view for an individual "user setting" object.
- User can only view / edit settings their own settings objects
@ -245,7 +247,7 @@ class NotificationUserSettingsList(SettingsList):
return queryset
class NotificationUserSettingsDetail(generics.RetrieveUpdateAPIView):
class NotificationUserSettingsDetail(RetrieveUpdateAPI):
"""Detail view for an individual "notification user setting" object.
- User can only view / edit settings their own settings objects
@ -259,7 +261,7 @@ class NotificationUserSettingsDetail(generics.RetrieveUpdateAPIView):
]
class NotificationList(BulkDeleteMixin, generics.ListAPIView):
class NotificationList(BulkDeleteMixin, ListAPI):
"""List view for all notifications of the current user."""
queryset = common.models.NotificationMessage.objects.all()
@ -310,7 +312,7 @@ class NotificationList(BulkDeleteMixin, generics.ListAPIView):
return queryset
class NotificationDetail(generics.RetrieveUpdateDestroyAPIView):
class NotificationDetail(RetrieveUpdateDestroyAPI):
"""Detail view for an individual notification object.
- User can only view / delete their own notification objects
@ -323,7 +325,7 @@ class NotificationDetail(generics.RetrieveUpdateDestroyAPIView):
]
class NotificationReadEdit(generics.CreateAPIView):
class NotificationReadEdit(CreateAPI):
"""General API endpoint to manipulate read state of a notification."""
queryset = common.models.NotificationMessage.objects.all()
@ -360,7 +362,7 @@ class NotificationUnread(NotificationReadEdit):
target = False
class NotificationReadAll(generics.RetrieveAPIView):
class NotificationReadAll(RetrieveAPI):
"""API endpoint to mark all notifications as read."""
queryset = common.models.NotificationMessage.objects.all()

View File

@ -1008,6 +1008,23 @@ class InvenTreeSetting(BaseInvenTreeSetting):
'validator': InvenTree.validators.validate_part_name_format
},
'LABEL_ENABLE': {
'name': _('Enable label printing'),
'description': _('Enable label printing from the web interface'),
'default': True,
'validator': bool,
},
'LABEL_DPI': {
'name': _('Label Image DPI'),
'description': _('DPI resolution when generating image files to supply to label printing plugins'),
'default': 300,
'validator': [
int,
MinValueValidator(100),
]
},
'REPORT_ENABLE': {
'name': _('Enable Reports'),
'description': _('Enable generation of reports'),
@ -1389,12 +1406,6 @@ class InvenTreeUserSetting(BaseInvenTreeSetting):
'default': True,
'validator': bool,
},
'LABEL_ENABLE': {
'name': _('Enable label printing'),
'description': _('Enable label printing from the web interface'),
'default': True,
'validator': bool,
},
"LABEL_INLINE": {
'name': _('Inline label display'),

View File

@ -5,10 +5,11 @@ from django.urls import include, re_path
from django_filters import rest_framework as rest_filters
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import filters, generics
from rest_framework import filters
from InvenTree.api import AttachmentMixin, ListCreateDestroyAPIView
from InvenTree.helpers import str2bool
from InvenTree.mixins import ListCreateAPI, RetrieveUpdateDestroyAPI
from .models import (Company, ManufacturerPart, ManufacturerPartAttachment,
ManufacturerPartParameter, SupplierPart,
@ -20,7 +21,7 @@ from .serializers import (CompanySerializer,
SupplierPriceBreakSerializer)
class CompanyList(generics.ListCreateAPIView):
class CompanyList(ListCreateAPI):
"""API endpoint for accessing a list of Company objects.
Provides two methods:
@ -67,7 +68,7 @@ class CompanyList(generics.ListCreateAPIView):
ordering = 'name'
class CompanyDetail(generics.RetrieveUpdateDestroyAPIView):
class CompanyDetail(RetrieveUpdateDestroyAPI):
"""API endpoint for detail of a single Company object."""
queryset = Company.objects.all()
@ -146,7 +147,7 @@ class ManufacturerPartList(ListCreateDestroyAPIView):
]
class ManufacturerPartDetail(generics.RetrieveUpdateDestroyAPIView):
class ManufacturerPartDetail(RetrieveUpdateDestroyAPI):
"""API endpoint for detail view of ManufacturerPart object.
- GET: Retrieve detail view
@ -173,7 +174,7 @@ class ManufacturerPartAttachmentList(AttachmentMixin, ListCreateDestroyAPIView):
]
class ManufacturerPartAttachmentDetail(AttachmentMixin, generics.RetrieveUpdateDestroyAPIView):
class ManufacturerPartAttachmentDetail(AttachmentMixin, RetrieveUpdateDestroyAPI):
"""Detail endpooint for ManufacturerPartAttachment model."""
queryset = ManufacturerPartAttachment.objects.all()
@ -246,7 +247,7 @@ class ManufacturerPartParameterList(ListCreateDestroyAPIView):
]
class ManufacturerPartParameterDetail(generics.RetrieveUpdateDestroyAPIView):
class ManufacturerPartParameterDetail(RetrieveUpdateDestroyAPI):
"""API endpoint for detail view of ManufacturerPartParameter model."""
queryset = ManufacturerPartParameter.objects.all()
@ -347,7 +348,7 @@ class SupplierPartList(ListCreateDestroyAPIView):
]
class SupplierPartDetail(generics.RetrieveUpdateDestroyAPIView):
class SupplierPartDetail(RetrieveUpdateDestroyAPI):
"""API endpoint for detail view of SupplierPart object.
- GET: Retrieve detail view
@ -362,7 +363,7 @@ class SupplierPartDetail(generics.RetrieveUpdateDestroyAPIView):
]
class SupplierPriceBreakList(generics.ListCreateAPIView):
class SupplierPriceBreakList(ListCreateAPI):
"""API endpoint for list view of SupplierPriceBreak object.
- GET: Retrieve list of SupplierPriceBreak objects
@ -381,7 +382,7 @@ class SupplierPriceBreakList(generics.ListCreateAPIView):
]
class SupplierPriceBreakDetail(generics.RetrieveUpdateDestroyAPIView):
class SupplierPriceBreakDetail(RetrieveUpdateDestroyAPI):
"""Detail endpoint for SupplierPriceBreak object."""
queryset = SupplierPriceBreak.objects.all()

View File

@ -127,7 +127,10 @@ class Company(models.Model):
upload_to=rename_company_image,
null=True,
blank=True,
variations={'thumbnail': (128, 128)},
variations={
'thumbnail': (128, 128),
'preview': (256, 256),
},
delete_orphans=True,
verbose_name=_('Image'),
)

View File

@ -47,7 +47,7 @@
<div class='dropzone part-thumb-container' id='company-thumb'>
<img class="part-thumb" id='company-image'
{% if company.image %}
src="{{ company.image.url }}"
src="{{ company.image.preview.url }}"
{% else %}
src="{% static 'img/blank_image.png' %}"
{% endif %}/>

View File

@ -50,7 +50,7 @@
{% block thumbnail %}
<img class='part-thumb'
{% if part.part.image %}
src='{{ part.part.image.url }}'
src='{{ part.part.image.preview.url }}'
{% else %}
src="{% static 'img/blank_image.png' %}"
{% endif %}/>

View File

@ -62,7 +62,7 @@
{% block thumbnail %}
<img class='part-thumb'
{% if part.part.image %}
src='{{ part.part.image.url }}'
src='{{ part.part.image.preview.url }}'
{% else %}
src="{% static 'img/blank_image.png' %}"
{% endif %}/>

View File

@ -6,11 +6,12 @@ from django.http import HttpResponse, JsonResponse
from django.urls import include, re_path
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import filters, generics
from rest_framework import filters
from rest_framework.exceptions import NotFound
import common.models
import InvenTree.helpers
from InvenTree.mixins import ListAPI, RetrieveAPI, RetrieveUpdateDestroyAPI
from InvenTree.tasks import offload_task
from part.models import Part
from plugin.base.label import label as plugin_label
@ -22,7 +23,7 @@ from .serializers import (PartLabelSerializer, StockItemLabelSerializer,
StockLocationLabelSerializer)
class LabelListView(generics.ListAPIView):
class LabelListView(ListAPI):
"""Generic API class for label templates."""
filter_backends = [
@ -275,14 +276,14 @@ class StockItemLabelList(LabelListView, StockItemLabelMixin):
return queryset
class StockItemLabelDetail(generics.RetrieveUpdateDestroyAPIView):
class StockItemLabelDetail(RetrieveUpdateDestroyAPI):
"""API endpoint for a single StockItemLabel object."""
queryset = StockItemLabel.objects.all()
serializer_class = StockItemLabelSerializer
class StockItemLabelPrint(generics.RetrieveAPIView, StockItemLabelMixin, LabelPrintMixin):
class StockItemLabelPrint(RetrieveAPI, StockItemLabelMixin, LabelPrintMixin):
"""API endpoint for printing a StockItemLabel object."""
queryset = StockItemLabel.objects.all()
@ -391,14 +392,14 @@ class StockLocationLabelList(LabelListView, StockLocationLabelMixin):
return queryset
class StockLocationLabelDetail(generics.RetrieveUpdateDestroyAPIView):
class StockLocationLabelDetail(RetrieveUpdateDestroyAPI):
"""API endpoint for a single StockLocationLabel object."""
queryset = StockLocationLabel.objects.all()
serializer_class = StockLocationLabelSerializer
class StockLocationLabelPrint(generics.RetrieveAPIView, StockLocationLabelMixin, LabelPrintMixin):
class StockLocationLabelPrint(RetrieveAPI, StockLocationLabelMixin, LabelPrintMixin):
"""API endpoint for printing a StockLocationLabel object."""
queryset = StockLocationLabel.objects.all()
@ -483,14 +484,14 @@ class PartLabelList(LabelListView, PartLabelMixin):
return queryset
class PartLabelDetail(generics.RetrieveUpdateDestroyAPIView):
class PartLabelDetail(RetrieveUpdateDestroyAPI):
"""API endpoint for a single PartLabel object."""
queryset = PartLabel.objects.all()
serializer_class = PartLabelSerializer
class PartLabelPrint(generics.RetrieveAPIView, PartLabelMixin, LabelPrintMixin):
class PartLabelPrint(RetrieveAPI, PartLabelMixin, LabelPrintMixin):
"""API endpoint for printing a PartLabel object."""
queryset = PartLabel.objects.all()

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -4,7 +4,7 @@ from django.db.models import F, Q
from django.urls import include, path, re_path
from django_filters import rest_framework as rest_filters
from rest_framework import filters, generics, status
from rest_framework import filters, status
from rest_framework.response import Response
import order.models as models
@ -14,6 +14,8 @@ from InvenTree.api import (APIDownloadMixin, AttachmentMixin,
ListCreateDestroyAPIView)
from InvenTree.filters import InvenTreeOrderingFilter
from InvenTree.helpers import DownloadFile, str2bool
from InvenTree.mixins import (CreateAPI, ListAPI, ListCreateAPI,
RetrieveUpdateAPI, RetrieveUpdateDestroyAPI)
from InvenTree.status_codes import PurchaseOrderStatus, SalesOrderStatus
from order.admin import (PurchaseOrderLineItemResource, PurchaseOrderResource,
SalesOrderResource)
@ -101,7 +103,7 @@ class PurchaseOrderFilter(rest_filters.FilterSet):
]
class PurchaseOrderList(APIDownloadMixin, generics.ListCreateAPIView):
class PurchaseOrderList(APIDownloadMixin, ListCreateAPI):
"""API endpoint for accessing a list of PurchaseOrder objects.
- GET: Return list of PurchaseOrder objects (with filters)
@ -114,7 +116,7 @@ class PurchaseOrderList(APIDownloadMixin, generics.ListCreateAPIView):
def create(self, request, *args, **kwargs):
"""Save user information on create."""
serializer = self.get_serializer(data=request.data)
serializer = self.get_serializer(data=self.clean_data(request.data))
serializer.is_valid(raise_exception=True)
item = serializer.save()
@ -254,7 +256,7 @@ class PurchaseOrderList(APIDownloadMixin, generics.ListCreateAPIView):
ordering = '-creation_date'
class PurchaseOrderDetail(generics.RetrieveUpdateDestroyAPIView):
class PurchaseOrderDetail(RetrieveUpdateDestroyAPI):
"""API endpoint for detail view of a PurchaseOrder object."""
queryset = models.PurchaseOrder.objects.all()
@ -304,7 +306,7 @@ class PurchaseOrderContextMixin:
return context
class PurchaseOrderCancel(PurchaseOrderContextMixin, generics.CreateAPIView):
class PurchaseOrderCancel(PurchaseOrderContextMixin, CreateAPI):
"""API endpoint to 'cancel' a purchase order.
The purchase order must be in a state which can be cancelled
@ -315,7 +317,7 @@ class PurchaseOrderCancel(PurchaseOrderContextMixin, generics.CreateAPIView):
serializer_class = serializers.PurchaseOrderCancelSerializer
class PurchaseOrderComplete(PurchaseOrderContextMixin, generics.CreateAPIView):
class PurchaseOrderComplete(PurchaseOrderContextMixin, CreateAPI):
"""API endpoint to 'complete' a purchase order."""
queryset = models.PurchaseOrder.objects.all()
@ -323,7 +325,7 @@ class PurchaseOrderComplete(PurchaseOrderContextMixin, generics.CreateAPIView):
serializer_class = serializers.PurchaseOrderCompleteSerializer
class PurchaseOrderIssue(PurchaseOrderContextMixin, generics.CreateAPIView):
class PurchaseOrderIssue(PurchaseOrderContextMixin, CreateAPI):
"""API endpoint to 'complete' a purchase order."""
queryset = models.PurchaseOrder.objects.all()
@ -331,7 +333,7 @@ class PurchaseOrderIssue(PurchaseOrderContextMixin, generics.CreateAPIView):
serializer_class = serializers.PurchaseOrderIssueSerializer
class PurchaseOrderMetadata(generics.RetrieveUpdateAPIView):
class PurchaseOrderMetadata(RetrieveUpdateAPI):
"""API endpoint for viewing / updating PurchaseOrder metadata."""
def get_serializer(self, *args, **kwargs):
@ -341,7 +343,7 @@ class PurchaseOrderMetadata(generics.RetrieveUpdateAPIView):
queryset = models.PurchaseOrder.objects.all()
class PurchaseOrderReceive(PurchaseOrderContextMixin, generics.CreateAPIView):
class PurchaseOrderReceive(PurchaseOrderContextMixin, CreateAPI):
"""API endpoint to receive stock items against a purchase order.
- The purchase order is specified in the URL.
@ -405,7 +407,7 @@ class PurchaseOrderLineItemFilter(rest_filters.FilterSet):
return queryset
class PurchaseOrderLineItemList(APIDownloadMixin, generics.ListCreateAPIView):
class PurchaseOrderLineItemList(APIDownloadMixin, ListCreateAPI):
"""API endpoint for accessing a list of PurchaseOrderLineItem objects.
- GET: Return a list of PurchaseOrder Line Item objects
@ -499,7 +501,7 @@ class PurchaseOrderLineItemList(APIDownloadMixin, generics.ListCreateAPIView):
]
class PurchaseOrderLineItemDetail(generics.RetrieveUpdateDestroyAPIView):
class PurchaseOrderLineItemDetail(RetrieveUpdateDestroyAPI):
"""Detail API endpoint for PurchaseOrderLineItem object."""
queryset = models.PurchaseOrderLineItem.objects.all()
@ -514,14 +516,14 @@ class PurchaseOrderLineItemDetail(generics.RetrieveUpdateDestroyAPIView):
return queryset
class PurchaseOrderExtraLineList(GeneralExtraLineList, generics.ListCreateAPIView):
class PurchaseOrderExtraLineList(GeneralExtraLineList, ListCreateAPI):
"""API endpoint for accessing a list of PurchaseOrderExtraLine objects."""
queryset = models.PurchaseOrderExtraLine.objects.all()
serializer_class = serializers.PurchaseOrderExtraLineSerializer
class PurchaseOrderExtraLineDetail(generics.RetrieveUpdateDestroyAPIView):
class PurchaseOrderExtraLineDetail(RetrieveUpdateDestroyAPI):
"""API endpoint for detail view of a PurchaseOrderExtraLine object."""
queryset = models.PurchaseOrderExtraLine.objects.all()
@ -543,14 +545,14 @@ class SalesOrderAttachmentList(AttachmentMixin, ListCreateDestroyAPIView):
]
class SalesOrderAttachmentDetail(AttachmentMixin, generics.RetrieveUpdateDestroyAPIView):
class SalesOrderAttachmentDetail(AttachmentMixin, RetrieveUpdateDestroyAPI):
"""Detail endpoint for SalesOrderAttachment."""
queryset = models.SalesOrderAttachment.objects.all()
serializer_class = serializers.SalesOrderAttachmentSerializer
class SalesOrderList(APIDownloadMixin, generics.ListCreateAPIView):
class SalesOrderList(APIDownloadMixin, ListCreateAPI):
"""API endpoint for accessing a list of SalesOrder objects.
- GET: Return list of SalesOrder objects (with filters)
@ -562,7 +564,7 @@ class SalesOrderList(APIDownloadMixin, generics.ListCreateAPIView):
def create(self, request, *args, **kwargs):
"""Save user information on create."""
serializer = self.get_serializer(data=request.data)
serializer = self.get_serializer(data=self.clean_data(request.data))
serializer.is_valid(raise_exception=True)
item = serializer.save()
@ -695,7 +697,7 @@ class SalesOrderList(APIDownloadMixin, generics.ListCreateAPIView):
ordering = '-creation_date'
class SalesOrderDetail(generics.RetrieveUpdateDestroyAPIView):
class SalesOrderDetail(RetrieveUpdateDestroyAPI):
"""API endpoint for detail view of a SalesOrder object."""
queryset = models.SalesOrder.objects.all()
@ -754,7 +756,7 @@ class SalesOrderLineItemFilter(rest_filters.FilterSet):
return queryset
class SalesOrderLineItemList(generics.ListCreateAPIView):
class SalesOrderLineItemList(ListCreateAPI):
"""API endpoint for accessing a list of SalesOrderLineItem objects."""
queryset = models.SalesOrderLineItem.objects.all()
@ -818,21 +820,21 @@ class SalesOrderLineItemList(generics.ListCreateAPIView):
]
class SalesOrderExtraLineList(GeneralExtraLineList, generics.ListCreateAPIView):
class SalesOrderExtraLineList(GeneralExtraLineList, ListCreateAPI):
"""API endpoint for accessing a list of SalesOrderExtraLine objects."""
queryset = models.SalesOrderExtraLine.objects.all()
serializer_class = serializers.SalesOrderExtraLineSerializer
class SalesOrderExtraLineDetail(generics.RetrieveUpdateDestroyAPIView):
class SalesOrderExtraLineDetail(RetrieveUpdateDestroyAPI):
"""API endpoint for detail view of a SalesOrderExtraLine object."""
queryset = models.SalesOrderExtraLine.objects.all()
serializer_class = serializers.SalesOrderExtraLineSerializer
class SalesOrderLineItemDetail(generics.RetrieveUpdateDestroyAPIView):
class SalesOrderLineItemDetail(RetrieveUpdateDestroyAPI):
"""API endpoint for detail view of a SalesOrderLineItem object."""
queryset = models.SalesOrderLineItem.objects.all()
@ -864,21 +866,21 @@ class SalesOrderContextMixin:
return ctx
class SalesOrderCancel(SalesOrderContextMixin, generics.CreateAPIView):
class SalesOrderCancel(SalesOrderContextMixin, CreateAPI):
"""API endpoint to cancel a SalesOrder"""
queryset = models.SalesOrder.objects.all()
serializer_class = serializers.SalesOrderCancelSerializer
class SalesOrderComplete(SalesOrderContextMixin, generics.CreateAPIView):
class SalesOrderComplete(SalesOrderContextMixin, CreateAPI):
"""API endpoint for manually marking a SalesOrder as "complete"."""
queryset = models.SalesOrder.objects.all()
serializer_class = serializers.SalesOrderCompleteSerializer
class SalesOrderMetadata(generics.RetrieveUpdateAPIView):
class SalesOrderMetadata(RetrieveUpdateAPI):
"""API endpoint for viewing / updating SalesOrder metadata."""
def get_serializer(self, *args, **kwargs):
@ -888,14 +890,14 @@ class SalesOrderMetadata(generics.RetrieveUpdateAPIView):
queryset = models.SalesOrder.objects.all()
class SalesOrderAllocateSerials(SalesOrderContextMixin, generics.CreateAPIView):
class SalesOrderAllocateSerials(SalesOrderContextMixin, CreateAPI):
"""API endpoint to allocation stock items against a SalesOrder, by specifying serial numbers."""
queryset = models.SalesOrder.objects.none()
serializer_class = serializers.SalesOrderSerialAllocationSerializer
class SalesOrderAllocate(SalesOrderContextMixin, generics.CreateAPIView):
class SalesOrderAllocate(SalesOrderContextMixin, CreateAPI):
"""API endpoint to allocate stock items against a SalesOrder.
- The SalesOrder is specified in the URL
@ -906,14 +908,14 @@ class SalesOrderAllocate(SalesOrderContextMixin, generics.CreateAPIView):
serializer_class = serializers.SalesOrderShipmentAllocationSerializer
class SalesOrderAllocationDetail(generics.RetrieveUpdateDestroyAPIView):
class SalesOrderAllocationDetail(RetrieveUpdateDestroyAPI):
"""API endpoint for detali view of a SalesOrderAllocation object."""
queryset = models.SalesOrderAllocation.objects.all()
serializer_class = serializers.SalesOrderAllocationSerializer
class SalesOrderAllocationList(generics.ListAPIView):
class SalesOrderAllocationList(ListAPI):
"""API endpoint for listing SalesOrderAllocation objects."""
queryset = models.SalesOrderAllocation.objects.all()
@ -1017,7 +1019,7 @@ class SalesOrderShipmentFilter(rest_filters.FilterSet):
]
class SalesOrderShipmentList(generics.ListCreateAPIView):
class SalesOrderShipmentList(ListCreateAPI):
"""API list endpoint for SalesOrderShipment model."""
queryset = models.SalesOrderShipment.objects.all()
@ -1029,14 +1031,14 @@ class SalesOrderShipmentList(generics.ListCreateAPIView):
]
class SalesOrderShipmentDetail(generics.RetrieveUpdateDestroyAPIView):
class SalesOrderShipmentDetail(RetrieveUpdateDestroyAPI):
"""API detail endpooint for SalesOrderShipment model."""
queryset = models.SalesOrderShipment.objects.all()
serializer_class = serializers.SalesOrderShipmentSerializer
class SalesOrderShipmentComplete(generics.CreateAPIView):
class SalesOrderShipmentComplete(CreateAPI):
"""API endpoint for completing (shipping) a SalesOrderShipment."""
queryset = models.SalesOrderShipment.objects.all()
@ -1072,7 +1074,7 @@ class PurchaseOrderAttachmentList(AttachmentMixin, ListCreateDestroyAPIView):
]
class PurchaseOrderAttachmentDetail(AttachmentMixin, generics.RetrieveUpdateDestroyAPIView):
class PurchaseOrderAttachmentDetail(AttachmentMixin, RetrieveUpdateDestroyAPI):
"""Detail endpoint for a PurchaseOrderAttachment."""
queryset = models.PurchaseOrderAttachment.objects.all()

View File

@ -14,7 +14,7 @@ from django_filters.rest_framework import DjangoFilterBackend
from djmoney.contrib.exchange.exceptions import MissingRate
from djmoney.contrib.exchange.models import convert_money
from djmoney.money import Money
from rest_framework import filters, generics, serializers, status
from rest_framework import filters, serializers, status
from rest_framework.exceptions import ValidationError
from rest_framework.response import Response
@ -25,6 +25,9 @@ from company.models import Company, ManufacturerPart, SupplierPart
from InvenTree.api import (APIDownloadMixin, AttachmentMixin,
ListCreateDestroyAPIView)
from InvenTree.helpers import DownloadFile, increment, isNull, str2bool
from InvenTree.mixins import (CreateAPI, ListAPI, ListCreateAPI, RetrieveAPI,
RetrieveUpdateAPI, RetrieveUpdateDestroyAPI,
UpdateAPI)
from InvenTree.status_codes import (BuildStatus, PurchaseOrderStatus,
SalesOrderStatus)
from part.admin import PartResource
@ -39,7 +42,7 @@ from .models import (BomItem, BomItemSubstitute, Part, PartAttachment,
PartTestTemplate)
class CategoryList(generics.ListCreateAPIView):
class CategoryList(ListCreateAPI):
"""API endpoint for accessing a list of PartCategory objects.
- GET: Return a list of PartCategory objects
@ -155,7 +158,7 @@ class CategoryList(generics.ListCreateAPIView):
]
class CategoryDetail(generics.RetrieveUpdateDestroyAPIView):
class CategoryDetail(RetrieveUpdateDestroyAPI):
"""API endpoint for detail view of a single PartCategory object."""
serializer_class = part_serializers.CategorySerializer
@ -175,8 +178,11 @@ class CategoryDetail(generics.RetrieveUpdateDestroyAPIView):
def update(self, request, *args, **kwargs):
"""Perform 'update' function and mark this part as 'starred' (or not)"""
if 'starred' in request.data:
starred = str2bool(request.data.get('starred', False))
# Clean up input data
data = self.clean_data(request.data)
if 'starred' in data:
starred = str2bool(data.get('starred', False))
self.get_object().set_starred(request.user, starred)
@ -185,7 +191,7 @@ class CategoryDetail(generics.RetrieveUpdateDestroyAPIView):
return response
class CategoryMetadata(generics.RetrieveUpdateAPIView):
class CategoryMetadata(RetrieveUpdateAPI):
"""API endpoint for viewing / updating PartCategory metadata."""
def get_serializer(self, *args, **kwargs):
@ -195,7 +201,7 @@ class CategoryMetadata(generics.RetrieveUpdateAPIView):
queryset = PartCategory.objects.all()
class CategoryParameterList(generics.ListCreateAPIView):
class CategoryParameterList(ListCreateAPI):
"""API endpoint for accessing a list of PartCategoryParameterTemplate objects.
- GET: Return a list of PartCategoryParameterTemplate objects
@ -236,14 +242,14 @@ class CategoryParameterList(generics.ListCreateAPIView):
return queryset
class CategoryParameterDetail(generics.RetrieveUpdateDestroyAPIView):
class CategoryParameterDetail(RetrieveUpdateDestroyAPI):
"""Detail endpoint fro the PartCategoryParameterTemplate model"""
queryset = PartCategoryParameterTemplate.objects.all()
serializer_class = part_serializers.CategoryParameterTemplateSerializer
class CategoryTree(generics.ListAPIView):
class CategoryTree(ListAPI):
"""API endpoint for accessing a list of PartCategory objects ready for rendering a tree."""
queryset = PartCategory.objects.all()
@ -258,14 +264,14 @@ class CategoryTree(generics.ListAPIView):
ordering = ['level', 'name']
class PartSalePriceDetail(generics.RetrieveUpdateDestroyAPIView):
class PartSalePriceDetail(RetrieveUpdateDestroyAPI):
"""Detail endpoint for PartSellPriceBreak model."""
queryset = PartSellPriceBreak.objects.all()
serializer_class = part_serializers.PartSalePriceSerializer
class PartSalePriceList(generics.ListCreateAPIView):
class PartSalePriceList(ListCreateAPI):
"""API endpoint for list view of PartSalePriceBreak model."""
queryset = PartSellPriceBreak.objects.all()
@ -280,14 +286,14 @@ class PartSalePriceList(generics.ListCreateAPIView):
]
class PartInternalPriceDetail(generics.RetrieveUpdateDestroyAPIView):
class PartInternalPriceDetail(RetrieveUpdateDestroyAPI):
"""Detail endpoint for PartInternalPriceBreak model."""
queryset = PartInternalPriceBreak.objects.all()
serializer_class = part_serializers.PartInternalPriceSerializer
class PartInternalPriceList(generics.ListCreateAPIView):
class PartInternalPriceList(ListCreateAPI):
"""API endpoint for list view of PartInternalPriceBreak model."""
queryset = PartInternalPriceBreak.objects.all()
@ -318,21 +324,21 @@ class PartAttachmentList(AttachmentMixin, ListCreateDestroyAPIView):
]
class PartAttachmentDetail(AttachmentMixin, generics.RetrieveUpdateDestroyAPIView):
class PartAttachmentDetail(AttachmentMixin, RetrieveUpdateDestroyAPI):
"""Detail endpoint for PartAttachment model."""
queryset = PartAttachment.objects.all()
serializer_class = part_serializers.PartAttachmentSerializer
class PartTestTemplateDetail(generics.RetrieveUpdateDestroyAPIView):
class PartTestTemplateDetail(RetrieveUpdateDestroyAPI):
"""Detail endpoint for PartTestTemplate model."""
queryset = PartTestTemplate.objects.all()
serializer_class = part_serializers.PartTestTemplateSerializer
class PartTestTemplateList(generics.ListCreateAPIView):
class PartTestTemplateList(ListCreateAPI):
"""API endpoint for listing (and creating) a PartTestTemplate."""
queryset = PartTestTemplate.objects.all()
@ -372,7 +378,7 @@ class PartTestTemplateList(generics.ListCreateAPIView):
]
class PartThumbs(generics.ListAPIView):
class PartThumbs(ListAPI):
"""API endpoint for retrieving information on available Part thumbnails."""
queryset = Part.objects.all()
@ -415,7 +421,7 @@ class PartThumbs(generics.ListAPIView):
]
class PartThumbsUpdate(generics.RetrieveUpdateAPIView):
class PartThumbsUpdate(RetrieveUpdateAPI):
"""API endpoint for updating Part thumbnails."""
queryset = Part.objects.all()
@ -426,7 +432,7 @@ class PartThumbsUpdate(generics.RetrieveUpdateAPIView):
]
class PartScheduling(generics.RetrieveAPIView):
class PartScheduling(RetrieveAPI):
"""API endpoint for delivering "scheduling" information about a given part via the API.
Returns a chronologically ordered list about future "scheduled" events,
@ -560,7 +566,7 @@ class PartScheduling(generics.RetrieveAPIView):
return Response(schedule)
class PartMetadata(generics.RetrieveUpdateAPIView):
class PartMetadata(RetrieveUpdateAPI):
"""API endpoint for viewing / updating Part metadata."""
def get_serializer(self, *args, **kwargs):
@ -570,7 +576,7 @@ class PartMetadata(generics.RetrieveUpdateAPIView):
queryset = Part.objects.all()
class PartSerialNumberDetail(generics.RetrieveAPIView):
class PartSerialNumberDetail(RetrieveAPI):
"""API endpoint for returning extra serial number information about a particular part."""
queryset = Part.objects.all()
@ -595,7 +601,7 @@ class PartSerialNumberDetail(generics.RetrieveAPIView):
return Response(data)
class PartCopyBOM(generics.CreateAPIView):
class PartCopyBOM(CreateAPI):
"""API endpoint for duplicating a BOM."""
queryset = Part.objects.all()
@ -613,7 +619,7 @@ class PartCopyBOM(generics.CreateAPIView):
return ctx
class PartValidateBOM(generics.RetrieveUpdateAPIView):
class PartValidateBOM(RetrieveUpdateAPI):
"""API endpoint for 'validating' the BOM for a given Part."""
class BOMValidateSerializer(serializers.ModelSerializer):
@ -654,7 +660,10 @@ class PartValidateBOM(generics.RetrieveUpdateAPIView):
partial = kwargs.pop('partial', False)
serializer = self.get_serializer(part, data=request.data, partial=partial)
# Clean up input data before using it
data = self.clean_data(request.data)
serializer = self.get_serializer(part, data=data, partial=partial)
serializer.is_valid(raise_exception=True)
part.validate_bom(request.user)
@ -664,7 +673,7 @@ class PartValidateBOM(generics.RetrieveUpdateAPIView):
})
class PartDetail(generics.RetrieveUpdateDestroyAPIView):
class PartDetail(RetrieveUpdateDestroyAPI):
"""API endpoint for detail view of a single Part object."""
queryset = Part.objects.all()
@ -721,8 +730,11 @@ class PartDetail(generics.RetrieveUpdateDestroyAPIView):
- If the 'starred' field is provided, update the 'starred' status against current user
"""
if 'starred' in request.data:
starred = str2bool(request.data.get('starred', False))
# Clean input data
data = self.clean_data(request.data)
if 'starred' in data:
starred = str2bool(data.get('starred', False))
self.get_object().set_starred(request.user, starred)
@ -874,7 +886,7 @@ class PartFilter(rest_filters.FilterSet):
virtual = rest_filters.BooleanFilter()
class PartList(APIDownloadMixin, generics.ListCreateAPIView):
class PartList(APIDownloadMixin, ListCreateAPI):
"""API endpoint for accessing a list of Part objects.
- GET: Return list of objects
@ -1003,7 +1015,10 @@ class PartList(APIDownloadMixin, generics.ListCreateAPIView):
"""
# TODO: Unit tests for this function!
serializer = self.get_serializer(data=request.data)
# Clean up input data
data = self.clean_data(request.data)
serializer = self.get_serializer(data=data)
serializer.is_valid(raise_exception=True)
part = serializer.save()
@ -1011,23 +1026,23 @@ class PartList(APIDownloadMixin, generics.ListCreateAPIView):
# Optionally copy templates from category or parent category
copy_templates = {
'main': str2bool(request.data.get('copy_category_templates', False)),
'parent': str2bool(request.data.get('copy_parent_templates', False))
'main': str2bool(data.get('copy_category_templates', False)),
'parent': str2bool(data.get('copy_parent_templates', False))
}
part.save(**{'add_category_templates': copy_templates})
# Optionally copy data from another part (e.g. when duplicating)
copy_from = request.data.get('copy_from', None)
copy_from = data.get('copy_from', None)
if copy_from is not None:
try:
original = Part.objects.get(pk=copy_from)
copy_bom = str2bool(request.data.get('copy_bom', False))
copy_parameters = str2bool(request.data.get('copy_parameters', False))
copy_image = str2bool(request.data.get('copy_image', True))
copy_bom = str2bool(data.get('copy_bom', False))
copy_parameters = str2bool(data.get('copy_parameters', False))
copy_image = str2bool(data.get('copy_image', True))
# Copy image?
if copy_image:
@ -1046,12 +1061,12 @@ class PartList(APIDownloadMixin, generics.ListCreateAPIView):
pass
# Optionally create initial stock item
initial_stock = str2bool(request.data.get('initial_stock', False))
initial_stock = str2bool(data.get('initial_stock', False))
if initial_stock:
try:
initial_stock_quantity = Decimal(request.data.get('initial_stock_quantity', ''))
initial_stock_quantity = Decimal(data.get('initial_stock_quantity', ''))
if initial_stock_quantity <= 0:
raise ValidationError({
@ -1062,7 +1077,7 @@ class PartList(APIDownloadMixin, generics.ListCreateAPIView):
'initial_stock_quantity': [_('Must be a valid quantity')],
})
initial_stock_location = request.data.get('initial_stock_location', None)
initial_stock_location = data.get('initial_stock_location', None)
try:
initial_stock_location = StockLocation.objects.get(pk=initial_stock_location)
@ -1086,20 +1101,20 @@ class PartList(APIDownloadMixin, generics.ListCreateAPIView):
stock_item.save(user=request.user)
# Optionally add manufacturer / supplier data to the part
if part.purchaseable and str2bool(request.data.get('add_supplier_info', False)):
if part.purchaseable and str2bool(data.get('add_supplier_info', False)):
try:
manufacturer = Company.objects.get(pk=request.data.get('manufacturer', None))
manufacturer = Company.objects.get(pk=data.get('manufacturer', None))
except Exception:
manufacturer = None
try:
supplier = Company.objects.get(pk=request.data.get('supplier', None))
supplier = Company.objects.get(pk=data.get('supplier', None))
except Exception:
supplier = None
mpn = str(request.data.get('MPN', '')).strip()
sku = str(request.data.get('SKU', '')).strip()
mpn = str(data.get('MPN', '')).strip()
sku = str(data.get('SKU', '')).strip()
# Construct a manufacturer part
if manufacturer or mpn:
@ -1347,7 +1362,7 @@ class PartList(APIDownloadMixin, generics.ListCreateAPIView):
]
class PartRelatedList(generics.ListCreateAPIView):
class PartRelatedList(ListCreateAPI):
"""API endpoint for accessing a list of PartRelated objects."""
queryset = PartRelated.objects.all()
@ -1374,14 +1389,14 @@ class PartRelatedList(generics.ListCreateAPIView):
return queryset
class PartRelatedDetail(generics.RetrieveUpdateDestroyAPIView):
class PartRelatedDetail(RetrieveUpdateDestroyAPI):
"""API endpoint for accessing detail view of a PartRelated object."""
queryset = PartRelated.objects.all()
serializer_class = part_serializers.PartRelationSerializer
class PartParameterTemplateList(generics.ListCreateAPIView):
class PartParameterTemplateList(ListCreateAPI):
"""API endpoint for accessing a list of PartParameterTemplate objects.
- GET: Return list of PartParameterTemplate objects
@ -1441,14 +1456,14 @@ class PartParameterTemplateList(generics.ListCreateAPIView):
return queryset
class PartParameterTemplateDetail(generics.RetrieveUpdateDestroyAPIView):
class PartParameterTemplateDetail(RetrieveUpdateDestroyAPI):
"""API endpoint for accessing the detail view for a PartParameterTemplate object"""
queryset = PartParameterTemplate.objects.all()
serializer_class = part_serializers.PartParameterTemplateSerializer
class PartParameterList(generics.ListCreateAPIView):
class PartParameterList(ListCreateAPI):
"""API endpoint for accessing a list of PartParameter objects.
- GET: Return list of PartParameter objects
@ -1468,7 +1483,7 @@ class PartParameterList(generics.ListCreateAPIView):
]
class PartParameterDetail(generics.RetrieveUpdateDestroyAPIView):
class PartParameterDetail(RetrieveUpdateDestroyAPI):
"""API endpoint for detail view of a single PartParameter object."""
queryset = PartParameter.objects.all()
@ -1747,7 +1762,7 @@ class BomList(ListCreateDestroyAPIView):
]
class BomImportUpload(generics.CreateAPIView):
class BomImportUpload(CreateAPI):
"""API endpoint for uploading a complete Bill of Materials.
It is assumed that the BOM has been extracted from a file using the BomExtract endpoint.
@ -1758,7 +1773,10 @@ class BomImportUpload(generics.CreateAPIView):
def create(self, request, *args, **kwargs):
"""Custom create function to return the extracted data."""
serializer = self.get_serializer(data=request.data)
# Clean up input data
data = self.clean_data(request.data)
serializer = self.get_serializer(data=data)
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
headers = self.get_success_headers(serializer.data)
@ -1768,21 +1786,21 @@ class BomImportUpload(generics.CreateAPIView):
return Response(data, status=status.HTTP_201_CREATED, headers=headers)
class BomImportExtract(generics.CreateAPIView):
class BomImportExtract(CreateAPI):
"""API endpoint for extracting BOM data from a BOM file."""
queryset = Part.objects.none()
serializer_class = part_serializers.BomImportExtractSerializer
class BomImportSubmit(generics.CreateAPIView):
class BomImportSubmit(CreateAPI):
"""API endpoint for submitting BOM data from a BOM file."""
queryset = BomItem.objects.none()
serializer_class = part_serializers.BomImportSubmitSerializer
class BomDetail(generics.RetrieveUpdateDestroyAPIView):
class BomDetail(RetrieveUpdateDestroyAPI):
"""API endpoint for detail view of a single BomItem object."""
queryset = BomItem.objects.all()
@ -1798,7 +1816,7 @@ class BomDetail(generics.RetrieveUpdateDestroyAPIView):
return queryset
class BomItemValidate(generics.UpdateAPIView):
class BomItemValidate(UpdateAPI):
"""API endpoint for validating a BomItem."""
class BomItemValidationSerializer(serializers.Serializer):
@ -1812,11 +1830,13 @@ class BomItemValidate(generics.UpdateAPIView):
"""Perform update request."""
partial = kwargs.pop('partial', False)
valid = request.data.get('valid', False)
# Clean up input data
data = self.clean_data(request.data)
valid = data.get('valid', False)
instance = self.get_object()
serializer = self.get_serializer(instance, data=request.data, partial=partial)
serializer = self.get_serializer(instance, data=data, partial=partial)
serializer.is_valid(raise_exception=True)
if type(instance) == BomItem:
@ -1825,7 +1845,7 @@ class BomItemValidate(generics.UpdateAPIView):
return Response(serializer.data)
class BomItemSubstituteList(generics.ListCreateAPIView):
class BomItemSubstituteList(ListCreateAPI):
"""API endpoint for accessing a list of BomItemSubstitute objects."""
serializer_class = part_serializers.BomItemSubstituteSerializer
@ -1843,7 +1863,7 @@ class BomItemSubstituteList(generics.ListCreateAPIView):
]
class BomItemSubstituteDetail(generics.RetrieveUpdateDestroyAPIView):
class BomItemSubstituteDetail(RetrieveUpdateDestroyAPI):
"""API endpoint for detail view of a single BomItemSubstitute object."""
queryset = BomItemSubstitute.objects.all()

View File

@ -19,7 +19,7 @@ Relevant PRs:
from decimal import Decimal
from django.db import models
from django.db.models import OuterRef, Q
from django.db.models import F, FloatField, Func, OuterRef, Q, Subquery
from django.db.models.functions import Coalesce
from sql_util.utils import SubquerySum
@ -139,3 +139,22 @@ def variant_stock_query(reference: str = '', filter: Q = stock.models.StockItem.
part__lft__gt=OuterRef(f'{reference}lft'),
part__rght__lt=OuterRef(f'{reference}rght'),
).filter(filter)
def annotate_variant_quantity(subquery: Q, reference: str = 'quantity'):
"""Create a subquery annotation for all variant part stock items on the given parent query
Args:
subquery: A 'variant_stock_query' Q object
reference: The relationship reference of the variant stock items from the current queryset
"""
return Coalesce(
Subquery(
subquery.annotate(
total=Func(F(reference), function='SUM', output_field=FloatField())
).values('total')
),
0,
output_field=FloatField(),
)

View File

@ -13,7 +13,7 @@ from django.contrib.auth.models import User
from django.core.exceptions import ValidationError
from django.core.validators import MinValueValidator
from django.db import models, transaction
from django.db.models import Q, Sum, UniqueConstraint
from django.db.models import ExpressionWrapper, F, Q, Sum, UniqueConstraint
from django.db.models.functions import Coalesce
from django.db.models.signals import post_save
from django.db.utils import IntegrityError
@ -34,6 +34,7 @@ from stdimage.models import StdImageField
import common.models
import InvenTree.ready
import InvenTree.tasks
import part.filters as part_filters
import part.settings as part_settings
from build import models as BuildModels
from common.models import InvenTreeSetting
@ -74,9 +75,9 @@ class PartCategory(MetadataMixin, InvenTreeTree):
tree_id = self.tree_id
# Update each part in this category to point to the parent category
for part in self.parts.all():
part.category = self.parent
part.save()
for p in self.parts.all():
p.category = self.parent
p.save()
# Update each child category
for child in self.children.all():
@ -221,7 +222,7 @@ class PartCategory(MetadataMixin, InvenTreeTree):
if include_parents:
queryset = PartCategoryStar.objects.filter(
category__pk__in=[cat.pk for cat in cats]
category__in=cats,
)
else:
queryset = PartCategoryStar.objects.filter(
@ -800,7 +801,10 @@ class Part(MetadataMixin, MPTTModel):
upload_to=rename_part_image,
null=True,
blank=True,
variations={'thumbnail': (128, 128)},
variations={
'thumbnail': (128, 128),
'preview': (256, 256),
},
delete_orphans=False,
verbose_name=_('Image'),
)
@ -968,13 +972,10 @@ class Part(MetadataMixin, MPTTModel):
def requiring_build_orders(self):
"""Return list of outstanding build orders which require this part."""
# List parts that this part is required for
parts = self.get_used_in().all()
part_ids = [part.pk for part in parts]
# Now, get a list of outstanding build orders which require this part
builds = BuildModels.Build.objects.filter(
part__in=part_ids,
part__in=self.get_used_in().all(),
status__in=BuildStatus.ACTIVE_CODES
)
@ -1098,7 +1099,7 @@ class Part(MetadataMixin, MPTTModel):
if include_variants:
queryset = queryset.filter(
part__pk__in=[part.pk for part in self.get_ancestors(include_self=True)]
part__in=self.get_ancestors(include_self=True),
)
else:
queryset = queryset.filter(part=self)
@ -1142,18 +1143,70 @@ class Part(MetadataMixin, MPTTModel):
total = None
bom_items = self.get_bom_items().prefetch_related('sub_part__stock_items')
# Prefetch related tables, to reduce query expense
queryset = self.get_bom_items().prefetch_related(
'sub_part__stock_items',
'sub_part__stock_items__allocations',
'sub_part__stock_items__sales_order_allocations',
'substitutes',
'substitutes__part__stock_items',
)
# Calculate the minimum number of parts that can be built using each sub-part
for item in bom_items.all():
stock = item.sub_part.available_stock
# Annotate the 'available stock' for each part in the BOM
ref = 'sub_part__'
queryset = queryset.alias(
total_stock=part_filters.annotate_total_stock(reference=ref),
so_allocations=part_filters.annotate_sales_order_allocations(reference=ref),
bo_allocations=part_filters.annotate_build_order_allocations(reference=ref),
)
# If (by some chance) we get here but the BOM item quantity is invalid,
# ignore!
if item.quantity <= 0:
continue
# Calculate the 'available stock' based on previous annotations
queryset = queryset.annotate(
available_stock=ExpressionWrapper(
F('total_stock') - F('so_allocations') - F('bo_allocations'),
output_field=models.DecimalField(),
)
)
n = int(stock / item.quantity)
# Extract similar information for any 'substitute' parts
ref = 'substitutes__part__'
queryset = queryset.alias(
sub_total_stock=part_filters.annotate_total_stock(reference=ref),
sub_so_allocations=part_filters.annotate_sales_order_allocations(reference=ref),
sub_bo_allocations=part_filters.annotate_build_order_allocations(reference=ref),
)
queryset = queryset.annotate(
substitute_stock=ExpressionWrapper(
F('sub_total_stock') - F('sub_so_allocations') - F('sub_bo_allocations'),
output_field=models.DecimalField(),
)
)
# Extract similar information for any 'variant' parts
variant_stock_query = part_filters.variant_stock_query(reference='sub_part__')
queryset = queryset.alias(
var_total_stock=part_filters.annotate_variant_quantity(variant_stock_query, reference='quantity'),
var_bo_allocations=part_filters.annotate_variant_quantity(variant_stock_query, reference='allocations__quantity'),
var_so_allocations=part_filters.annotate_variant_quantity(variant_stock_query, reference='sales_order_allocations__quantity'),
)
queryset = queryset.annotate(
variant_stock=ExpressionWrapper(
F('var_total_stock') - F('var_bo_allocations') - F('var_so_allocations'),
output_field=models.DecimalField(),
)
)
for item in queryset.all():
# Iterate through each item in the queryset, work out the limiting quantity
quantity = item.available_stock + item.substitute_stock
if item.allow_variants:
quantity += item.variant_stock
n = int(quantity / item.quantity)
if total is None or n < total:
total = n
@ -1336,11 +1389,10 @@ class Part(MetadataMixin, MPTTModel):
parents = self.get_ancestors(include_self=False)
# There are parents available
if parents.count() > 0:
parent_ids = [p.pk for p in parents]
if parents.exists():
parent_filter = Q(
part__id__in=parent_ids,
part__in=parents,
inherited=True
)
@ -1425,7 +1477,7 @@ class Part(MetadataMixin, MPTTModel):
@property
def has_bom(self):
"""Return True if this Part instance has any BOM items"""
return self.get_bom_items().count() > 0
return self.get_bom_items().exists()
def get_trackable_parts(self):
"""Return a queryset of all trackable parts in the BOM for this part."""
@ -1440,7 +1492,7 @@ class Part(MetadataMixin, MPTTModel):
This is important when building the part.
"""
return self.get_trackable_parts().count() > 0
return self.get_trackable_parts().exists()
@property
def bom_count(self):
@ -1482,7 +1534,7 @@ class Part(MetadataMixin, MPTTModel):
# Validate each line item, ignoring inherited ones
bom_items = self.get_bom_items(include_inherited=False)
for item in bom_items.all():
for item in bom_items:
item.validate_hash()
self.bom_checksum = self.get_bom_hash()
@ -1509,7 +1561,7 @@ class Part(MetadataMixin, MPTTModel):
if parts is None:
parts = set()
bom_items = self.get_bom_items().all()
bom_items = self.get_bom_items()
for bom_item in bom_items:
@ -1533,7 +1585,7 @@ class Part(MetadataMixin, MPTTModel):
def has_complete_bom_pricing(self):
"""Return true if there is pricing information for each item in the BOM."""
use_internal = common.models.InvenTreeSetting.get_setting('PART_BOM_USE_INTERNAL_PRICE', False)
for item in self.get_bom_items().all().select_related('sub_part'):
for item in self.get_bom_items().select_related('sub_part'):
if item.sub_part.get_price_range(internal=use_internal) is None:
return False
@ -1609,7 +1661,7 @@ class Part(MetadataMixin, MPTTModel):
min_price = None
max_price = None
for item in self.get_bom_items().all().select_related('sub_part'):
for item in self.get_bom_items().select_related('sub_part'):
if item.sub_part.pk == self.pk:
logger.warning(f"WARNING: BomItem ID {item.pk} contains itself in BOM")
@ -1689,7 +1741,7 @@ class Part(MetadataMixin, MPTTModel):
@property
def has_price_breaks(self):
"""Return True if this part has sale price breaks"""
return self.price_breaks.count() > 0
return self.price_breaks.exists()
@property
def price_breaks(self):
@ -1725,7 +1777,7 @@ class Part(MetadataMixin, MPTTModel):
@property
def has_internal_price_breaks(self):
"""Return True if this Part has internal pricing information"""
return self.internal_price_breaks.count() > 0
return self.internal_price_breaks.exists()
@property
def internal_price_breaks(self):
@ -1978,7 +2030,7 @@ class Part(MetadataMixin, MPTTModel):
@property
def has_variants(self):
"""Check if this Part object has variants underneath it."""
return self.get_all_variants().count() > 0
return self.get_all_variants().exists()
def get_all_variants(self):
"""Return all Part object which exist as a variant under this part."""
@ -1993,7 +2045,7 @@ class Part(MetadataMixin, MPTTModel):
b) It has non-virtual template parts above it
c) It has non-virtual sibling variants
"""
return self.get_conversion_options().count() > 0
return self.get_conversion_options().exists()
def get_conversion_options(self):
"""Return options for converting this part to a "variant" within the same tree.
@ -2520,7 +2572,7 @@ class BomItem(DataImportMixin, models.Model):
- Allow stock from all directly specified substitute parts
- If allow_variants is True, allow all part variants
"""
return Q(part__in=[part.pk for part in self.get_valid_parts_for_allocation()])
return Q(part__in=self.get_valid_parts_for_allocation())
def save(self, *args, **kwargs):
"""Enforce 'clean' operation when saving a BomItem instance"""

View File

@ -4,8 +4,7 @@ import imghdr
from decimal import Decimal
from django.db import models, transaction
from django.db.models import (ExpressionWrapper, F, FloatField, Func, Q,
Subquery)
from django.db.models import ExpressionWrapper, F, FloatField, Q
from django.db.models.functions import Coalesce
from django.urls import reverse_lazy
from django.utils.translation import gettext_lazy as _
@ -251,8 +250,6 @@ class PartBriefSerializer(InvenTreeModelSerializer):
thumbnail = serializers.CharField(source='get_thumbnail_url', read_only=True)
stock = serializers.FloatField(source='total_stock')
class Meta:
"""Metaclass defining serializer fields"""
model = Part
@ -270,7 +267,6 @@ class PartBriefSerializer(InvenTreeModelSerializer):
'is_template',
'purchaseable',
'salable',
'stock',
'trackable',
'virtual',
'units',
@ -322,14 +318,7 @@ class PartSerializer(InvenTreeModelSerializer):
variant_query = part.filters.variant_stock_query()
queryset = queryset.annotate(
variant_stock=Coalesce(
Subquery(
variant_query.annotate(
total=Func(F('quantity'), function='SUM', output_field=FloatField())
).values('total')),
0,
output_field=FloatField(),
)
variant_stock=part.filters.annotate_variant_quantity(variant_query, reference='quantity'),
)
# Filter to limit builds to "active"
@ -642,35 +631,14 @@ class BomItemSerializer(InvenTreeModelSerializer):
variant_stock_query = part.filters.variant_stock_query(reference='sub_part__')
queryset = queryset.alias(
variant_stock_total=Coalesce(
Subquery(
variant_stock_query.annotate(
total=Func(F('quantity'), function='SUM', output_field=FloatField())
).values('total')),
0,
output_field=FloatField()
),
variant_stock_build_order_allocations=Coalesce(
Subquery(
variant_stock_query.annotate(
total=Func(F('sales_order_allocations__quantity'), function='SUM', output_field=FloatField()),
).values('total')),
0,
output_field=FloatField(),
),
variant_stock_sales_order_allocations=Coalesce(
Subquery(
variant_stock_query.annotate(
total=Func(F('allocations__quantity'), function='SUM', output_field=FloatField()),
).values('total')),
0,
output_field=FloatField(),
)
variant_stock_total=part.filters.annotate_variant_quantity(variant_stock_query, reference='quantity'),
variant_bo_allocations=part.filters.annotate_variant_quantity(variant_stock_query, reference='sales_order_allocations__quantity'),
variant_so_allocations=part.filters.annotate_variant_quantity(variant_stock_query, reference='allocations__quantity'),
)
queryset = queryset.annotate(
available_variant_stock=ExpressionWrapper(
F('variant_stock_total') - F('variant_stock_build_order_allocations') - F('variant_stock_sales_order_allocations'),
F('variant_stock_total') - F('variant_bo_allocations') - F('variant_so_allocations'),
output_field=FloatField(),
)
)

View File

@ -690,17 +690,6 @@
});
});
// Load the BOM table data in the pricing view
{% if part.has_bom and roles.sales_order.view %}
loadBomTable($("#bom-pricing-table"), {
editable: false,
bom_url: "{% url 'api-bom-list' %}",
part_url: "{% url 'api-part-list' %}",
parent_id: {{ part.id }} ,
sub_part_detail: true,
});
{% endif %}
onPanelLoad("purchase-orders", function() {
loadPartPurchaseOrderTable(
"#purchase-order-table",
@ -885,9 +874,20 @@
);
});
onPanelLoad('pricing', function() {
{% default_currency as currency %}
// Load the BOM table data in the pricing view
{% if part.has_bom and roles.sales_order.view %}
loadBomTable($("#bom-pricing-table"), {
editable: false,
bom_url: "{% url 'api-bom-list' %}",
part_url: "{% url 'api-part-list' %}",
parent_id: {{ part.id }} ,
sub_part_detail: true,
});
{% endif %}
// history graphs
{% if price_history %}
var purchasepricedata = {
@ -1031,6 +1031,7 @@
}
var SalePriceChart = loadSellPricingChart($('#SalePriceChart'), salepricedata)
{% endif %}
});
enableSidebar('part');

View File

@ -18,7 +18,7 @@
{% endif %}
<img class="part-thumb" id='part-image'
{% if part.image %}
src="{{ part.image.url }}"
src="{{ part.image.preview.url }}"
{% else %}
src="{% static 'img/blank_image.png' %}"
{% endif %}/>

View File

@ -4,12 +4,14 @@ from django.conf import settings
from django.urls import include, re_path
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import filters, generics, permissions, status
from rest_framework import filters, permissions, status
from rest_framework.exceptions import NotFound
from rest_framework.response import Response
import plugin.serializers as PluginSerializers
from common.api import GlobalSettingsPermissions
from InvenTree.mixins import (CreateAPI, ListAPI, RetrieveUpdateAPI,
RetrieveUpdateDestroyAPI)
from plugin.base.action.api import ActionPluginView
from plugin.base.barcodes.api import barcode_api_urls
from plugin.base.locate.api import LocatePluginView
@ -17,7 +19,7 @@ from plugin.models import PluginConfig, PluginSetting
from plugin.registry import registry
class PluginList(generics.ListAPIView):
class PluginList(ListAPI):
"""API endpoint for list of PluginConfig objects.
- GET: Return a list of all PluginConfig objects
@ -80,7 +82,7 @@ class PluginList(generics.ListAPIView):
]
class PluginDetail(generics.RetrieveUpdateDestroyAPIView):
class PluginDetail(RetrieveUpdateDestroyAPI):
"""API detail endpoint for PluginConfig object.
get:
@ -97,7 +99,7 @@ class PluginDetail(generics.RetrieveUpdateDestroyAPIView):
serializer_class = PluginSerializers.PluginConfigSerializer
class PluginInstall(generics.CreateAPIView):
class PluginInstall(CreateAPI):
"""Endpoint for installing a new plugin."""
queryset = PluginConfig.objects.none()
@ -105,7 +107,10 @@ class PluginInstall(generics.CreateAPIView):
def create(self, request, *args, **kwargs):
"""Install a plugin via the API"""
serializer = self.get_serializer(data=request.data)
# Clean up input data
data = self.clean_data(request.data)
serializer = self.get_serializer(data=data)
serializer.is_valid(raise_exception=True)
result = self.perform_create(serializer)
result['input'] = serializer.data
@ -117,7 +122,7 @@ class PluginInstall(generics.CreateAPIView):
return serializer.save()
class PluginSettingList(generics.ListAPIView):
class PluginSettingList(ListAPI):
"""List endpoint for all plugin related settings.
- read only
@ -141,7 +146,7 @@ class PluginSettingList(generics.ListAPIView):
]
class PluginSettingDetail(generics.RetrieveUpdateAPIView):
class PluginSettingDetail(RetrieveUpdateAPI):
"""Detail endpoint for a plugin-specific setting.
Note that these cannot be created or deleted via the API

View File

@ -8,6 +8,7 @@ from django.utils.translation import gettext_lazy as _
import pdf2image
import common.notifications
from common.models import InvenTreeSetting
from InvenTree.exceptions import log_error
from plugin.registry import registry
@ -36,9 +37,10 @@ def print_label(plugin_slug: str, pdf_data, filename=None, label_instance=None,
return
# In addition to providing a .pdf image, we'll also provide a .png file
dpi = InvenTreeSetting.get_setting('LABEL_DPI', 300)
png_file = pdf2image.convert_from_bytes(
pdf_data,
dpi=300,
dpi=dpi,
)[0]
try:

View File

@ -8,7 +8,7 @@ from django.urls import include, path, re_path
from django.utils.translation import gettext_lazy as _
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import filters, generics
from rest_framework import filters
from rest_framework.response import Response
import build.models
@ -16,6 +16,7 @@ import common.models
import InvenTree.helpers
import order.models
import part.models
from InvenTree.mixins import ListAPI, RetrieveAPI, RetrieveUpdateDestroyAPI
from stock.models import StockItem, StockItemAttachment
from .models import (BillOfMaterialsReport, BuildReport, PurchaseOrderReport,
@ -25,7 +26,7 @@ from .serializers import (BOMReportSerializer, BuildReportSerializer,
SalesOrderReportSerializer, TestReportSerializer)
class ReportListView(generics.ListAPIView):
class ReportListView(ListAPI):
"""Generic API class for report templates."""
filter_backends = [
@ -330,14 +331,14 @@ class StockItemTestReportList(ReportListView, StockItemReportMixin):
return queryset
class StockItemTestReportDetail(generics.RetrieveUpdateDestroyAPIView):
class StockItemTestReportDetail(RetrieveUpdateDestroyAPI):
"""API endpoint for a single TestReport object."""
queryset = TestReport.objects.all()
serializer_class = TestReportSerializer
class StockItemTestReportPrint(generics.RetrieveAPIView, StockItemReportMixin, ReportPrintMixin):
class StockItemTestReportPrint(RetrieveAPI, StockItemReportMixin, ReportPrintMixin):
"""API endpoint for printing a TestReport object."""
queryset = TestReport.objects.all()
@ -427,14 +428,14 @@ class BOMReportList(ReportListView, PartReportMixin):
return queryset
class BOMReportDetail(generics.RetrieveUpdateDestroyAPIView):
class BOMReportDetail(RetrieveUpdateDestroyAPI):
"""API endpoint for a single BillOfMaterialReport object."""
queryset = BillOfMaterialsReport.objects.all()
serializer_class = BOMReportSerializer
class BOMReportPrint(generics.RetrieveAPIView, PartReportMixin, ReportPrintMixin):
class BOMReportPrint(RetrieveAPI, PartReportMixin, ReportPrintMixin):
"""API endpoint for printing a BillOfMaterialReport object."""
queryset = BillOfMaterialsReport.objects.all()
@ -509,14 +510,14 @@ class BuildReportList(ReportListView, BuildReportMixin):
return queryset
class BuildReportDetail(generics.RetrieveUpdateDestroyAPIView):
class BuildReportDetail(RetrieveUpdateDestroyAPI):
"""API endpoint for a single BuildReport object."""
queryset = BuildReport.objects.all()
serializer_class = BuildReportSerializer
class BuildReportPrint(generics.RetrieveAPIView, BuildReportMixin, ReportPrintMixin):
class BuildReportPrint(RetrieveAPI, BuildReportMixin, ReportPrintMixin):
"""API endpoint for printing a BuildReport."""
queryset = BuildReport.objects.all()
@ -586,14 +587,14 @@ class PurchaseOrderReportList(ReportListView, OrderReportMixin):
return queryset
class PurchaseOrderReportDetail(generics.RetrieveUpdateDestroyAPIView):
class PurchaseOrderReportDetail(RetrieveUpdateDestroyAPI):
"""API endpoint for a single PurchaseOrderReport object."""
queryset = PurchaseOrderReport.objects.all()
serializer_class = PurchaseOrderReportSerializer
class PurchaseOrderReportPrint(generics.RetrieveAPIView, OrderReportMixin, ReportPrintMixin):
class PurchaseOrderReportPrint(RetrieveAPI, OrderReportMixin, ReportPrintMixin):
"""API endpoint for printing a PurchaseOrderReport object."""
OrderModel = order.models.PurchaseOrder
@ -665,14 +666,14 @@ class SalesOrderReportList(ReportListView, OrderReportMixin):
return queryset
class SalesOrderReportDetail(generics.RetrieveUpdateDestroyAPIView):
class SalesOrderReportDetail(RetrieveUpdateDestroyAPI):
"""API endpoint for a single SalesOrderReport object."""
queryset = SalesOrderReport.objects.all()
serializer_class = SalesOrderReportSerializer
class SalesOrderReportPrint(generics.RetrieveAPIView, OrderReportMixin, ReportPrintMixin):
class SalesOrderReportPrint(RetrieveAPI, OrderReportMixin, ReportPrintMixin):
"""API endpoint for printing a PurchaseOrderReport object."""
OrderModel = order.models.SalesOrder

View File

@ -12,7 +12,7 @@ from django.utils.translation import gettext_lazy as _
from django_filters import rest_framework as rest_filters
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import filters, generics, status
from rest_framework import filters, status
from rest_framework.response import Response
from rest_framework.serializers import ValidationError
@ -27,6 +27,8 @@ from InvenTree.api import (APIDownloadMixin, AttachmentMixin,
from InvenTree.filters import InvenTreeOrderingFilter
from InvenTree.helpers import (DownloadFile, extract_serial_numbers, isNull,
str2bool)
from InvenTree.mixins import (CreateAPI, ListAPI, ListCreateAPI, RetrieveAPI,
RetrieveUpdateAPI, RetrieveUpdateDestroyAPI)
from order.models import PurchaseOrder, SalesOrder, SalesOrderAllocation
from order.serializers import PurchaseOrderSerializer
from part.models import BomItem, Part, PartCategory
@ -37,7 +39,7 @@ from stock.models import (StockItem, StockItemAttachment, StockItemTestResult,
StockItemTracking, StockLocation)
class StockDetail(generics.RetrieveUpdateDestroyAPIView):
class StockDetail(RetrieveUpdateDestroyAPI):
"""API detail endpoint for Stock object.
get:
@ -72,13 +74,12 @@ class StockDetail(generics.RetrieveUpdateDestroyAPIView):
kwargs['part_detail'] = True
kwargs['location_detail'] = True
kwargs['supplier_part_detail'] = True
kwargs['test_detail'] = True
kwargs['context'] = self.get_serializer_context()
return self.serializer_class(*args, **kwargs)
class StockMetadata(generics.RetrieveUpdateAPIView):
class StockMetadata(RetrieveUpdateAPI):
"""API endpoint for viewing / updating StockItem metadata."""
def get_serializer(self, *args, **kwargs):
@ -106,13 +107,13 @@ class StockItemContextMixin:
return context
class StockItemSerialize(StockItemContextMixin, generics.CreateAPIView):
class StockItemSerialize(StockItemContextMixin, CreateAPI):
"""API endpoint for serializing a stock item."""
serializer_class = StockSerializers.SerializeStockItemSerializer
class StockItemInstall(StockItemContextMixin, generics.CreateAPIView):
class StockItemInstall(StockItemContextMixin, CreateAPI):
"""API endpoint for installing a particular stock item into this stock item.
- stock_item.part must be in the BOM for this part
@ -123,25 +124,25 @@ class StockItemInstall(StockItemContextMixin, generics.CreateAPIView):
serializer_class = StockSerializers.InstallStockItemSerializer
class StockItemUninstall(StockItemContextMixin, generics.CreateAPIView):
class StockItemUninstall(StockItemContextMixin, CreateAPI):
"""API endpoint for removing (uninstalling) items from this item."""
serializer_class = StockSerializers.UninstallStockItemSerializer
class StockItemConvert(StockItemContextMixin, generics.CreateAPIView):
class StockItemConvert(StockItemContextMixin, CreateAPI):
"""API endpoint for converting a stock item to a variant part"""
serializer_class = StockSerializers.ConvertStockItemSerializer
class StockItemReturn(StockItemContextMixin, generics.CreateAPIView):
class StockItemReturn(StockItemContextMixin, CreateAPI):
"""API endpoint for returning a stock item from a customer"""
serializer_class = StockSerializers.ReturnStockItemSerializer
class StockAdjustView(generics.CreateAPIView):
class StockAdjustView(CreateAPI):
"""A generic class for handling stocktake actions.
Subclasses exist for:
@ -186,7 +187,7 @@ class StockTransfer(StockAdjustView):
serializer_class = StockSerializers.StockTransferSerializer
class StockAssign(generics.CreateAPIView):
class StockAssign(CreateAPI):
"""API endpoint for assigning stock to a particular customer."""
queryset = StockItem.objects.all()
@ -200,7 +201,7 @@ class StockAssign(generics.CreateAPIView):
return ctx
class StockMerge(generics.CreateAPIView):
class StockMerge(CreateAPI):
"""API endpoint for merging multiple stock items."""
queryset = StockItem.objects.none()
@ -213,7 +214,7 @@ class StockMerge(generics.CreateAPIView):
return ctx
class StockLocationList(generics.ListCreateAPIView):
class StockLocationList(ListCreateAPI):
"""API endpoint for list view of StockLocation objects.
- GET: Return list of StockLocation objects
@ -305,7 +306,7 @@ class StockLocationList(generics.ListCreateAPIView):
]
class StockLocationTree(generics.ListAPIView):
class StockLocationTree(ListAPI):
"""API endpoint for accessing a list of StockLocation objects, ready for rendering as a tree."""
queryset = StockLocation.objects.all()
@ -502,7 +503,8 @@ class StockList(APIDownloadMixin, ListCreateDestroyAPIView):
# Copy the request data, to side-step "mutability" issues
data = OrderedDict()
data.update(request.data)
# Update with cleaned input data
data.update(self.clean_data(request.data))
quantity = data.get('quantity', None)
@ -1067,14 +1069,14 @@ class StockAttachmentList(AttachmentMixin, ListCreateDestroyAPIView):
]
class StockAttachmentDetail(AttachmentMixin, generics.RetrieveUpdateDestroyAPIView):
class StockAttachmentDetail(AttachmentMixin, RetrieveUpdateDestroyAPI):
"""Detail endpoint for StockItemAttachment."""
queryset = StockItemAttachment.objects.all()
serializer_class = StockSerializers.StockItemAttachmentSerializer
class StockItemTestResultDetail(generics.RetrieveUpdateDestroyAPIView):
class StockItemTestResultDetail(RetrieveUpdateDestroyAPI):
"""Detail endpoint for StockItemTestResult."""
queryset = StockItemTestResult.objects.all()
@ -1170,14 +1172,14 @@ class StockItemTestResultList(ListCreateDestroyAPIView):
test_result.save()
class StockTrackingDetail(generics.RetrieveAPIView):
class StockTrackingDetail(RetrieveAPI):
"""Detail API endpoint for StockItemTracking model."""
queryset = StockItemTracking.objects.all()
serializer_class = StockSerializers.StockTrackingSerializer
class StockTrackingList(generics.ListAPIView):
class StockTrackingList(ListAPI):
"""API endpoint for list view of StockItemTracking objects.
StockItemTracking objects are read-only
@ -1276,7 +1278,10 @@ class StockTrackingList(generics.ListAPIView):
Here we override the default 'create' implementation,
to save the user information associated with the request object.
"""
serializer = self.get_serializer(data=request.data)
# Clean up input data
data = self.clean_data(request.data)
serializer = self.get_serializer(data=data)
serializer.is_valid(raise_exception=True)
# Record the user who created this Part object
@ -1314,7 +1319,7 @@ class StockTrackingList(generics.ListAPIView):
]
class LocationMetadata(generics.RetrieveUpdateAPIView):
class LocationMetadata(RetrieveUpdateAPI):
"""API endpoint for viewing / updating StockLocation metadata."""
def get_serializer(self, *args, **kwargs):
@ -1324,7 +1329,7 @@ class LocationMetadata(generics.RetrieveUpdateAPIView):
queryset = StockLocation.objects.all()
class LocationDetail(generics.RetrieveUpdateDestroyAPIView):
class LocationDetail(RetrieveUpdateDestroyAPI):
"""API endpoint for detail view of StockLocation object.
- GET: Return a single StockLocation object

View File

@ -88,6 +88,12 @@ class StockItemSerializer(InvenTree.serializers.InvenTreeModelSerializer):
@staticmethod
def annotate_queryset(queryset):
"""Add some extra annotations to the queryset, performing database queries as efficiently as possible."""
queryset = queryset.prefetch_related(
'sales_order',
'purchase_order',
)
# Annotate the queryset with the total allocated to sales orders
queryset = queryset.annotate(
allocated=Coalesce(
@ -136,20 +142,14 @@ class StockItemSerializer(InvenTree.serializers.InvenTreeModelSerializer):
location_detail = LocationBriefSerializer(source='location', many=False, read_only=True)
tracking_items = serializers.IntegerField(source='tracking_info_count', read_only=True, required=False)
quantity = InvenTreeDecimalField()
allocated = serializers.FloatField(source='allocation_count', required=False)
# Annotated fields
tracking_items = serializers.IntegerField(read_only=True, required=False)
allocated = serializers.FloatField(required=False)
expired = serializers.BooleanField(required=False, read_only=True)
stale = serializers.BooleanField(required=False, read_only=True)
# serial = serializers.CharField(required=False)
required_tests = serializers.IntegerField(source='required_test_count', read_only=True, required=False)
purchase_price = InvenTree.serializers.InvenTreeMoneySerializer(
label=_('Purchase Price'),
max_digits=19, decimal_places=4,
@ -171,7 +171,6 @@ class StockItemSerializer(InvenTree.serializers.InvenTreeModelSerializer):
return str(obj.purchase_price) if obj.purchase_price else '-'
purchase_order_reference = serializers.CharField(source='purchase_order.reference', read_only=True)
sales_order_reference = serializers.CharField(source='sales_order.reference', read_only=True)
def __init__(self, *args, **kwargs):
@ -179,7 +178,6 @@ class StockItemSerializer(InvenTree.serializers.InvenTreeModelSerializer):
part_detail = kwargs.pop('part_detail', False)
location_detail = kwargs.pop('location_detail', False)
supplier_part_detail = kwargs.pop('supplier_part_detail', False)
test_detail = kwargs.pop('test_detail', False)
super(StockItemSerializer, self).__init__(*args, **kwargs)
@ -192,9 +190,6 @@ class StockItemSerializer(InvenTree.serializers.InvenTreeModelSerializer):
if supplier_part_detail is not True:
self.fields.pop('supplier_part_detail')
if test_detail is not True:
self.fields.pop('required_tests')
class Meta:
"""Metaclass options."""
@ -208,7 +203,6 @@ class StockItemSerializer(InvenTree.serializers.InvenTreeModelSerializer):
'delete_on_deplete',
'expired',
'expiry_date',
'in_stock',
'is_building',
'link',
'location',
@ -222,7 +216,6 @@ class StockItemSerializer(InvenTree.serializers.InvenTreeModelSerializer):
'purchase_order_reference',
'pk',
'quantity',
'required_tests',
'sales_order',
'sales_order_reference',
'serial',
@ -249,7 +242,6 @@ class StockItemSerializer(InvenTree.serializers.InvenTreeModelSerializer):
'stocktake_date',
'stocktake_user',
'updated',
'in_stock'
]

View File

@ -136,7 +136,7 @@
{% endblock actions %}
{% block thumbnail %}
<img class='part-thumb' {% if item.part.image %}src="{{ item.part.image.url }}"{% else %}src="{% static 'img/blank_image.png' %}"{% endif %}/>
<img class='part-thumb' {% if item.part.image %}src="{{ item.part.image.preview.url }}"{% else %}src="{% static 'img/blank_image.png' %}"{% endif %}/>
{% endblock thumbnail %}
{% block details %}

View File

@ -0,0 +1,20 @@
{% extends "panel.html" %}
{% load i18n %}
{% load inventree_extras %}
{% block label %}labels{% endblock %}
{% block heading %}
{% trans "Label Settings" %}
{% endblock %}
{% block content %}
<table class='table table-striped table-condensed'>
<tbody>
{% include "InvenTree/settings/setting.html" with key="LABEL_ENABLE" icon='fa-toggle-on' %}
{% include "InvenTree/settings/setting.html" with key="LABEL_DPI" icon='fa-toggle-on' %}
</tbody>
</table>
{% endblock %}

View File

@ -33,6 +33,7 @@
{% include "InvenTree/settings/login.html" %}
{% include "InvenTree/settings/barcode.html" %}
{% include "InvenTree/settings/currencies.html" %}
{% include "InvenTree/settings/label.html" %}
{% include "InvenTree/settings/report.html" %}
{% include "InvenTree/settings/part.html" %}
{% include "InvenTree/settings/category.html" %}

View File

@ -34,6 +34,8 @@
{% include "sidebar_item.html" with label='barcodes' text=text icon="fa-qrcode" %}
{% trans "Currencies" as text %}
{% include "sidebar_item.html" with label='currencies' text=text icon="fa-dollar-sign" %}
{% trans "Label Printing" as text %}
{% include "sidebar_item.html" with label='labels' text=text icon='fa-tag' %}
{% trans "Reporting" as text %}
{% include "sidebar_item.html" with label='reporting' text=text icon="fa-file-pdf" %}
{% trans "Parts" as text %}

View File

@ -14,7 +14,6 @@
<div class='row'>
<table class='table table-striped table-condensed'>
<tbody>
{% include "InvenTree/settings/setting.html" with key="LABEL_ENABLE" icon='fa-toggle-on' user_setting=True %}
{% include "InvenTree/settings/setting.html" with key="LABEL_INLINE" icon='fa-tag' user_setting=True %}
</tbody>
</table>

View File

@ -2,18 +2,7 @@
{% load inventree_extras %}
{% load i18n %}
<div class='modal fade modal-fixed-footer' tabindex='-1' role='dialog' id='modal-about'>
<div class='modal-dialog'>
<div class='modal-content'>
<div class="modal-header">
<img src="{% static 'img/inventree.png' %}" height='40px' style='float: left; padding-right: 25px;' alt='Inventree Logo'>
<h4>{% trans "InvenTree Version Information" %}</h4>
<button type='button' class='btn-close' data-bs-dismiss='modal' aria-label='{% trans "Close" %}'></button>
</div>
<div class='modal-form-content-wrapper'>
<div class='modal-form-content'>
<div>
<table class='table table-striped table-condensed'>
<table class='table table-striped table-condensed'>
<col width='25'>
<tr>
<td><span class='fas fa-hashtag'></span></td>
@ -96,14 +85,4 @@
</span>
</td>
</tr>
</table>
</div>
</div>
</div>
<div class='modal-footer'>
<button type='button' class='btn btn-outline-secondary' data-bs-dismiss='modal'>{% trans "Close" %}</button>
</div>
</div>
</div>
</div>
</table>

View File

@ -84,29 +84,14 @@
{% endblock %}
</div>
<!-- Scripts -->
<script type="text/javascript" src="{% static 'script/jquery_3.3.1_jquery.min.js' %}"></script>
<script type='text/javascript' src="{% static 'script/jquery-ui/jquery-ui.min.js' %}"></script>
<script type="text/javascript" src="{% static 'bootstrap/js/bootstrap.bundle.min.js' %}"></script>
<!-- general JS -->
{% include "third_party_js.html" %}
<script type='text/javascript' src="{% static 'script/inventree/inventree.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'notification.js' %}"></script>
<!-- fontawesome -->
<script type='text/javascript' src="{% static 'fontawesome/js/solid.js' %}"></script>
<script type='text/javascript' src="{% static 'fontawesome/js/brands.js' %}"></script>
<script type='text/javascript' src="{% static 'fontawesome/js/fontawesome.js' %}"></script>
<!-- 3rd party general js -->
<script type="text/javascript" src="{% static 'fullcalendar/main.js' %}"></script>
<script type="text/javascript" src="{% static 'fullcalendar/locales-all.js' %}"></script>
<script type="text/javascript" src="{% static 'select2/js/select2.full.js' %}"></script>
<script type='text/javascript' src="{% static 'script/moment.js' %}"></script>
<script type='text/javascript' src="{% static 'script/chart.min.js' %}"></script>
<script type='text/javascript' src="{% static 'script/clipboard.min.js' %}"></script>
<script type='text/javascript' src="{% static 'script/randomColor.min.js' %}"></script>
<script type='text/javascript'>

View File

@ -39,7 +39,9 @@ for a account and sign in below:{% endblocktrans %}</p>
<div>{{ login_message | safe }}<hr></div>
{% endif %}
<div class="btn-group float-right" role="group">
<button class="btn btn-success" type="submit">{% trans "Sign In" %}</button>
<button class="btn btn-success" type="submit">
<span class='fas fa-sign-in-alt'></span> {% trans "Sign In" %}
</button>
</div>
{% if mail_conf and enable_pwd_forgot %}
<a class="" href="{% url 'account_reset_password' %}"><small>{% trans "Forgot Password?" %}</small></a>

View File

@ -5,7 +5,7 @@
{% block head_title %}{% trans "Sign Out" %}{% endblock %}
{% block content %}
<h1>{% trans "Sign Out" %}</h1>
<h3>{% trans "Sign Out" %}</h3>
<p>{% trans 'Are you sure you want to sign out?' %}</p>
@ -16,10 +16,16 @@
{% endif %}
<hr>
<div class='btn-group float-right' role='group'>
<a type='button' class='btn btn-secondary' href='{% url "index" %}'><span class='fas fa-undo-alt'></span> {% trans "Back to Site" %}</a>
<button type="submit" class="btn btn-danger btn-block">{% trans 'Sign Out' %}</button>
<button type="submit" class="btn btn-danger btn-block">
<span class='fas fa-sign-out-alt'></span> {% trans 'Sign Out' %}
</button>
</div>
</form>
<div>
<a type='button' class='btn btn-secondary' href='{% url "index" %}'>
<span class='fas fa-undo-alt'></span> {% trans "Return to Site" %}
</a>
</div>
{% endblock %}

View File

@ -9,7 +9,7 @@
{{ form|crispy }}
<button type="submit" class="btn btn-primary">
{% trans 'Authenticate' %}
<span class='fas fa-check-circle'></span> {% trans 'Authenticate' %}
</button>
</form>
{% endblock %}

View File

@ -17,17 +17,23 @@
{% trans 'Backup tokens have been generated, but are not revealed here for security reasons. Press the button below to generate new ones.' %}
{% endif %}
{% else %}
{% trans 'No tokens. Press the button below to generate some.' %}
{% trans 'No backup tokens are available. Press the button below to generate some.' %}
{% endif %}
<br>
<hr>
<form method="post">
{% csrf_token %}
<div class='btn-group float-right' role='group'>
<button type="submit" class="btn btn-primary w-100">
{% trans 'Generate backup tokens' %}
<span class='fas fa-key'></span> {% trans 'Generate Tokens' %}
</button>
</div>
</form>
<br>
<a href="{% url 'settings' %}" class="btn btn-secondary w-100 btn-sm">{% trans "Back to settings" %}</a>
<div>
<a type='button' href="{% url 'settings' %}" class="btn btn-secondary">
<span class='fas fa-undo-alt'></span> {% trans "Return to Site" %}
</a>
</div>
{% endblock %}

View File

@ -1,5 +1,5 @@
{% extends "account/base.html" %}
{% load i18n %}
{% load i18n crispy_forms_tags %}
{% block content %}
<h3>
@ -10,9 +10,18 @@
<form method="post">
{% csrf_token %}
{{ form|crispy }}
<hr>
<div class='btn-group float-right' role='group'>
<button type="submit" class="btn btn-danger w-100">
{% trans 'Disable Two-Factor' %}
<span class='fas fa-times-circle'></span> {% trans 'Disable 2FA' %}
</button>
</div>
</form>
<div>
<a type='button' href="{% url 'settings' %}" class="btn btn-secondary">
<span class='fas fa-undo-alt'></span> {% trans "Return to Site" %}
</a>
</div>
{% endblock %}

View File

@ -31,12 +31,17 @@
{% csrf_token %}
{{ form|crispy }}
<hr>
<div class='btn-group float-right' role='group'>
<button type="submit" class="btn btn-primary btn-block w-100">
{% trans 'Verify' %}
<span class='fas fa-check-circle'></span> {% trans 'Verify' %}
</button>
</div>
</form>
<div>
<a href="{% url 'settings' %}" class="btn btn-secondary w-100 btn-sm mt-3">{% trans "Back to settings" %}</a>
<a type='button' href="{% url 'settings' %}" class="btn btn-secondary">
<span class='fas fa-undo-alt'></span> {% trans "Return to Site" %}
</a>
</div>
{% endblock %}

View File

@ -7,7 +7,7 @@
{% settings_value 'REPORT_ENABLE_TEST_REPORT' as test_report_enabled %}
{% settings_value "REPORT_ENABLE" as report_enabled %}
{% settings_value "SERVER_RESTART_REQUIRED" as server_restart_required %}
{% settings_value "LABEL_ENABLE" with user=user as labels_enabled %}
{% settings_value "LABEL_ENABLE" as labels_enabled %}
{% inventree_show_about user as show_about %}
<!DOCTYPE html>
@ -39,15 +39,15 @@
<!-- CSS -->
<link rel="stylesheet" href="{% static 'bootstrap/css/bootstrap.min.css' %}">
<link rel="stylesheet" href="{% static 'bootstrap-table/bootstrap-table.css' %}">
<link rel="stylesheet" href="{% static 'bootstrap-table/extensions/group-by-v2/bootstrap-table-group-by.css' %}">
<link rel="stylesheet" href="{% static 'bootstrap-table/bootstrap-table.min.css' %}">
<link rel="stylesheet" href="{% static 'bootstrap-table/extensions/group-by-v2/bootstrap-table-group-by.min.css' %}">
<link rel="stylesheet" href="{% static 'bootstrap-table/extensions/filter-control/bootstrap-table-filter-control.css' %}">
<link rel='stylesheet' href='{% static "treegrid/css/jquery.treegrid.css" %}'>
<link rel="stylesheet" href="{% static 'fontawesome/css/brands.min.css' %}">
<link rel="stylesheet" href="{% static 'fontawesome/css/solid.min.css' %}">
<link rel="stylesheet" href="{% static 'select2/css/select2.css' %}">
<link rel="stylesheet" href="{% static 'select2/css/select2.min.css' %}">
<link rel="stylesheet" href="{% static 'select2/css/select2-bootstrap-5-theme.css' %}">
<link rel="stylesheet" href="{% static 'fullcalendar/main.css' %}">
<link rel="stylesheet" href="{% static 'fullcalendar/main.min.css' %}">
<link rel="stylesheet" href="{% static 'script/jquery-ui/jquery-ui.min.css' %}">
<link rel="stylesheet" href="{% static 'easymde/easymde.min.css' %}">
@ -132,83 +132,48 @@
</div>
{% include 'modals.html' %}
{% if show_about %}{% include 'about.html' %}{% endif %}
{% include "notifications.html" %}
{% include "search.html" %}
</div>
<!-- Scripts -->
<script type="text/javascript" src="{% static 'script/jquery_3.3.1_jquery.min.js' %}"></script>
<script type='text/javascript' src="{% static 'script/jquery.form.min.js' %}"></script>
<script type='text/javascript' src="{% static 'script/jquery-ui/jquery-ui.min.js' %}"></script>
{% include "third_party_js.html" %}
<script type="text/javascript" src="{% static 'bootstrap/js/bootstrap.bundle.min.js' %}"></script>
<script type='text/javascript' src="{% static 'script/bootstrap/bootstrap-treeview.js' %}"></script>
<script type='text/javascript' src="{% static 'bootstrap-table/bootstrap-table.js' %}"></script>
<!-- jquery-treegrid -->
<script type='text/javascript' src='{% static "treegrid/js/jquery.treegrid.js" %}'></script>
<script type='text/javascript' src='{% static "treegrid/js/jquery.treegrid.bootstrap3.js" %}'></script>
<!-- boostrap-table extensions -->
<script type='text/javascript' src='{% static "bootstrap-table/extensions/group-by-v2/bootstrap-table-group-by.js" %}'></script>
<script type='text/javascript' src='{% static "bootstrap-table/extensions/filter-control/bootstrap-table-filter-control.js" %}'></script>
<script type='text/javascript' src='{% static "bootstrap-table/extensions/treegrid/bootstrap-table-treegrid.js" %}'></script>
<script type='text/javascript' src='{% static "bootstrap-table/extensions/custom-view/bootstrap-table-custom-view.js" %}'></script>
<!-- 3rd party general js -->
<script type="text/javascript" src="{% static 'fullcalendar/main.js' %}"></script>
<script type="text/javascript" src="{% static 'fullcalendar/locales-all.js' %}"></script>
<script type="text/javascript" src="{% static 'select2/js/select2.full.js' %}"></script>
<script type='text/javascript' src="{% static 'script/chart.js' %}"></script>
<script type='text/javascript' src="{% static 'script/moment.js' %}"></script>
<script type='text/javascript' src="{% static 'script/chartjs-adapter-moment.js' %}"></script>
<script type='text/javascript' src="{% static 'easymde/easymde.min.js' %}"></script>
<script type='text/javascript' src="{% static 'script/clipboard.min.js' %}"></script>
<script type='text/javascript' src="{% static 'script/randomColor.min.js' %}"></script>
<script type='text/javascript' src="{% static 'script/qr-scanner.umd.min.js' %}"></script>
<!-- general JS -->
<script type='text/javascript' src="{% static 'script/inventree/inventree.js' %}"></script>
<script defer type='text/javascript' src="{% static 'script/inventree/inventree.js' %}"></script>
<!-- dynamic javascript templates -->
<script type='text/javascript' src="{% url 'calendar.js' %}"></script>
<script type='text/javascript' src="{% url 'nav.js' %}"></script>
<script type='text/javascript' src="{% url 'settings.js' %}"></script>
<script defer type='text/javascript' src="{% url 'calendar.js' %}"></script>
<script defer type='text/javascript' src="{% url 'nav.js' %}"></script>
<script defer type='text/javascript' src="{% url 'settings.js' %}"></script>
<!-- translated javascript templates-->
<script type='text/javascript' src="{% i18n_static 'api.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'attachment.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'barcode.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'bom.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'build.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'company.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'filters.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'forms.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'helpers.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'label.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'modals.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'model_renderers.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'order.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'part.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'report.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'search.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'stock.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'plugin.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'tables.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'table_filters.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'notification.js' %}"></script>
<script type='text/javascript' src="{% static 'fontawesome/js/solid.min.js' %}"></script>
<script type='text/javascript' src="{% static 'fontawesome/js/regular.min.js' %}"></script>
<script type='text/javascript' src="{% static 'fontawesome/js/brands.min.js' %}"></script>
<script type='text/javascript' src="{% static 'fontawesome/js/fontawesome.min.js' %}"></script>
<script defer type='text/javascript' src="{% i18n_static 'api.js' %}"></script>
<script defer type='text/javascript' src="{% i18n_static 'attachment.js' %}"></script>
<script defer type='text/javascript' src="{% i18n_static 'barcode.js' %}"></script>
<script defer type='text/javascript' src="{% i18n_static 'bom.js' %}"></script>
<script defer type='text/javascript' src="{% i18n_static 'build.js' %}"></script>
<script defer type='text/javascript' src="{% i18n_static 'company.js' %}"></script>
<script defer type='text/javascript' src="{% i18n_static 'filters.js' %}"></script>
<script defer type='text/javascript' src="{% i18n_static 'forms.js' %}"></script>
<script defer type='text/javascript' src="{% i18n_static 'helpers.js' %}"></script>
<script defer type='text/javascript' src="{% i18n_static 'label.js' %}"></script>
<script defer type='text/javascript' src="{% i18n_static 'modals.js' %}"></script>
<script defer type='text/javascript' src="{% i18n_static 'model_renderers.js' %}"></script>
<script defer type='text/javascript' src="{% i18n_static 'order.js' %}"></script>
<script defer type='text/javascript' src="{% i18n_static 'part.js' %}"></script>
<script defer type='text/javascript' src="{% i18n_static 'report.js' %}"></script>
<script defer type='text/javascript' src="{% i18n_static 'search.js' %}"></script>
<script defer type='text/javascript' src="{% i18n_static 'stock.js' %}"></script>
<script defer type='text/javascript' src="{% i18n_static 'plugin.js' %}"></script>
<script defer type='text/javascript' src="{% i18n_static 'tables.js' %}"></script>
<script defer type='text/javascript' src="{% i18n_static 'table_filters.js' %}"></script>
<script defer type='text/javascript' src="{% i18n_static 'notification.js' %}"></script>
{% block js_load %}
{% endblock %}
<script type='text/javascript'>
<script defer type='text/javascript'>
$(document).ready(function () {

View File

@ -2540,7 +2540,7 @@ function loadBuildTable(table, options) {
if (value) {
return row.responsible_detail.name;
} else {
return '{% trans "No information" %}';
return '-';
}
}
},

View File

@ -274,6 +274,11 @@ function setupNotesField(element, url, options={}) {
initialValue: initial,
toolbar: toolbar_icons,
shortcuts: [],
renderingConfig: {
markedOptions: {
sanitize: true,
}
}
});

View File

@ -32,7 +32,7 @@
<div class='card'>
{% block details_left %}
<div class='row'>
<div class='col' style='max-width: 220px;'>
<div class='col' style='max-width: 280px;'>
{% block thumbnail %}
{% endblock thumbnail %}
</div>

View File

@ -1,5 +1,6 @@
{% load static %}
{% load i18n %}
{% load inventree_extras %}
<!DOCTYPE html>
<html lang="en">
@ -65,15 +66,7 @@
{% block body_scripts_general %}
{% endblock %}
<!-- 3rd party general js -->
<script type="text/javascript" src="{% static 'fullcalendar/main.js' %}"></script>
<script type="text/javascript" src="{% static 'fullcalendar/locales-all.js' %}"></script>
<script type="text/javascript" src="{% static 'select2/js/select2.full.js' %}"></script>
<script type='text/javascript' src="{% static 'script/moment.js' %}"></script>
<script type='text/javascript' src="{% static 'script/chart.min.js' %}"></script>
<script type='text/javascript' src="{% static 'script/clipboard.min.js' %}"></script>
<script type='text/javascript' src="{% static 'script/randomColor.min.js' %}"></script>
{% include "third_party_js.html" %}
<!-- general JS -->
<script type='text/javascript' src="{% static 'script/inventree/inventree.js' %}"></script>

View File

@ -74,7 +74,7 @@
<td><span class='fas fa-envelope'></span></td>
<td>{% trans "Email Settings" %}</td>
<td>
<a href='{% inventree_docs_url %}/admin/email'>
<a href='{% inventree_docs_url %}/settings/email'>
<span class='badge rounded-pill bg-warning'>{% trans "Email settings not configured" %}</span>
</a>
</td>

View File

@ -0,0 +1,37 @@
{% load static %}
<!-- jquery -->
<script type="text/javascript" src="{% static 'script/jquery_3.3.1_jquery.min.js' %}"></script>
<script type='text/javascript' src="{% static 'script/jquery.form.min.js' %}"></script>
<script type='text/javascript' src="{% static 'script/jquery-ui/jquery-ui.min.js' %}"></script>
<!-- Bootstrap-->
<script type="text/javascript" src="{% static 'bootstrap/js/bootstrap.bundle.min.js' %}"></script>
<!-- Bootstrap Table -->
<script defer type='text/javascript' src="{% static 'script/bootstrap/bootstrap-treeview.js' %}"></script>
<script defer type='text/javascript' src='{% static "treegrid/js/jquery.treegrid.js" %}'></script>
<script defer type='text/javascript' src='{% static "treegrid/js/jquery.treegrid.bootstrap3.js" %}'></script>
<script defer type='text/javascript' src="{% static 'bootstrap-table/bootstrap-table.min.js' %}"></script>
<script defer type='text/javascript' src='{% static "bootstrap-table/extensions/group-by-v2/bootstrap-table-group-by.min.js" %}'></script>
<script defer type='text/javascript' src='{% static "bootstrap-table/extensions/filter-control/bootstrap-table-filter-control.min.js" %}'></script>
<script defer type='text/javascript' src='{% static "bootstrap-table/extensions/treegrid/bootstrap-table-treegrid.min.js" %}'></script>
<script defer type='text/javascript' src='{% static "bootstrap-table/extensions/custom-view/bootstrap-table-custom-view.min.js" %}'></script>
<!-- fontawesome -->
<script defer type='text/javascript' src="{% static 'fontawesome/js/solid.min.js' %}"></script>
<script defer type='text/javascript' src="{% static 'fontawesome/js/regular.min.js' %}"></script>
<script defer type='text/javascript' src="{% static 'fontawesome/js/brands.min.js' %}"></script>
<script defer type='text/javascript' src="{% static 'fontawesome/js/fontawesome.min.js' %}"></script>
<!-- 3rd party general js -->
<script defer type="text/javascript" src="{% static 'fullcalendar/main.min.js' %}"></script>
<script defer type="text/javascript" src="{% static 'fullcalendar/locales-all.min.js' %}"></script>
<script defer type="text/javascript" src="{% static 'select2/js/select2.full.min.js' %}"></script>
<script defer type='text/javascript' src="{% static 'script/moment.js' %}"></script>
<script defer type='text/javascript' src="{% static 'script/chart.js' %}"></script>
<script defer type='text/javascript' src="{% static 'script/chartjs-adapter-moment.js' %}"></script>
<script defer type='text/javascript' src="{% static 'script/clipboard.min.js' %}"></script>
<script defer type='text/javascript' src="{% static 'easymde/easymde.min.js' %}"></script>
<script defer type='text/javascript' src="{% static 'script/randomColor.min.js' %}"></script>
<script defer type='text/javascript' src="{% static 'script/qr-scanner.umd.min.js' %}"></script>

Some files were not shown because too many files have changed in this diff Show More