Part page loading improvements (#3185)

* Lazy load the pricing bom table when the "pricing" tab is selected

* Update django-debug-toolbar configuration

* Major refactoring for the 'can_build' function

- Use a single annotated query to the db, rather than a for loop (which is what a caveman would use)
- Query performance is greatly improved
- Also refactors existing variant-part-stock subquery code, to make it re-usable

* Use minified JS and CSS where possible

* Render a 'preview' version of each part image

- Saves load time when the image is quite large
- Adds a data migration to render out the new variation

* Adds 'preview' version of company images

* Defer loading of javascript files

Note: some cannot be deferred - jquery in particular

* Crucial bugfix for user roles context

- Previously was *not* being calculated correctly
- A non-superuser role would most likely display pages incorrectly

* Prevent loading of "about" on every page

- Load dynamically when requested
- Takes ~400ms!
- Cuts out a lot of fat

* Match displayed image size to preview image size

* Utilize caching framework for accessing user "role" information

- Reduces number of DB queries required by rendering framework

* Remove redundant query elements

* Remove 'stock' field from PartBrief serializer

- A calculated field on a serializer is a *bad idea* when that calculation requires a DB hit

* Query improvements for StockItem serializer

- Remove calculated fields
- Fix annotations

* Bug fixes

* Remove JS load test

- Loading of JS files is now deferred, so the unit test does not work as it used to

* Fix broken template for "maintenance" page

* Remove thumbnail generation migrations

- Already performed manually as part of ''invoke migrate"
- Running as a migration causes unit test problems
- Not sensible to run this as a data-migration anyway

* tweak for build table
This commit is contained in:
Oliver 2022-06-17 21:26:28 +10:00 committed by GitHub
parent 0d01ea2f2e
commit 74bec86675
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
51 changed files with 3592 additions and 2212 deletions

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

@ -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,14 +1139,14 @@ 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;
right: 0;
bottom: 0;
left: 0;
}
}
.fc-media-screen .fc-timegrid-cols {
position: absolute; /* no z-index. children will decide and go above slots */
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 */
top: 0;
bottom: 1px; /* stay away from bottom slot line */
left: 0;
.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;
}
.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

@ -750,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

@ -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

@ -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

@ -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,152 +874,164 @@
);
});
onPanelLoad('pricing', function() {
{% default_currency as currency %}
{% 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 = {
labels: [
{% for line in price_history %}'{% render_date line.date %}',{% endfor %}
],
datasets: [{
label: '{% blocktrans %}Purchase Unit Price - {{currency}}{% endblocktrans %}',
backgroundColor: 'rgba(255, 99, 132, 0.2)',
borderColor: 'rgb(255, 99, 132)',
yAxisID: 'y',
data: [
{% for line in price_history %}{{ line.price|stringformat:".2f" }},{% endfor %}
],
borderWidth: 1,
type: 'line'
},
{% if 'price_diff' in price_history.0 %}
{
label: '{% blocktrans %}Unit Price-Cost Difference - {{currency}}{% endblocktrans %}',
backgroundColor: 'rgba(68, 157, 68, 0.2)',
borderColor: 'rgb(68, 157, 68)',
yAxisID: 'y2',
data: [
{% for line in price_history %}{{ line.price_diff|stringformat:".2f" }},{% endfor %}
],
borderWidth: 1,
type: 'line',
hidden: true,
},
{
label: '{% blocktrans %}Supplier Unit Cost - {{currency}}{% endblocktrans %}',
backgroundColor: 'rgba(70, 127, 155, 0.2)',
borderColor: 'rgb(70, 127, 155)',
yAxisID: 'y',
data: [
{% for line in price_history %}{{ line.price_part|stringformat:".2f" }},{% endfor %}
],
borderWidth: 1,
type: 'line',
hidden: true,
},
{% endif %}
{
label: '{% trans "Quantity" %}',
backgroundColor: 'rgba(255, 206, 86, 0.2)',
borderColor: 'rgb(255, 206, 86)',
yAxisID: 'y1',
data: [
{% for line in price_history %}{{ line.qty|stringformat:"f" }},{% endfor %}
],
borderWidth: 1
}]
}
var StockPriceChart = loadStockPricingChart($('#StockPriceChart'), purchasepricedata)
{% endif %}
{% if bom_parts %}
var bom_colors = randomColor({hue: 'green', count: {{ bom_parts|length }} })
var bomdata = {
labels: [{% for line in bom_parts %}'{{ line.name }}',{% endfor %}],
datasets: [
{
label: 'Price',
data: [{% for line in bom_parts %}{{ line.min_price }},{% endfor %}],
backgroundColor: bom_colors,
},
{% if bom_pie_max %}
{
label: 'Max Price',
data: [{% for line in bom_parts %}{{ line.max_price }},{% endfor %}],
backgroundColor: bom_colors,
},
{% endif %}
]
};
var BomChart = loadBomChart(document.getElementById('BomChart'), bomdata)
{% endif %}
// Internal pricebreaks
{% settings_value "PART_INTERNAL_PRICE" as show_internal_price %}
{% if show_internal_price and roles.sales_order.view %}
initPriceBreakSet(
$('#internal-price-break-table'),
{
part_id: {{part.id}},
pb_human_name: 'internal price break',
pb_url_slug: 'internal-price',
pb_url: '{% url 'api-part-internal-price-list' %}',
pb_new_btn: $('#new-internal-price-break'),
pb_new_url: '{% url 'api-part-internal-price-list' %}',
linkedGraph: $('#InternalPriceBreakChart'),
},
);
{% endif %}
// Sales pricebreaks
{% if part.salable and roles.sales_order.view %}
initPriceBreakSet(
$('#price-break-table'),
{
part_id: {{part.id}},
pb_human_name: 'sale price break',
pb_url_slug: 'sale-price',
pb_url: "{% url 'api-part-sale-price-list' %}",
pb_new_btn: $('#new-price-break'),
pb_new_url: '{% url 'api-part-sale-price-list' %}',
linkedGraph: $('#SalePriceBreakChart'),
},
);
{% endif %}
// Sale price history
{% if sale_history %}
var salepricedata = {
// history graphs
{% if price_history %}
var purchasepricedata = {
labels: [
{% for line in sale_history %}'{% render_date line.date %}',{% endfor %}
{% for line in price_history %}'{% render_date line.date %}',{% endfor %}
],
datasets: [{
label: '{% blocktrans %}Unit Price - {{currency}}{% endblocktrans %}',
label: '{% blocktrans %}Purchase Unit Price - {{currency}}{% endblocktrans %}',
backgroundColor: 'rgba(255, 99, 132, 0.2)',
borderColor: 'rgb(255, 99, 132)',
yAxisID: 'y',
data: [
{% for line in sale_history %}{{ line.price|stringformat:".2f" }},{% endfor %}
{% for line in price_history %}{{ line.price|stringformat:".2f" }},{% endfor %}
],
borderWidth: 1,
type: 'line'
},
{% if 'price_diff' in price_history.0 %}
{
label: '{% blocktrans %}Unit Price-Cost Difference - {{currency}}{% endblocktrans %}',
backgroundColor: 'rgba(68, 157, 68, 0.2)',
borderColor: 'rgb(68, 157, 68)',
yAxisID: 'y2',
data: [
{% for line in price_history %}{{ line.price_diff|stringformat:".2f" }},{% endfor %}
],
borderWidth: 1,
type: 'line',
hidden: true,
},
{
label: '{% blocktrans %}Supplier Unit Cost - {{currency}}{% endblocktrans %}',
backgroundColor: 'rgba(70, 127, 155, 0.2)',
borderColor: 'rgb(70, 127, 155)',
yAxisID: 'y',
data: [
{% for line in price_history %}{{ line.price_part|stringformat:".2f" }},{% endfor %}
],
borderWidth: 1,
type: 'line',
hidden: true,
},
{% endif %}
{
label: '{% trans "Quantity" %}',
backgroundColor: 'rgba(255, 206, 86, 0.2)',
borderColor: 'rgb(255, 206, 86)',
yAxisID: 'y1',
data: [
{% for line in sale_history %}{{ line.qty|stringformat:"f" }},{% endfor %}
{% for line in price_history %}{{ line.qty|stringformat:"f" }},{% endfor %}
],
borderWidth: 1,
type: 'bar',
borderWidth: 1
}]
}
var SalePriceChart = loadSellPricingChart($('#SalePriceChart'), salepricedata)
{% endif %}
var StockPriceChart = loadStockPricingChart($('#StockPriceChart'), purchasepricedata)
{% endif %}
{% if bom_parts %}
var bom_colors = randomColor({hue: 'green', count: {{ bom_parts|length }} })
var bomdata = {
labels: [{% for line in bom_parts %}'{{ line.name }}',{% endfor %}],
datasets: [
{
label: 'Price',
data: [{% for line in bom_parts %}{{ line.min_price }},{% endfor %}],
backgroundColor: bom_colors,
},
{% if bom_pie_max %}
{
label: 'Max Price',
data: [{% for line in bom_parts %}{{ line.max_price }},{% endfor %}],
backgroundColor: bom_colors,
},
{% endif %}
]
};
var BomChart = loadBomChart(document.getElementById('BomChart'), bomdata)
{% endif %}
// Internal pricebreaks
{% settings_value "PART_INTERNAL_PRICE" as show_internal_price %}
{% if show_internal_price and roles.sales_order.view %}
initPriceBreakSet(
$('#internal-price-break-table'),
{
part_id: {{part.id}},
pb_human_name: 'internal price break',
pb_url_slug: 'internal-price',
pb_url: '{% url 'api-part-internal-price-list' %}',
pb_new_btn: $('#new-internal-price-break'),
pb_new_url: '{% url 'api-part-internal-price-list' %}',
linkedGraph: $('#InternalPriceBreakChart'),
},
);
{% endif %}
// Sales pricebreaks
{% if part.salable and roles.sales_order.view %}
initPriceBreakSet(
$('#price-break-table'),
{
part_id: {{part.id}},
pb_human_name: 'sale price break',
pb_url_slug: 'sale-price',
pb_url: "{% url 'api-part-sale-price-list' %}",
pb_new_btn: $('#new-price-break'),
pb_new_url: '{% url 'api-part-sale-price-list' %}',
linkedGraph: $('#SalePriceBreakChart'),
},
);
{% endif %}
// Sale price history
{% if sale_history %}
var salepricedata = {
labels: [
{% for line in sale_history %}'{% render_date line.date %}',{% endfor %}
],
datasets: [{
label: '{% blocktrans %}Unit Price - {{currency}}{% endblocktrans %}',
backgroundColor: 'rgba(255, 99, 132, 0.2)',
borderColor: 'rgb(255, 99, 132)',
yAxisID: 'y',
data: [
{% for line in sale_history %}{{ line.price|stringformat:".2f" }},{% endfor %}
],
borderWidth: 1,
},
{
label: '{% trans "Quantity" %}',
backgroundColor: 'rgba(255, 206, 86, 0.2)',
borderColor: 'rgb(255, 206, 86)',
yAxisID: 'y1',
data: [
{% for line in sale_history %}{{ line.qty|stringformat:"f" }},{% endfor %}
],
borderWidth: 1,
type: 'bar',
}]
}
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

@ -74,7 +74,6 @@ class StockDetail(RetrieveUpdateDestroyAPI):
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)

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

@ -2,108 +2,87 @@
{% 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'>
<col width='25'>
<tr>
<td><span class='fas fa-hashtag'></span></td>
<td>{% trans "InvenTree Version" %}</td>
<td>
<a href="https://github.com/inventree/InvenTree/releases">{% inventree_version %}</a>{% include "clip.html" %}
{% inventree_is_development as dev %}
{% if dev %}
<span class='badge badge-right rounded-pill bg-primary'>{% trans "Development Version" %}</span>
{% else %}
{% if up_to_date %}
<span class='badge badge-right rounded-pill bg-success'>{% trans "Up to Date" %}</span>
{% else %}
<span class='badge badge-right rounded-pill bg-info'>{% trans "Update Available" %}</span>
{% endif %}
{% endif %}
</td>
</tr>
{% if dev %}
{% inventree_commit_hash as hash %}
{% if hash %}
<tr>
<td><span class='fas fa-code-branch'></span></td>
<td>{% trans "Commit Hash" %}</td><td>{{ hash }}{% include "clip.html" %}</td>
</tr>
{% endif %}
{% inventree_commit_date as commit_date %}
{% if commit_date %}
<tr>
<td><span class='fas fa-calendar-alt'></span></td>
<td>{% trans "Commit Date" %}</td><td>{% render_date commit_date %}{% include "clip.html" %}</td>
</tr>
{% endif %}
{% endif %}
<tr>
<td><span class='fas fa-book'></span></td>
<td>{% trans "InvenTree Documentation" %}</td>
<td><a href="{% inventree_docs_url %}">{% inventree_docs_url %}</a></td>
</tr>
<tr>
<td><span class='fas fa-code'></span></td>
<td>{% trans "API Version" %}</td>
<td>{% inventree_api_version %}{% include "clip.html" %}</td>
</tr>
<tr>
<td><span class='fas fa-hashtag'></span></td>
<td>{% trans "Python Version" %}</td>
<td>{% python_version %}</td>
</tr>
<tr>
<td><span class='fas fa-hashtag'></span></td>
<td>{% trans "Django Version" %}</td>
<td><a href="https://www.djangoproject.com/">{% django_version %}</a>{% include "clip.html" %}</td>
</tr>
<tr>
<td><span class='fab fa-github'></span></td>
<td>{% trans "View Code on GitHub" %}</td>
<td><a href="{% inventree_github_url %}">{% inventree_github_url %}</a></td>
</tr>
<tr>
<td><span class='fas fa-balance-scale'></span></td>
<td>{% trans "Credits" %}</td>
<td><a href="{% inventree_credits_url %}">{% inventree_credits_url %}</a></td>
</tr>
<tr>
<td><span class='fas fa-mobile-alt'></span></td>
<td>{% trans "Mobile App" %}</td>
<td><a href="{% inventree_docs_url %}/app/app">{% inventree_docs_url %}/app/app</a></td>
</tr>
<tr>
<td><span class='fas fa-bug'></span></td>
<td>{% trans "Submit Bug Report" %}</td>
<td><a href='{% inventree_github_url %}/issues'>{% inventree_github_url %}issues</a></td>
</tr>
<tr><td></td><td></td>
<td>
<span style="display: none;" id="about-copy-text">{% include "version.html" %}</span>
<span class="float-right">
<button class="btn clip-btn-version" type="button" data-bs-toggle='tooltip' title='{% trans "copy to clipboard" %}'><em class="fas fa-copy"></em> {% trans "copy version information" %}</button>
</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 class='table table-striped table-condensed'>
<col width='25'>
<tr>
<td><span class='fas fa-hashtag'></span></td>
<td>{% trans "InvenTree Version" %}</td>
<td>
<a href="https://github.com/inventree/InvenTree/releases">{% inventree_version %}</a>{% include "clip.html" %}
{% inventree_is_development as dev %}
{% if dev %}
<span class='badge badge-right rounded-pill bg-primary'>{% trans "Development Version" %}</span>
{% else %}
{% if up_to_date %}
<span class='badge badge-right rounded-pill bg-success'>{% trans "Up to Date" %}</span>
{% else %}
<span class='badge badge-right rounded-pill bg-info'>{% trans "Update Available" %}</span>
{% endif %}
{% endif %}
</td>
</tr>
{% if dev %}
{% inventree_commit_hash as hash %}
{% if hash %}
<tr>
<td><span class='fas fa-code-branch'></span></td>
<td>{% trans "Commit Hash" %}</td><td>{{ hash }}{% include "clip.html" %}</td>
</tr>
{% endif %}
{% inventree_commit_date as commit_date %}
{% if commit_date %}
<tr>
<td><span class='fas fa-calendar-alt'></span></td>
<td>{% trans "Commit Date" %}</td><td>{% render_date commit_date %}{% include "clip.html" %}</td>
</tr>
{% endif %}
{% endif %}
<tr>
<td><span class='fas fa-book'></span></td>
<td>{% trans "InvenTree Documentation" %}</td>
<td><a href="{% inventree_docs_url %}">{% inventree_docs_url %}</a></td>
</tr>
<tr>
<td><span class='fas fa-code'></span></td>
<td>{% trans "API Version" %}</td>
<td>{% inventree_api_version %}{% include "clip.html" %}</td>
</tr>
<tr>
<td><span class='fas fa-hashtag'></span></td>
<td>{% trans "Python Version" %}</td>
<td>{% python_version %}</td>
</tr>
<tr>
<td><span class='fas fa-hashtag'></span></td>
<td>{% trans "Django Version" %}</td>
<td><a href="https://www.djangoproject.com/">{% django_version %}</a>{% include "clip.html" %}</td>
</tr>
<tr>
<td><span class='fab fa-github'></span></td>
<td>{% trans "View Code on GitHub" %}</td>
<td><a href="{% inventree_github_url %}">{% inventree_github_url %}</a></td>
</tr>
<tr>
<td><span class='fas fa-balance-scale'></span></td>
<td>{% trans "Credits" %}</td>
<td><a href="{% inventree_credits_url %}">{% inventree_credits_url %}</a></td>
</tr>
<tr>
<td><span class='fas fa-mobile-alt'></span></td>
<td>{% trans "Mobile App" %}</td>
<td><a href="{% inventree_docs_url %}/app/app">{% inventree_docs_url %}/app/app</a></td>
</tr>
<tr>
<td><span class='fas fa-bug'></span></td>
<td>{% trans "Submit Bug Report" %}</td>
<td><a href='{% inventree_github_url %}/issues'>{% inventree_github_url %}issues</a></td>
</tr>
<tr><td></td><td></td>
<td>
<span style="display: none;" id="about-copy-text">{% include "version.html" %}</span>
<span class="float-right">
<button class="btn clip-btn-version" type="button" data-bs-toggle='tooltip' title='{% trans "copy to clipboard" %}'><em class="fas fa-copy"></em> {% trans "copy version information" %}</button>
</span>
</td>
</tr>
</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,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

@ -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

@ -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>

View File

@ -6,6 +6,7 @@ from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group, Permission
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.core.cache import cache
from django.db import models
from django.db.models import Q, UniqueConstraint
from django.db.models.signals import post_delete, post_save
@ -474,13 +475,19 @@ def update_group_roles(group, debug=False):
logger.info(f"Adding permission {child_perm} to group {group.name}")
@receiver(post_save, sender=Group, dispatch_uid='create_missing_rule_sets')
def create_missing_rule_sets(sender, instance, **kwargs):
"""Called *after* a Group object is saved.
def clear_user_role_cache(user):
"""Remove user role permission information from the cache.
As the linked RuleSet instances are saved *before* the Group, then we can now use these RuleSet values to update the group permissions.
- This function is called whenever the user / group is updated
Args:
user: The User object to be expunged from the cache
"""
update_group_roles(instance)
for role in RuleSet.RULESET_MODELS.keys():
for perm in ['add', 'change', 'view', 'delete']:
key = f"role_{user}_{role}_{perm}"
cache.delete(key)
def check_user_role(user, role, permission):
@ -491,6 +498,17 @@ def check_user_role(user, role, permission):
if user.is_superuser:
return True
# First, check the cache
key = f"role_{user}_{role}_{permission}"
result = cache.get(key)
if result is not None:
return result
# Default for no match
result = False
for group in user.groups.all():
for rule in group.rule_sets.all():
@ -498,19 +516,24 @@ def check_user_role(user, role, permission):
if rule.name == role:
if permission == 'add' and rule.can_add:
return True
result = True
break
if permission == 'change' and rule.can_change:
return True
result = True
break
if permission == 'view' and rule.can_view:
return True
result = True
break
if permission == 'delete' and rule.can_delete:
return True
result = True
break
# No matching permissions found
return False
# Save result to cache
cache.set(key, result, timeout=3600)
return result
class Owner(models.Model):
@ -659,3 +682,22 @@ def delete_owner(sender, instance, **kwargs):
"""Callback function to delete an owner instance after either a new group or user instance is deleted."""
owner = Owner.get_owner(instance)
owner.delete()
@receiver(post_save, sender=get_user_model(), dispatch_uid='clear_user_cache')
def clear_user_cache(sender, instance, **kwargs):
"""Callback function when a user object is saved"""
clear_user_role_cache(instance)
@receiver(post_save, sender=Group, dispatch_uid='create_missing_rule_sets')
def create_missing_rule_sets(sender, instance, **kwargs):
"""Called *after* a Group object is saved.
As the linked RuleSet instances are saved *before* the Group, then we can now use these RuleSet values to update the group permissions.
"""
update_group_roles(instance)
for user in get_user_model().objects.filter(groups__name=instance.name):
clear_user_role_cache(user)