2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-05-11 17:53:00 +00:00

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

View File

@ -309,6 +309,11 @@ if DEBUG_TOOLBAR_ENABLED: # pragma: no cover
INSTALLED_APPS.append('debug_toolbar') INSTALLED_APPS.append('debug_toolbar')
MIDDLEWARE.append('debug_toolbar.middleware.DebugToolbarMiddleware') 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 IP addresses allowed to see the debug toolbar
INTERNAL_IPS = [ INTERNAL_IPS = [
'127.0.0.1', '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 { .part-thumb {
width: 200px; width: 256px;
height: 200px; height: 256px;
margin: 2px; margin: 2px;
padding: 3px; padding: 3px;
object-fit: contain; object-fit: contain;

View File

@ -222,6 +222,29 @@
}; };
var l10 = { 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', code: 'bs',
week: { week: {
dow: 1, // Monday is the first day of the week. dow: 1, // Monday is the first day of the week.
@ -244,7 +267,7 @@
noEventsText: 'Nema događaja za prikazivanje', noEventsText: 'Nema događaja za prikazivanje',
}; };
var l11 = { var l12 = {
code: 'ca', code: 'ca',
week: { week: {
dow: 1, // Monday is the first day of the week. dow: 1, // Monday is the first day of the week.
@ -265,7 +288,7 @@
noEventsText: 'No hi ha esdeveniments per mostrar', noEventsText: 'No hi ha esdeveniments per mostrar',
}; };
var l12 = { var l13 = {
code: 'cs', code: 'cs',
week: { week: {
dow: 1, // Monday is the first day of the week. dow: 1, // Monday is the first day of the week.
@ -288,7 +311,7 @@
noEventsText: 'Žádné akce k zobrazení', noEventsText: 'Žádné akce k zobrazení',
}; };
var l13 = { var l14 = {
code: 'cy', code: 'cy',
week: { week: {
dow: 1, // Monday is the first day of the week. dow: 1, // Monday is the first day of the week.
@ -310,7 +333,7 @@
noEventsText: 'Dim digwyddiadau', noEventsText: 'Dim digwyddiadau',
}; };
var l14 = { var l15 = {
code: 'da', code: 'da',
week: { week: {
dow: 1, // Monday is the first day of the week. dow: 1, // Monday is the first day of the week.
@ -331,7 +354,12 @@
noEventsText: 'Ingen arrangementer at vise', 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', code: 'de-at',
week: { week: {
dow: 1, // Monday is the first day of the week. dow: 1, // Monday is the first day of the week.
@ -348,14 +376,49 @@
list: 'Terminübersicht', list: 'Terminübersicht',
}, },
weekText: 'KW', weekText: 'KW',
weekTextLong: 'Woche',
allDayText: 'Ganztägig', allDayText: 'Ganztägig',
moreLinkText: function(n) { moreLinkText: function(n) {
return '+ weitere ' + n return '+ weitere ' + n
}, },
noEventsText: 'Keine Ereignisse anzuzeigen', 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', code: 'de',
week: { week: {
dow: 1, // Monday is the first day of the week. dow: 1, // Monday is the first day of the week.
@ -372,14 +435,44 @@
list: 'Terminübersicht', list: 'Terminübersicht',
}, },
weekText: 'KW', weekText: 'KW',
weekTextLong: 'Woche',
allDayText: 'Ganztägig', allDayText: 'Ganztägig',
moreLinkText: function(n) { moreLinkText: function(n) {
return '+ weitere ' + n return '+ weitere ' + n
}, },
noEventsText: 'Keine Ereignisse anzuzeigen', 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', code: 'el',
week: { week: {
dow: 1, // Monday is the first day of the week. dow: 1, // Monday is the first day of the week.
@ -400,31 +493,61 @@
noEventsText: 'Δεν υπάρχουν γεγονότα προς εμφάνιση', noEventsText: 'Δεν υπάρχουν γεγονότα προς εμφάνιση',
}; };
var l18 = { var l19 = {
code: 'en-au', code: 'en-au',
week: { week: {
dow: 1, // Monday is the first day of the 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. 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', code: 'en-gb',
week: { week: {
dow: 1, // Monday is the first day of the 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. 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', code: 'en-nz',
week: { week: {
dow: 1, // Monday is the first day of the 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. 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', code: 'eo',
week: { week: {
dow: 1, // Monday is the first day of the week. dow: 1, // Monday is the first day of the week.
@ -445,7 +568,7 @@
noEventsText: 'Neniuj eventoj por montri', noEventsText: 'Neniuj eventoj por montri',
}; };
var l22 = { var l23 = {
code: 'es', code: 'es',
week: { week: {
dow: 0, // Sunday is the first day of the week. dow: 0, // Sunday is the first day of the week.
@ -466,7 +589,7 @@
noEventsText: 'No hay eventos para mostrar', noEventsText: 'No hay eventos para mostrar',
}; };
var l23 = { var l24 = {
code: 'es', code: 'es',
week: { week: {
dow: 1, // Monday is the first day of the week. dow: 1, // Monday is the first day of the week.
@ -481,13 +604,32 @@
day: 'Día', day: 'Día',
list: 'Agenda', 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', weekText: 'Sm',
weekTextLong: 'Semana',
allDayText: 'Todo el día', allDayText: 'Todo el día',
moreLinkText: 'más', moreLinkText: 'más',
moreLinkHint(eventCnt) {
return `Mostrar ${eventCnt} eventos más`
},
noEventsText: 'No hay eventos para mostrar', noEventsText: 'No hay eventos para mostrar',
navLinkHint: 'Ir al $0',
closeHint: 'Cerrar',
timeHint: 'La hora',
eventHint: 'Evento',
}; };
var l24 = { var l25 = {
code: 'et', code: 'et',
week: { week: {
dow: 1, // Monday is the first day of the week. dow: 1, // Monday is the first day of the week.
@ -510,7 +652,7 @@
noEventsText: 'Kuvamiseks puuduvad sündmused', noEventsText: 'Kuvamiseks puuduvad sündmused',
}; };
var l25 = { var l26 = {
code: 'eu', code: 'eu',
week: { week: {
dow: 1, // Monday is the first day of the week. dow: 1, // Monday is the first day of the week.
@ -531,7 +673,7 @@
noEventsText: 'Ez dago ekitaldirik erakusteko', noEventsText: 'Ez dago ekitaldirik erakusteko',
}; };
var l26 = { var l27 = {
code: 'fa', code: 'fa',
week: { week: {
dow: 6, // Saturday is the first day of the week. dow: 6, // Saturday is the first day of the week.
@ -555,7 +697,7 @@
noEventsText: 'هیچ رویدادی به نمایش', noEventsText: 'هیچ رویدادی به نمایش',
}; };
var l27 = { var l28 = {
code: 'fi', code: 'fi',
week: { week: {
dow: 1, // Monday is the first day of the week. dow: 1, // Monday is the first day of the week.
@ -576,7 +718,7 @@
noEventsText: 'Ei näytettäviä tapahtumia', noEventsText: 'Ei näytettäviä tapahtumia',
}; };
var l28 = { var l29 = {
code: 'fr', code: 'fr',
buttonText: { buttonText: {
prev: 'Précédent', prev: 'Précédent',
@ -594,7 +736,7 @@
noEventsText: 'Aucun événement à afficher', noEventsText: 'Aucun événement à afficher',
}; };
var l29 = { var l30 = {
code: 'fr-ch', code: 'fr-ch',
week: { week: {
dow: 1, // Monday is the first day of the week. dow: 1, // Monday is the first day of the week.
@ -616,7 +758,7 @@
noEventsText: 'Aucun événement à afficher', noEventsText: 'Aucun événement à afficher',
}; };
var l30 = { var l31 = {
code: 'fr', code: 'fr',
week: { week: {
dow: 1, // Monday is the first day of the week. dow: 1, // Monday is the first day of the week.
@ -638,7 +780,7 @@
noEventsText: 'Aucun événement à afficher', noEventsText: 'Aucun événement à afficher',
}; };
var l31 = { var l32 = {
code: 'gl', code: 'gl',
week: { week: {
dow: 1, // Monday is the first day of the week. dow: 1, // Monday is the first day of the week.
@ -659,7 +801,7 @@
noEventsText: 'Non hai eventos para amosar', noEventsText: 'Non hai eventos para amosar',
}; };
var l32 = { var l33 = {
code: 'he', code: 'he',
direction: 'rtl', direction: 'rtl',
buttonText: { buttonText: {
@ -677,7 +819,7 @@
weekText: 'שבוע', weekText: 'שבוע',
}; };
var l33 = { var l34 = {
code: 'hi', code: 'hi',
week: { week: {
dow: 0, // Sunday is the first day of the week. dow: 0, // Sunday is the first day of the week.
@ -700,7 +842,7 @@
noEventsText: 'कोई घटनाओं को प्रदर्शित करने के लिए', noEventsText: 'कोई घटनाओं को प्रदर्शित करने के लिए',
}; };
var l34 = { var l35 = {
code: 'hr', code: 'hr',
week: { week: {
dow: 1, // Monday is the first day of the week. dow: 1, // Monday is the first day of the week.
@ -723,7 +865,7 @@
noEventsText: 'Nema događaja za prikaz', noEventsText: 'Nema događaja za prikaz',
}; };
var l35 = { var l36 = {
code: 'hu', code: 'hu',
week: { week: {
dow: 1, // Monday is the first day of the week. dow: 1, // Monday is the first day of the week.
@ -736,7 +878,7 @@
month: 'Hónap', month: 'Hónap',
week: 'Hét', week: 'Hét',
day: 'Nap', day: 'Nap',
list: 'Napló', list: 'Lista',
}, },
weekText: 'Hét', weekText: 'Hét',
allDayText: 'Egész nap', allDayText: 'Egész nap',
@ -744,7 +886,7 @@
noEventsText: 'Nincs megjeleníthető esemény', noEventsText: 'Nincs megjeleníthető esemény',
}; };
var l36 = { var l37 = {
code: 'hy-am', code: 'hy-am',
week: { week: {
dow: 1, // Monday is the first day of the week. dow: 1, // Monday is the first day of the week.
@ -767,7 +909,7 @@
noEventsText: 'Բացակայում է իրադարձությունը ցուցադրելու', noEventsText: 'Բացակայում է իրադարձությունը ցուցադրելու',
}; };
var l37 = { var l38 = {
code: 'id', code: 'id',
week: { week: {
dow: 1, // Monday is the first day of the week. dow: 1, // Monday is the first day of the week.
@ -788,7 +930,7 @@
noEventsText: 'Tidak ada acara untuk ditampilkan', noEventsText: 'Tidak ada acara untuk ditampilkan',
}; };
var l38 = { var l39 = {
code: 'is', code: 'is',
week: { week: {
dow: 1, // Monday is the first day of the week. dow: 1, // Monday is the first day of the week.
@ -809,7 +951,7 @@
noEventsText: 'Engir viðburðir til að sýna', noEventsText: 'Engir viðburðir til að sýna',
}; };
var l39 = { var l40 = {
code: 'it', code: 'it',
week: { week: {
dow: 1, // Monday is the first day of the week. dow: 1, // Monday is the first day of the week.
@ -832,7 +974,7 @@
noEventsText: 'Non ci sono eventi da visualizzare', noEventsText: 'Non ci sono eventi da visualizzare',
}; };
var l40 = { var l41 = {
code: 'ja', code: 'ja',
buttonText: { buttonText: {
prev: '前', prev: '前',
@ -851,7 +993,7 @@
noEventsText: '表示する予定はありません', noEventsText: '表示する予定はありません',
}; };
var l41 = { var l42 = {
code: 'ka', code: 'ka',
week: { week: {
dow: 1, dow: 1,
@ -874,7 +1016,7 @@
noEventsText: 'ღონისძიებები არ არის', noEventsText: 'ღონისძიებები არ არის',
}; };
var l42 = { var l43 = {
code: 'kk', code: 'kk',
week: { week: {
dow: 1, // Monday is the first day of the week. dow: 1, // Monday is the first day of the week.
@ -897,7 +1039,29 @@
noEventsText: 'Көрсету үшін оқиғалар жоқ', 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', code: 'ko',
buttonText: { buttonText: {
prev: '이전달', prev: '이전달',
@ -914,7 +1078,29 @@
noEventsText: '일정이 없습니다', 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', code: 'lb',
week: { week: {
dow: 1, // Monday is the first day of the week. dow: 1, // Monday is the first day of the week.
@ -935,7 +1121,7 @@
noEventsText: 'Nee Evenementer ze affichéieren', noEventsText: 'Nee Evenementer ze affichéieren',
}; };
var l45 = { var l48 = {
code: 'lt', code: 'lt',
week: { week: {
dow: 1, // Monday is the first day of the week. dow: 1, // Monday is the first day of the week.
@ -956,7 +1142,7 @@
noEventsText: 'Nėra įvykių rodyti', noEventsText: 'Nėra įvykių rodyti',
}; };
var l46 = { var l49 = {
code: 'lv', code: 'lv',
week: { week: {
dow: 1, // Monday is the first day of the week. dow: 1, // Monday is the first day of the week.
@ -979,7 +1165,7 @@
noEventsText: 'Nav notikumu', noEventsText: 'Nav notikumu',
}; };
var l47 = { var l50 = {
code: 'mk', code: 'mk',
buttonText: { buttonText: {
prev: 'претходно', prev: 'претходно',
@ -998,7 +1184,7 @@
noEventsText: 'Нема настани за прикажување', noEventsText: 'Нема настани за прикажување',
}; };
var l48 = { var l51 = {
code: 'ms', code: 'ms',
week: { week: {
dow: 1, // Monday is the first day of the week. dow: 1, // Monday is the first day of the week.
@ -1021,7 +1207,7 @@
noEventsText: 'Tiada peristiwa untuk dipaparkan', noEventsText: 'Tiada peristiwa untuk dipaparkan',
}; };
var l49 = { var l52 = {
code: 'nb', code: 'nb',
week: { week: {
dow: 1, // Monday is the first day of the week. dow: 1, // Monday is the first day of the week.
@ -1037,12 +1223,23 @@
list: 'Agenda', list: 'Agenda',
}, },
weekText: 'Uke', weekText: 'Uke',
weekTextLong: 'Uke',
allDayText: 'Hele dagen', allDayText: 'Hele dagen',
moreLinkText: 'til', moreLinkText: 'til',
noEventsText: 'Ingen hendelser å vise', 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 code: 'ne', // code for nepal
week: { week: {
dow: 7, // Sunday is the first day of the week. dow: 7, // Sunday is the first day of the week.
@ -1063,7 +1260,7 @@
noEventsText: 'देखाउनको लागि कुनै घटनाहरू छैनन्', noEventsText: 'देखाउनको लागि कुनै घटनाहरू छैनन्',
}; };
var l51 = { var l54 = {
code: 'nl', code: 'nl',
week: { week: {
dow: 1, // Monday is the first day of the week. dow: 1, // Monday is the first day of the week.
@ -1084,7 +1281,7 @@
noEventsText: 'Geen evenementen om te laten zien', noEventsText: 'Geen evenementen om te laten zien',
}; };
var l52 = { var l55 = {
code: 'nn', code: 'nn',
week: { week: {
dow: 1, // Monday is the first day of the week. dow: 1, // Monday is the first day of the week.
@ -1105,7 +1302,7 @@
noEventsText: 'Ingen hendelser å vise', noEventsText: 'Ingen hendelser å vise',
}; };
var l53 = { var l56 = {
code: 'pl', code: 'pl',
week: { week: {
dow: 1, // Monday is the first day of the week. dow: 1, // Monday is the first day of the week.
@ -1126,7 +1323,7 @@
noEventsText: 'Brak wydarzeń do wyświetlenia', noEventsText: 'Brak wydarzeń do wyświetlenia',
}; };
var l54 = { var l57 = {
code: 'pt-br', code: 'pt-br',
buttonText: { buttonText: {
prev: 'Anterior', prev: 'Anterior',
@ -1145,7 +1342,7 @@
noEventsText: 'Não há eventos para mostrar', noEventsText: 'Não há eventos para mostrar',
}; };
var l55 = { var l58 = {
code: 'pt', code: 'pt',
week: { week: {
dow: 1, // Monday is the first day of the week. dow: 1, // Monday is the first day of the week.
@ -1166,7 +1363,7 @@
noEventsText: 'Não há eventos para mostrar', noEventsText: 'Não há eventos para mostrar',
}; };
var l56 = { var l59 = {
code: 'ro', code: 'ro',
week: { week: {
dow: 1, // Monday is the first day of the week. dow: 1, // Monday is the first day of the week.
@ -1189,7 +1386,7 @@
noEventsText: 'Nu există evenimente de afișat', noEventsText: 'Nu există evenimente de afișat',
}; };
var l57 = { var l60 = {
code: 'ru', code: 'ru',
week: { week: {
dow: 1, // Monday is the first day of the week. dow: 1, // Monday is the first day of the week.
@ -1212,7 +1409,28 @@
noEventsText: 'Нет событий для отображения', 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', code: 'sk',
week: { week: {
dow: 1, // Monday is the first day of the week. dow: 1, // Monday is the first day of the week.
@ -1235,7 +1453,7 @@
noEventsText: 'Žiadne akcie na zobrazenie', noEventsText: 'Žiadne akcie na zobrazenie',
}; };
var l59 = { var l63 = {
code: 'sl', code: 'sl',
week: { week: {
dow: 1, // Monday is the first day of the week. dow: 1, // Monday is the first day of the week.
@ -1256,7 +1474,24 @@
noEventsText: 'Ni dogodkov za prikaz', 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', code: 'sq',
week: { week: {
dow: 1, // Monday is the first day of the week. dow: 1, // Monday is the first day of the week.
@ -1279,7 +1514,7 @@
noEventsText: 'Nuk ka evente për të shfaqur', noEventsText: 'Nuk ka evente për të shfaqur',
}; };
var l61 = { var l66 = {
code: 'sr-cyrl', code: 'sr-cyrl',
week: { week: {
dow: 1, // Monday is the first day of the week. dow: 1, // Monday is the first day of the week.
@ -1302,7 +1537,7 @@
noEventsText: 'Нема догађаја за приказ', noEventsText: 'Нема догађаја за приказ',
}; };
var l62 = { var l67 = {
code: 'sr', code: 'sr',
week: { week: {
dow: 1, // Monday is the first day of the week. dow: 1, // Monday is the first day of the week.
@ -1325,7 +1560,7 @@
noEventsText: 'Nеma događaja za prikaz', noEventsText: 'Nеma događaja za prikaz',
}; };
var l63 = { var l68 = {
code: 'sv', code: 'sv',
week: { week: {
dow: 1, // Monday is the first day of the week. dow: 1, // Monday is the first day of the week.
@ -1340,13 +1575,56 @@
day: 'Dag', day: 'Dag',
list: 'Program', 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.', weekText: 'v.',
weekTextLong: 'Vecka',
allDayText: 'Heldag', allDayText: 'Heldag',
moreLinkText: 'till', moreLinkText: 'till',
noEventsText: 'Inga händelser att visa', 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', code: 'th',
week: { week: {
dow: 1, // Monday is the first day of the week. dow: 1, // Monday is the first day of the week.
@ -1370,7 +1648,7 @@
noEventsText: 'ไม่มีกิจกรรมที่จะแสดง', noEventsText: 'ไม่มีกิจกรรมที่จะแสดง',
}; };
var l65 = { var l71 = {
code: 'tr', code: 'tr',
week: { week: {
dow: 1, // Monday is the first day of the week. dow: 1, // Monday is the first day of the week.
@ -1391,7 +1669,7 @@
noEventsText: 'Gösterilecek etkinlik yok', noEventsText: 'Gösterilecek etkinlik yok',
}; };
var l66 = { var l72 = {
code: 'ug', code: 'ug',
buttonText: { buttonText: {
month: 'ئاي', month: 'ئاي',
@ -1402,7 +1680,7 @@
allDayText: 'پۈتۈن كۈن', allDayText: 'پۈتۈن كۈن',
}; };
var l67 = { var l73 = {
code: 'uk', code: 'uk',
week: { week: {
dow: 1, // Monday is the first day of the week. dow: 1, // Monday is the first day of the week.
@ -1425,7 +1703,7 @@
noEventsText: 'Немає подій для відображення', noEventsText: 'Немає подій для відображення',
}; };
var l68 = { var l74 = {
code: 'uz', code: 'uz',
buttonText: { buttonText: {
month: 'Oy', month: 'Oy',
@ -1440,7 +1718,7 @@
noEventsText: "Ko'rsatish uchun voqealar yo'q", noEventsText: "Ko'rsatish uchun voqealar yo'q",
}; };
var l69 = { var l75 = {
code: 'vi', code: 'vi',
week: { week: {
dow: 1, // Monday is the first day of the 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ị', noEventsText: 'Không có sự kiện để hiển thị',
}; };
var l70 = { var l76 = {
code: 'zh-cn', code: 'zh-cn',
week: { week: {
// GB/T 7408-1994《数据元和交换格式·信息交换·日期和时间表示法》与ISO 8601:1988等效 // GB/T 7408-1994《数据元和交换格式·信息交换·日期和时间表示法》与ISO 8601:1988等效
@ -1487,7 +1765,7 @@
noEventsText: '没有事件显示', noEventsText: '没有事件显示',
}; };
var l71 = { var l77 = {
code: 'zh-tw', code: 'zh-tw',
buttonText: { buttonText: {
prev: '上月', prev: '上月',
@ -1507,7 +1785,7 @@
/* eslint max-len: off */ /* eslint max-len: off */
var localesAll = [ 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; 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 () { FullCalendar.globalLocales.push(function () {
'use strict'; 'use strict';
function affix(buttonText) {
return (buttonText === 'Tag' || buttonText === 'Monat') ? 'r' :
buttonText === 'Jahr' ? 's' : ''
}
var deAt = { var deAt = {
code: 'de-at', code: 'de-at',
week: { week: {
@ -18,11 +23,41 @@ FullCalendar.globalLocales.push(function () {
list: 'Terminübersicht', list: 'Terminübersicht',
}, },
weekText: 'KW', weekText: 'KW',
weekTextLong: 'Woche',
allDayText: 'Ganztägig', allDayText: 'Ganztägig',
moreLinkText: function(n) { moreLinkText: function(n) {
return '+ weitere ' + n return '+ weitere ' + n
}, },
noEventsText: 'Keine Ereignisse anzuzeigen', 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; return deAt;

View File

@ -1,6 +1,11 @@
FullCalendar.globalLocales.push(function () { FullCalendar.globalLocales.push(function () {
'use strict'; 'use strict';
function affix(buttonText) {
return (buttonText === 'Tag' || buttonText === 'Monat') ? 'r' :
buttonText === 'Jahr' ? 's' : ''
}
var de = { var de = {
code: 'de', code: 'de',
week: { week: {
@ -18,11 +23,41 @@ FullCalendar.globalLocales.push(function () {
list: 'Terminübersicht', list: 'Terminübersicht',
}, },
weekText: 'KW', weekText: 'KW',
weekTextLong: 'Woche',
allDayText: 'Ganztägig', allDayText: 'Ganztägig',
moreLinkText: function(n) { moreLinkText: function(n) {
return '+ weitere ' + n return '+ weitere ' + n
}, },
noEventsText: 'Keine Ereignisse anzuzeigen', 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; return de;

View File

@ -7,6 +7,16 @@ FullCalendar.globalLocales.push(function () {
dow: 1, // Monday is the first day of the 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. 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; return enAu;

View File

@ -7,6 +7,16 @@ FullCalendar.globalLocales.push(function () {
dow: 1, // Monday is the first day of the 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. 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; return enGb;

View File

@ -7,6 +7,16 @@ FullCalendar.globalLocales.push(function () {
dow: 1, // Monday is the first day of the 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. 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; return enNz;

View File

@ -16,10 +16,29 @@ FullCalendar.globalLocales.push(function () {
day: 'Día', day: 'Día',
list: 'Agenda', 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', weekText: 'Sm',
weekTextLong: 'Semana',
allDayText: 'Todo el día', allDayText: 'Todo el día',
moreLinkText: 'más', moreLinkText: 'más',
moreLinkHint(eventCnt) {
return `Mostrar ${eventCnt} eventos más`
},
noEventsText: 'No hay eventos para mostrar', noEventsText: 'No hay eventos para mostrar',
navLinkHint: 'Ir al $0',
closeHint: 'Cerrar',
timeHint: 'La hora',
eventHint: 'Evento',
}; };
return es; return es;

View File

@ -14,7 +14,7 @@ FullCalendar.globalLocales.push(function () {
month: 'Hónap', month: 'Hónap',
week: 'Hét', week: 'Hét',
day: 'Nap', day: 'Nap',
list: 'Napló', list: 'Lista',
}, },
weekText: 'Hét', weekText: 'Hét',
allDayText: 'Egész nap', 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', list: 'Agenda',
}, },
weekText: 'Uke', weekText: 'Uke',
weekTextLong: 'Uke',
allDayText: 'Hele dagen', allDayText: 'Hele dagen',
moreLinkText: 'til', moreLinkText: 'til',
noEventsText: 'Ingen hendelser å vise', 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; 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', day: 'Dag',
list: 'Program', 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.', weekText: 'v.',
weekTextLong: 'Vecka',
allDayText: 'Heldag', allDayText: 'Heldag',
moreLinkText: 'till', moreLinkText: 'till',
noEventsText: 'Inga händelser att visa', noEventsText: 'Inga händelser att visa',
closeHint: 'Stäng',
timeHint: 'Klockan',
eventHint: 'Händelse',
}; };
return sv; 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> */ /* classes attached to <body> */
/* TODO: make fc-event selector work when calender in shadow DOM */
.fc-not-allowed, .fc-not-allowed,
.fc-not-allowed .fc-event { /* override events' custom cursors */ .fc-not-allowed .fc-event { /* override events' custom cursors */
cursor: not-allowed; cursor: not-allowed;
} }
/* TODO: not attached to body. attached to specific els. move */
.fc-unselectable { .fc-unselectable {
-webkit-user-select: none; -webkit-user-select: none;
-moz-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 */ /* 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 */ /* serves as a min-height. harmless */
} }
.fc .fc-scrollgrid-section-liquid {
height: auto
}
.fc .fc-scrollgrid-section-liquid > td { .fc .fc-scrollgrid-section-liquid > td {
height: 100%; /* better than `auto`, for firefox */ 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 > * { .fc .fc-scrollgrid-section-sticky > * {
background: #fff; background: #fff;
background: var(--fc-page-bg-color, #fff); background: var(--fc-page-bg-color, #fff);
position: -webkit-sticky;
position: sticky; position: sticky;
z-index: 2; /* TODO: var */ z-index: 3; /* TODO: var */
/* TODO: box-shadow when sticking */ /* TODO: box-shadow when sticking */
} }
.fc .fc-scrollgrid-section-header.fc-scrollgrid-section-sticky > * { .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; margin-bottom: -1px;
} }
.fc-sticky { /* no .fc wrap because used as child of body */ .fc-sticky { /* no .fc wrap because used as child of body */
position: -webkit-sticky;
position: sticky; position: sticky;
} }
.fc .fc-view-harness { .fc .fc-view-harness {
@ -535,14 +530,17 @@ a.fc-event:hover {
bottom: -20px; bottom: -20px;
} }
/* selecting (always TOUCH) */ /* 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) box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2)
/* expand hit area (subclasses should expand) */ /* expand hit area (subclasses should expand) */
} }
.fc-event-selected:before { .fc-event-selected:before, .fc-event:focus:before {
content: ""; content: "";
position: absolute; position: absolute;
z-index: 3; z-index: 3;
@ -551,12 +549,13 @@ a.fc-event:hover {
right: 0; right: 0;
bottom: 0; bottom: 0;
} }
.fc-event-selected { .fc-event-selected,
.fc-event:focus {
/* dimmer effect */ /* dimmer effect */
} }
.fc-event-selected:after { .fc-event-selected:after, .fc-event:focus:after {
content: ""; content: "";
background: rgba(0, 0, 0, 0.25); background: rgba(0, 0, 0, 0.25);
background: var(--fc-event-selected-overlay-color, 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 { .fc-direction-rtl .fc-h-event:not(.fc-event-selected) .fc-event-resizer-end {
cursor: w-resize; cursor: w-resize;
left: -4px; 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-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 { .fc-direction-rtl .fc-h-event:not(.fc-event-selected) .fc-event-resizer-start {
cursor: e-resize; cursor: e-resize;
right: -4px; right: -4px;
right: calc(var(--fc-event-resizer-thickness, 8px) / -2); right: calc(-0.5 * var(--fc-event-resizer-thickness, 8px));
} }
/* resizers for TOUCH */ /* resizers for TOUCH */
.fc-h-event.fc-event-selected .fc-event-resizer { .fc-h-event.fc-event-selected .fc-event-resizer {
top: 50%; top: 50%;
margin-top: -4px; 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-ltr .fc-h-event.fc-event-selected .fc-event-resizer-start,
.fc-direction-rtl .fc-h-event.fc-event-selected .fc-event-resizer-end { .fc-direction-rtl .fc-h-event.fc-event-selected .fc-event-resizer-end {
left: -4px; 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-ltr .fc-h-event.fc-event-selected .fc-event-resizer-end,
.fc-direction-rtl .fc-h-event.fc-event-selected .fc-event-resizer-start { .fc-direction-rtl .fc-h-event.fc-event-selected .fc-event-resizer-start {
right: -4px; right: -4px;
right: calc(var(--fc-event-resizer-dot-total-width, 8px) / -2); right: calc(-0.5 * var(--fc-event-resizer-dot-total-width, 8px));
}
:root {
--fc-daygrid-event-dot-width: 8px;
} }
.fc .fc-popover { .fc .fc-popover {
position: fixed; position: absolute;
top: 0; /* for when not positioned yet */ z-index: 9999;
box-shadow: 0 2px 6px rgba(0,0,0,.15); box-shadow: 0 2px 6px rgba(0,0,0,.15);
} }
.fc .fc-popover-header { .fc .fc-popover-header {
@ -694,6 +688,11 @@ A HORIZONTAL event
background: rgba(208, 208, 208, 0.3); background: rgba(208, 208, 208, 0.3);
background: var(--fc-neutral-bg-color, 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 */ /* help things clear margins of inner content */
.fc-daygrid-day-frame, .fc-daygrid-day-frame,
.fc-daygrid-day-events, .fc-daygrid-day-events,
@ -814,8 +813,12 @@ A HORIZONTAL event
} }
.fc .fc-daygrid-day-bottom { .fc .fc-daygrid-day-bottom {
font-size: .85em; 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 { .fc .fc-daygrid-more-link {
position: relative; position: relative;
z-index: 4; z-index: 4;
@ -843,9 +846,6 @@ A HORIZONTAL event
/* popover */ /* popover */
} }
.fc .fc-more-popover {
z-index: 8;
}
.fc .fc-more-popover .fc-popover-body { .fc .fc-more-popover .fc-popover-body {
min-width: 220px; min-width: 220px;
padding: 10px; padding: 10px;
@ -1139,14 +1139,14 @@ A VERTICAL event
min-height: 100%; /* liquid-hack is below */ min-height: 100%; /* liquid-hack is below */
position: relative; position: relative;
} }
.fc-liquid-hack .fc-timegrid-col-frame { .fc-media-screen.fc-liquid-hack .fc-timegrid-col-frame {
height: auto; height: auto;
position: absolute; position: absolute;
top: 0; top: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
left: 0; left: 0;
} }
.fc-media-screen .fc-timegrid-cols { .fc-media-screen .fc-timegrid-cols {
position: absolute; /* no z-index. children will decide and go above slots */ position: absolute; /* no z-index. children will decide and go above slots */
top: 0; top: 0;
@ -1165,9 +1165,6 @@ A VERTICAL event
left: 0; left: 0;
right: 0; right: 0;
} }
.fc-media-screen .fc-timegrid-event-harness {
position: absolute; /* top/left/right/bottom will all be set by JS */
}
.fc { .fc {
/* bg */ /* bg */
@ -1211,18 +1208,30 @@ A VERTICAL event
.fc-direction-rtl .fc-timegrid-col-events { .fc-direction-rtl .fc-timegrid-col-events {
margin: 0 2px 0 2.5%; 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-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 #fff;
box-shadow: 0px 0px 0px 1px var(--fc-page-bg-color, #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: .85em;
font-size: var(--fc-small-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 { .fc-timegrid-event .fc-event-main {
padding: 1px 1px 0; padding: 1px 1px 0;
@ -1233,24 +1242,37 @@ A VERTICAL event
font-size: var(--fc-small-font-size, .85em); font-size: var(--fc-small-font-size, .85em);
margin-bottom: 1px; margin-bottom: 1px;
} }
.fc-timegrid-event-condensed .fc-event-main-frame { .fc-timegrid-event-short .fc-event-main-frame {
flex-direction: row; flex-direction: row;
overflow: hidden; 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 */ 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: .85em;
font-size: var(--fc-small-font-size, .85em) font-size: var(--fc-small-font-size, .85em)
} }
.fc-media-screen .fc-timegrid-event { .fc-timegrid-more-link { /* does NOT inherit from fc-timegrid-event */
position: absolute; /* absolute WITHIN the harness */ position: absolute;
top: 0; z-index: 9999; /* hack */
bottom: 1px; /* stay away from bottom slot line */ color: inherit;
left: 0; 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; right: 0;
} }
.fc-direction-rtl .fc-timegrid-more-link {
left: 0;
}
.fc { .fc {
/* line */ /* line */
@ -1336,12 +1358,28 @@ A VERTICAL event
border-right: 0; border-right: 0;
} }
.fc .fc-list-sticky .fc-list-day > * { /* the cells */ .fc .fc-list-sticky .fc-list-day > * { /* the cells */
position: -webkit-sticky;
position: sticky; position: sticky;
top: 0; top: 0;
background: #fff; background: #fff;
background: var(--fc-page-bg-color, #fff); /* for when headers are styled to be transparent and sticky */ 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 { .fc .fc-list-table th {
padding: 0; /* uses an inner-wrapper instead... */ padding: 0; /* uses an inner-wrapper instead... */
} }
@ -1427,3 +1465,31 @@ A VERTICAL event
color: inherit; /* natural color for navlinks */ 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 // Callback to launch the 'About' window
$('#launch-about').click(function() { $('#launch-about').click(function() {
var modal = $('#modal-about'); launchModalForm(`/about/`, {
no_post: true,
modal.modal({
backdrop: 'static',
keyboard: true,
}); });
modal.modal('show');
}); });
// Callback to launch the 'Database Stats' window // Callback to launch the 'Database Stats' window
@ -126,8 +121,6 @@ function inventreeDocReady() {
// Initialize clipboard-buttons // Initialize clipboard-buttons
attachClipboard('.clip-btn'); attachClipboard('.clip-btn');
attachClipboard('.clip-btn', 'modal-about');
attachClipboard('.clip-btn-version', 'modal-about', 'about-copy-text');
// Generate brand-icons // Generate brand-icons
$('.brand-icon').each(function(i, obj) { $('.brand-icon').each(function(i, obj) {

View File

@ -1,7 +1,6 @@
"""Unit tests for the main web views.""" """Unit tests for the main web views."""
import os import os
import re
from django.urls import reverse from django.urls import reverse
@ -42,17 +41,3 @@ class ViewTests(InvenTreeTestCase):
self.assertIn("<div id='detail-panels'>", content) self.assertIn("<div id='detail-panels'>", content)
# TODO: In future, run the javascript and ensure that the panels get created! # 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 users.api import user_urls
from .api import InfoView, NotFoundView from .api import InfoView, NotFoundView
from .views import (AppearanceSelectView, CurrencyRefreshView, from .views import (AboutView, AppearanceSelectView, CurrencyRefreshView,
CustomConnectionsView, CustomEmailView, CustomConnectionsView, CustomEmailView,
CustomPasswordResetFromKeyView, CustomPasswordResetFromKeyView,
CustomSessionDeleteOtherView, CustomSessionDeleteView, CustomSessionDeleteOtherView, CustomSessionDeleteView,
@ -150,6 +150,7 @@ frontendpatterns = [
re_path(r'^notifications/', include(notifications_urls)), re_path(r'^notifications/', include(notifications_urls)),
re_path(r'^search/', SearchView.as_view(), name='search'), re_path(r'^search/', SearchView.as_view(), name='search'),
re_path(r'^settings/', include(settings_urls)), 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'), re_path(r'^stats/', DatabaseStatsView.as_view(), name='stats'),
# admin sites # admin sites

View File

@ -750,6 +750,13 @@ class DatabaseStatsView(AjaxView):
ajax_form_title = _("System Information") 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): class NotificationsView(TemplateView):
"""View for showing notifications.""" """View for showing notifications."""

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -19,7 +19,7 @@ Relevant PRs:
from decimal import Decimal from decimal import Decimal
from django.db import models 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 django.db.models.functions import Coalesce
from sql_util.utils import SubquerySum 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__lft__gt=OuterRef(f'{reference}lft'),
part__rght__lt=OuterRef(f'{reference}rght'), part__rght__lt=OuterRef(f'{reference}rght'),
).filter(filter) ).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.exceptions import ValidationError
from django.core.validators import MinValueValidator from django.core.validators import MinValueValidator
from django.db import models, transaction 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.functions import Coalesce
from django.db.models.signals import post_save from django.db.models.signals import post_save
from django.db.utils import IntegrityError from django.db.utils import IntegrityError
@ -34,6 +34,7 @@ from stdimage.models import StdImageField
import common.models import common.models
import InvenTree.ready import InvenTree.ready
import InvenTree.tasks import InvenTree.tasks
import part.filters as part_filters
import part.settings as part_settings import part.settings as part_settings
from build import models as BuildModels from build import models as BuildModels
from common.models import InvenTreeSetting from common.models import InvenTreeSetting
@ -74,9 +75,9 @@ class PartCategory(MetadataMixin, InvenTreeTree):
tree_id = self.tree_id tree_id = self.tree_id
# Update each part in this category to point to the parent category # Update each part in this category to point to the parent category
for part in self.parts.all(): for p in self.parts.all():
part.category = self.parent p.category = self.parent
part.save() p.save()
# Update each child category # Update each child category
for child in self.children.all(): for child in self.children.all():
@ -221,7 +222,7 @@ class PartCategory(MetadataMixin, InvenTreeTree):
if include_parents: if include_parents:
queryset = PartCategoryStar.objects.filter( queryset = PartCategoryStar.objects.filter(
category__pk__in=[cat.pk for cat in cats] category__in=cats,
) )
else: else:
queryset = PartCategoryStar.objects.filter( queryset = PartCategoryStar.objects.filter(
@ -800,7 +801,10 @@ class Part(MetadataMixin, MPTTModel):
upload_to=rename_part_image, upload_to=rename_part_image,
null=True, null=True,
blank=True, blank=True,
variations={'thumbnail': (128, 128)}, variations={
'thumbnail': (128, 128),
'preview': (256, 256),
},
delete_orphans=False, delete_orphans=False,
verbose_name=_('Image'), verbose_name=_('Image'),
) )
@ -968,13 +972,10 @@ class Part(MetadataMixin, MPTTModel):
def requiring_build_orders(self): def requiring_build_orders(self):
"""Return list of outstanding build orders which require this part.""" """Return list of outstanding build orders which require this part."""
# List parts that this part is required for # 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 # Now, get a list of outstanding build orders which require this part
builds = BuildModels.Build.objects.filter( builds = BuildModels.Build.objects.filter(
part__in=part_ids, part__in=self.get_used_in().all(),
status__in=BuildStatus.ACTIVE_CODES status__in=BuildStatus.ACTIVE_CODES
) )
@ -1098,7 +1099,7 @@ class Part(MetadataMixin, MPTTModel):
if include_variants: if include_variants:
queryset = queryset.filter( 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: else:
queryset = queryset.filter(part=self) queryset = queryset.filter(part=self)
@ -1142,18 +1143,70 @@ class Part(MetadataMixin, MPTTModel):
total = None 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 # Annotate the 'available stock' for each part in the BOM
for item in bom_items.all(): ref = 'sub_part__'
stock = item.sub_part.available_stock 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, # Calculate the 'available stock' based on previous annotations
# ignore! queryset = queryset.annotate(
if item.quantity <= 0: available_stock=ExpressionWrapper(
continue 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: if total is None or n < total:
total = n total = n
@ -1336,11 +1389,10 @@ class Part(MetadataMixin, MPTTModel):
parents = self.get_ancestors(include_self=False) parents = self.get_ancestors(include_self=False)
# There are parents available # There are parents available
if parents.count() > 0: if parents.exists():
parent_ids = [p.pk for p in parents]
parent_filter = Q( parent_filter = Q(
part__id__in=parent_ids, part__in=parents,
inherited=True inherited=True
) )
@ -1425,7 +1477,7 @@ class Part(MetadataMixin, MPTTModel):
@property @property
def has_bom(self): def has_bom(self):
"""Return True if this Part instance has any BOM items""" """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): def get_trackable_parts(self):
"""Return a queryset of all trackable parts in the BOM for this part.""" """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. This is important when building the part.
""" """
return self.get_trackable_parts().count() > 0 return self.get_trackable_parts().exists()
@property @property
def bom_count(self): def bom_count(self):
@ -1482,7 +1534,7 @@ class Part(MetadataMixin, MPTTModel):
# Validate each line item, ignoring inherited ones # Validate each line item, ignoring inherited ones
bom_items = self.get_bom_items(include_inherited=False) bom_items = self.get_bom_items(include_inherited=False)
for item in bom_items.all(): for item in bom_items:
item.validate_hash() item.validate_hash()
self.bom_checksum = self.get_bom_hash() self.bom_checksum = self.get_bom_hash()
@ -1509,7 +1561,7 @@ class Part(MetadataMixin, MPTTModel):
if parts is None: if parts is None:
parts = set() parts = set()
bom_items = self.get_bom_items().all() bom_items = self.get_bom_items()
for bom_item in bom_items: for bom_item in bom_items:
@ -1533,7 +1585,7 @@ class Part(MetadataMixin, MPTTModel):
def has_complete_bom_pricing(self): def has_complete_bom_pricing(self):
"""Return true if there is pricing information for each item in the BOM.""" """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) 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: if item.sub_part.get_price_range(internal=use_internal) is None:
return False return False
@ -1609,7 +1661,7 @@ class Part(MetadataMixin, MPTTModel):
min_price = None min_price = None
max_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: if item.sub_part.pk == self.pk:
logger.warning(f"WARNING: BomItem ID {item.pk} contains itself in BOM") logger.warning(f"WARNING: BomItem ID {item.pk} contains itself in BOM")
@ -1689,7 +1741,7 @@ class Part(MetadataMixin, MPTTModel):
@property @property
def has_price_breaks(self): def has_price_breaks(self):
"""Return True if this part has sale price breaks""" """Return True if this part has sale price breaks"""
return self.price_breaks.count() > 0 return self.price_breaks.exists()
@property @property
def price_breaks(self): def price_breaks(self):
@ -1725,7 +1777,7 @@ class Part(MetadataMixin, MPTTModel):
@property @property
def has_internal_price_breaks(self): def has_internal_price_breaks(self):
"""Return True if this Part has internal pricing information""" """Return True if this Part has internal pricing information"""
return self.internal_price_breaks.count() > 0 return self.internal_price_breaks.exists()
@property @property
def internal_price_breaks(self): def internal_price_breaks(self):
@ -1978,7 +2030,7 @@ class Part(MetadataMixin, MPTTModel):
@property @property
def has_variants(self): def has_variants(self):
"""Check if this Part object has variants underneath it.""" """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): def get_all_variants(self):
"""Return all Part object which exist as a variant under this part.""" """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 b) It has non-virtual template parts above it
c) It has non-virtual sibling variants 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): def get_conversion_options(self):
"""Return options for converting this part to a "variant" within the same tree. """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 - Allow stock from all directly specified substitute parts
- If allow_variants is True, allow all part variants - 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): def save(self, *args, **kwargs):
"""Enforce 'clean' operation when saving a BomItem instance""" """Enforce 'clean' operation when saving a BomItem instance"""

View File

@ -4,8 +4,7 @@ import imghdr
from decimal import Decimal from decimal import Decimal
from django.db import models, transaction from django.db import models, transaction
from django.db.models import (ExpressionWrapper, F, FloatField, Func, Q, from django.db.models import ExpressionWrapper, F, FloatField, Q
Subquery)
from django.db.models.functions import Coalesce from django.db.models.functions import Coalesce
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils.translation import gettext_lazy as _ 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) thumbnail = serializers.CharField(source='get_thumbnail_url', read_only=True)
stock = serializers.FloatField(source='total_stock')
class Meta: class Meta:
"""Metaclass defining serializer fields""" """Metaclass defining serializer fields"""
model = Part model = Part
@ -270,7 +267,6 @@ class PartBriefSerializer(InvenTreeModelSerializer):
'is_template', 'is_template',
'purchaseable', 'purchaseable',
'salable', 'salable',
'stock',
'trackable', 'trackable',
'virtual', 'virtual',
'units', 'units',
@ -322,14 +318,7 @@ class PartSerializer(InvenTreeModelSerializer):
variant_query = part.filters.variant_stock_query() variant_query = part.filters.variant_stock_query()
queryset = queryset.annotate( queryset = queryset.annotate(
variant_stock=Coalesce( variant_stock=part.filters.annotate_variant_quantity(variant_query, reference='quantity'),
Subquery(
variant_query.annotate(
total=Func(F('quantity'), function='SUM', output_field=FloatField())
).values('total')),
0,
output_field=FloatField(),
)
) )
# Filter to limit builds to "active" # Filter to limit builds to "active"
@ -642,35 +631,14 @@ class BomItemSerializer(InvenTreeModelSerializer):
variant_stock_query = part.filters.variant_stock_query(reference='sub_part__') variant_stock_query = part.filters.variant_stock_query(reference='sub_part__')
queryset = queryset.alias( queryset = queryset.alias(
variant_stock_total=Coalesce( variant_stock_total=part.filters.annotate_variant_quantity(variant_stock_query, reference='quantity'),
Subquery( variant_bo_allocations=part.filters.annotate_variant_quantity(variant_stock_query, reference='sales_order_allocations__quantity'),
variant_stock_query.annotate( variant_so_allocations=part.filters.annotate_variant_quantity(variant_stock_query, reference='allocations__quantity'),
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(),
)
) )
queryset = queryset.annotate( queryset = queryset.annotate(
available_variant_stock=ExpressionWrapper( 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(), 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() { onPanelLoad("purchase-orders", function() {
loadPartPurchaseOrderTable( loadPartPurchaseOrderTable(
"#purchase-order-table", "#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 // history graphs
{% if price_history %} {% if price_history %}
var purchasepricedata = { 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 = {
labels: [ labels: [
{% for line in sale_history %}'{% render_date line.date %}',{% endfor %} {% for line in price_history %}'{% render_date line.date %}',{% endfor %}
], ],
datasets: [{ datasets: [{
label: '{% blocktrans %}Unit Price - {{currency}}{% endblocktrans %}', label: '{% blocktrans %}Purchase Unit Price - {{currency}}{% endblocktrans %}',
backgroundColor: 'rgba(255, 99, 132, 0.2)', backgroundColor: 'rgba(255, 99, 132, 0.2)',
borderColor: 'rgb(255, 99, 132)', borderColor: 'rgb(255, 99, 132)',
yAxisID: 'y', yAxisID: 'y',
data: [ data: [
{% for line in sale_history %}{{ line.price|stringformat:".2f" }},{% endfor %} {% for line in price_history %}{{ line.price|stringformat:".2f" }},{% endfor %}
], ],
borderWidth: 1, 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" %}', label: '{% trans "Quantity" %}',
backgroundColor: 'rgba(255, 206, 86, 0.2)', backgroundColor: 'rgba(255, 206, 86, 0.2)',
borderColor: 'rgb(255, 206, 86)', borderColor: 'rgb(255, 206, 86)',
yAxisID: 'y1', yAxisID: 'y1',
data: [ data: [
{% for line in sale_history %}{{ line.qty|stringformat:"f" }},{% endfor %} {% for line in price_history %}{{ line.qty|stringformat:"f" }},{% endfor %}
], ],
borderWidth: 1, borderWidth: 1
type: 'bar',
}] }]
} }
var SalePriceChart = loadSellPricingChart($('#SalePriceChart'), salepricedata) var StockPriceChart = loadStockPricingChart($('#StockPriceChart'), purchasepricedata)
{% endif %} {% 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'); enableSidebar('part');

View File

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

View File

@ -74,7 +74,6 @@ class StockDetail(RetrieveUpdateDestroyAPI):
kwargs['part_detail'] = True kwargs['part_detail'] = True
kwargs['location_detail'] = True kwargs['location_detail'] = True
kwargs['supplier_part_detail'] = True kwargs['supplier_part_detail'] = True
kwargs['test_detail'] = True
kwargs['context'] = self.get_serializer_context() kwargs['context'] = self.get_serializer_context()
return self.serializer_class(*args, **kwargs) return self.serializer_class(*args, **kwargs)

View File

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

View File

@ -136,7 +136,7 @@
{% endblock actions %} {% endblock actions %}
{% block thumbnail %} {% 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 %} {% endblock thumbnail %}
{% block details %} {% block details %}

View File

@ -2,108 +2,87 @@
{% load inventree_extras %} {% load inventree_extras %}
{% load i18n %} {% load i18n %}
<div class='modal fade modal-fixed-footer' tabindex='-1' role='dialog' id='modal-about'> <table class='table table-striped table-condensed'>
<div class='modal-dialog'> <col width='25'>
<div class='modal-content'> <tr>
<div class="modal-header"> <td><span class='fas fa-hashtag'></span></td>
<img src="{% static 'img/inventree.png' %}" height='40px' style='float: left; padding-right: 25px;' alt='Inventree Logo'> <td>{% trans "InvenTree Version" %}</td>
<h4>{% trans "InvenTree Version Information" %}</h4> <td>
<button type='button' class='btn-close' data-bs-dismiss='modal' aria-label='{% trans "Close" %}'></button> <a href="https://github.com/inventree/InvenTree/releases">{% inventree_version %}</a>{% include "clip.html" %}
</div> {% inventree_is_development as dev %}
<div class='modal-form-content-wrapper'> {% if dev %}
<div class='modal-form-content'> <span class='badge badge-right rounded-pill bg-primary'>{% trans "Development Version" %}</span>
<div> {% else %}
<table class='table table-striped table-condensed'> {% if up_to_date %}
<col width='25'> <span class='badge badge-right rounded-pill bg-success'>{% trans "Up to Date" %}</span>
<tr> {% else %}
<td><span class='fas fa-hashtag'></span></td> <span class='badge badge-right rounded-pill bg-info'>{% trans "Update Available" %}</span>
<td>{% trans "InvenTree Version" %}</td> {% endif %}
<td> {% endif %}
<a href="https://github.com/inventree/InvenTree/releases">{% inventree_version %}</a>{% include "clip.html" %} </td>
{% inventree_is_development as dev %} </tr>
{% if dev %} {% if dev %}
<span class='badge badge-right rounded-pill bg-primary'>{% trans "Development Version" %}</span> {% inventree_commit_hash as hash %}
{% else %} {% if hash %}
{% if up_to_date %} <tr>
<span class='badge badge-right rounded-pill bg-success'>{% trans "Up to Date" %}</span> <td><span class='fas fa-code-branch'></span></td>
{% else %} <td>{% trans "Commit Hash" %}</td><td>{{ hash }}{% include "clip.html" %}</td>
<span class='badge badge-right rounded-pill bg-info'>{% trans "Update Available" %}</span> </tr>
{% endif %} {% endif %}
{% endif %} {% inventree_commit_date as commit_date %}
</td> {% if commit_date %}
</tr> <tr>
{% if dev %} <td><span class='fas fa-calendar-alt'></span></td>
{% inventree_commit_hash as hash %} <td>{% trans "Commit Date" %}</td><td>{% render_date commit_date %}{% include "clip.html" %}</td>
{% if hash %} </tr>
<tr> {% endif %}
<td><span class='fas fa-code-branch'></span></td> {% endif %}
<td>{% trans "Commit Hash" %}</td><td>{{ hash }}{% include "clip.html" %}</td> <tr>
</tr> <td><span class='fas fa-book'></span></td>
{% endif %} <td>{% trans "InvenTree Documentation" %}</td>
{% inventree_commit_date as commit_date %} <td><a href="{% inventree_docs_url %}">{% inventree_docs_url %}</a></td>
{% if commit_date %} </tr>
<tr> <tr>
<td><span class='fas fa-calendar-alt'></span></td> <td><span class='fas fa-code'></span></td>
<td>{% trans "Commit Date" %}</td><td>{% render_date commit_date %}{% include "clip.html" %}</td> <td>{% trans "API Version" %}</td>
</tr> <td>{% inventree_api_version %}{% include "clip.html" %}</td>
{% endif %} </tr>
{% endif %} <tr>
<tr> <td><span class='fas fa-hashtag'></span></td>
<td><span class='fas fa-book'></span></td> <td>{% trans "Python Version" %}</td>
<td>{% trans "InvenTree Documentation" %}</td> <td>{% python_version %}</td>
<td><a href="{% inventree_docs_url %}">{% inventree_docs_url %}</a></td> </tr>
</tr> <tr>
<tr> <td><span class='fas fa-hashtag'></span></td>
<td><span class='fas fa-code'></span></td> <td>{% trans "Django Version" %}</td>
<td>{% trans "API Version" %}</td> <td><a href="https://www.djangoproject.com/">{% django_version %}</a>{% include "clip.html" %}</td>
<td>{% inventree_api_version %}{% include "clip.html" %}</td> </tr>
</tr> <tr>
<tr> <td><span class='fab fa-github'></span></td>
<td><span class='fas fa-hashtag'></span></td> <td>{% trans "View Code on GitHub" %}</td>
<td>{% trans "Python Version" %}</td> <td><a href="{% inventree_github_url %}">{% inventree_github_url %}</a></td>
<td>{% python_version %}</td> </tr>
</tr> <tr>
<tr> <td><span class='fas fa-balance-scale'></span></td>
<td><span class='fas fa-hashtag'></span></td> <td>{% trans "Credits" %}</td>
<td>{% trans "Django Version" %}</td> <td><a href="{% inventree_credits_url %}">{% inventree_credits_url %}</a></td>
<td><a href="https://www.djangoproject.com/">{% django_version %}</a>{% include "clip.html" %}</td> </tr>
</tr> <tr>
<tr> <td><span class='fas fa-mobile-alt'></span></td>
<td><span class='fab fa-github'></span></td> <td>{% trans "Mobile App" %}</td>
<td>{% trans "View Code on GitHub" %}</td> <td><a href="{% inventree_docs_url %}/app/app">{% inventree_docs_url %}/app/app</a></td>
<td><a href="{% inventree_github_url %}">{% inventree_github_url %}</a></td> </tr>
</tr> <tr>
<tr> <td><span class='fas fa-bug'></span></td>
<td><span class='fas fa-balance-scale'></span></td> <td>{% trans "Submit Bug Report" %}</td>
<td>{% trans "Credits" %}</td> <td><a href='{% inventree_github_url %}/issues'>{% inventree_github_url %}issues</a></td>
<td><a href="{% inventree_credits_url %}">{% inventree_credits_url %}</a></td> </tr>
</tr> <tr><td></td><td></td>
<tr> <td>
<td><span class='fas fa-mobile-alt'></span></td> <span style="display: none;" id="about-copy-text">{% include "version.html" %}</span>
<td>{% trans "Mobile App" %}</td> <span class="float-right">
<td><a href="{% inventree_docs_url %}/app/app">{% inventree_docs_url %}/app/app</a></td> <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>
</tr> </span>
<tr> </td>
<td><span class='fas fa-bug'></span></td> </tr>
<td>{% trans "Submit Bug Report" %}</td> </table>
<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>

View File

@ -84,29 +84,14 @@
{% endblock %} {% endblock %}
</div> </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 --> <!-- general JS -->
{% include "third_party_js.html" %}
<script type='text/javascript' src="{% static 'script/inventree/inventree.js' %}"></script> <script type='text/javascript' src="{% static 'script/inventree/inventree.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'notification.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'> <script type='text/javascript'>

View File

@ -39,15 +39,15 @@
<!-- CSS --> <!-- CSS -->
<link rel="stylesheet" href="{% static 'bootstrap/css/bootstrap.min.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/bootstrap-table.min.css' %}">
<link rel="stylesheet" href="{% static 'bootstrap-table/extensions/group-by-v2/bootstrap-table-group-by.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 '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 "treegrid/css/jquery.treegrid.css" %}'>
<link rel="stylesheet" href="{% static 'fontawesome/css/brands.min.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 '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 '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 'script/jquery-ui/jquery-ui.min.css' %}">
<link rel="stylesheet" href="{% static 'easymde/easymde.min.css' %}"> <link rel="stylesheet" href="{% static 'easymde/easymde.min.css' %}">
@ -132,83 +132,48 @@
</div> </div>
{% include 'modals.html' %} {% include 'modals.html' %}
{% if show_about %}{% include 'about.html' %}{% endif %}
{% include "notifications.html" %} {% include "notifications.html" %}
{% include "search.html" %} {% include "search.html" %}
</div> </div>
<!-- Scripts --> {% include "third_party_js.html" %}
<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>
<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 --> <!-- 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 --> <!-- dynamic javascript templates -->
<script type='text/javascript' src="{% url 'calendar.js' %}"></script> <script defer type='text/javascript' src="{% url 'calendar.js' %}"></script>
<script type='text/javascript' src="{% url 'nav.js' %}"></script> <script defer type='text/javascript' src="{% url 'nav.js' %}"></script>
<script type='text/javascript' src="{% url 'settings.js' %}"></script> <script defer type='text/javascript' src="{% url 'settings.js' %}"></script>
<!-- translated javascript templates--> <!-- translated javascript templates-->
<script type='text/javascript' src="{% i18n_static 'api.js' %}"></script> <script defer type='text/javascript' src="{% i18n_static 'api.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'attachment.js' %}"></script> <script defer type='text/javascript' src="{% i18n_static 'attachment.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'barcode.js' %}"></script> <script defer type='text/javascript' src="{% i18n_static 'barcode.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'bom.js' %}"></script> <script defer type='text/javascript' src="{% i18n_static 'bom.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'build.js' %}"></script> <script defer type='text/javascript' src="{% i18n_static 'build.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'company.js' %}"></script> <script defer type='text/javascript' src="{% i18n_static 'company.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'filters.js' %}"></script> <script defer type='text/javascript' src="{% i18n_static 'filters.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'forms.js' %}"></script> <script defer type='text/javascript' src="{% i18n_static 'forms.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'helpers.js' %}"></script> <script defer type='text/javascript' src="{% i18n_static 'helpers.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'label.js' %}"></script> <script defer type='text/javascript' src="{% i18n_static 'label.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'modals.js' %}"></script> <script defer type='text/javascript' src="{% i18n_static 'modals.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'model_renderers.js' %}"></script> <script defer type='text/javascript' src="{% i18n_static 'model_renderers.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'order.js' %}"></script> <script defer type='text/javascript' src="{% i18n_static 'order.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'part.js' %}"></script> <script defer type='text/javascript' src="{% i18n_static 'part.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'report.js' %}"></script> <script defer type='text/javascript' src="{% i18n_static 'report.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'search.js' %}"></script> <script defer type='text/javascript' src="{% i18n_static 'search.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'stock.js' %}"></script> <script defer type='text/javascript' src="{% i18n_static 'stock.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'plugin.js' %}"></script> <script defer type='text/javascript' src="{% i18n_static 'plugin.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'tables.js' %}"></script> <script defer type='text/javascript' src="{% i18n_static 'tables.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'table_filters.js' %}"></script> <script defer type='text/javascript' src="{% i18n_static 'table_filters.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'notification.js' %}"></script> <script defer 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>
{% block js_load %} {% block js_load %}
{% endblock %} {% endblock %}
<script type='text/javascript'> <script defer type='text/javascript'>
$(document).ready(function () { $(document).ready(function () {

View File

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

View File

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

View File

@ -1,5 +1,6 @@
{% load static %} {% load static %}
{% load i18n %} {% load i18n %}
{% load inventree_extras %}
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
@ -65,15 +66,7 @@
{% block body_scripts_general %} {% block body_scripts_general %}
{% endblock %} {% endblock %}
{% include "third_party_js.html" %}
<!-- 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>
<!-- general JS --> <!-- general JS -->
<script type='text/javascript' src="{% static 'script/inventree/inventree.js' %}"></script> <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.auth.models import Group, Permission
from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.core.cache import cache
from django.db import models from django.db import models
from django.db.models import Q, UniqueConstraint from django.db.models import Q, UniqueConstraint
from django.db.models.signals import post_delete, post_save 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}") logger.info(f"Adding permission {child_perm} to group {group.name}")
@receiver(post_save, sender=Group, dispatch_uid='create_missing_rule_sets') def clear_user_role_cache(user):
def create_missing_rule_sets(sender, instance, **kwargs): """Remove user role permission information from the cache.
"""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. - 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): def check_user_role(user, role, permission):
@ -491,6 +498,17 @@ def check_user_role(user, role, permission):
if user.is_superuser: if user.is_superuser:
return True 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 group in user.groups.all():
for rule in group.rule_sets.all(): for rule in group.rule_sets.all():
@ -498,19 +516,24 @@ def check_user_role(user, role, permission):
if rule.name == role: if rule.name == role:
if permission == 'add' and rule.can_add: if permission == 'add' and rule.can_add:
return True result = True
break
if permission == 'change' and rule.can_change: if permission == 'change' and rule.can_change:
return True result = True
break
if permission == 'view' and rule.can_view: if permission == 'view' and rule.can_view:
return True result = True
break
if permission == 'delete' and rule.can_delete: if permission == 'delete' and rule.can_delete:
return True result = True
break
# No matching permissions found # Save result to cache
return False cache.set(key, result, timeout=3600)
return result
class Owner(models.Model): 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.""" """Callback function to delete an owner instance after either a new group or user instance is deleted."""
owner = Owner.get_owner(instance) owner = Owner.get_owner(instance)
owner.delete() 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)