diff --git a/.github/workflows/docker_build.yaml b/.github/workflows/docker_build.yaml index 89d52664e4..26fc69a0f5 100644 --- a/.github/workflows/docker_build.yaml +++ b/.github/workflows/docker_build.yaml @@ -8,7 +8,7 @@ on: - 'master' jobs: - + docker: runs-on: ubuntu-latest diff --git a/.github/workflows/mysql.yaml b/.github/workflows/mysql.yaml index 087a866fbd..5bafe56253 100644 --- a/.github/workflows/mysql.yaml +++ b/.github/workflows/mysql.yaml @@ -33,7 +33,7 @@ jobs: options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3 ports: - 3306:3306 - + steps: - name: Checkout Code uses: actions/checkout@v2 diff --git a/.github/workflows/postgresql.yaml b/.github/workflows/postgresql.yaml index ae8f52b962..3481895d85 100644 --- a/.github/workflows/postgresql.yaml +++ b/.github/workflows/postgresql.yaml @@ -5,7 +5,7 @@ name: PostgreSQL on: ["push", "pull_request"] jobs: - + test: runs-on: ubuntu-latest diff --git a/InvenTree/InvenTree/api_tester.py b/InvenTree/InvenTree/api_tester.py index 2e69e40969..eb92bd80c1 100644 --- a/InvenTree/InvenTree/api_tester.py +++ b/InvenTree/InvenTree/api_tester.py @@ -83,7 +83,7 @@ class InvenTreeAPITestCase(APITestCase): self.assertEqual(response.status_code, code) return response - + def post(self, url, data): """ Issue a POST request diff --git a/InvenTree/InvenTree/context.py b/InvenTree/InvenTree/context.py index e072f5a5ea..669b55b0c0 100644 --- a/InvenTree/InvenTree/context.py +++ b/InvenTree/InvenTree/context.py @@ -71,7 +71,7 @@ def status_codes(request): def user_roles(request): """ Return a map of the current roles assigned to the user. - + Roles are denoted by their simple names, and then the permission type. Permissions can be access as follows: diff --git a/InvenTree/InvenTree/exchange.py b/InvenTree/InvenTree/exchange.py index 04ceabccd8..06de4861ec 100644 --- a/InvenTree/InvenTree/exchange.py +++ b/InvenTree/InvenTree/exchange.py @@ -17,5 +17,5 @@ class InvenTreeManualExchangeBackend(BaseExchangeBackend): """ Do not get any rates... """ - + return {} diff --git a/InvenTree/InvenTree/fields.py b/InvenTree/InvenTree/fields.py index 155f77c639..c496c1bb22 100644 --- a/InvenTree/InvenTree/fields.py +++ b/InvenTree/InvenTree/fields.py @@ -102,5 +102,5 @@ class RoundingDecimalField(models.DecimalField): } defaults.update(kwargs) - + return super().formfield(**kwargs) diff --git a/InvenTree/InvenTree/helpers.py b/InvenTree/InvenTree/helpers.py index d5508b7db2..1097c5663b 100644 --- a/InvenTree/InvenTree/helpers.py +++ b/InvenTree/InvenTree/helpers.py @@ -35,7 +35,7 @@ def generateTestKey(test_name): """ Generate a test 'key' for a given test name. This must not have illegal chars as it will be used for dict lookup in a template. - + Tests must be named such that they will have unique keys. """ @@ -102,7 +102,7 @@ def TestIfImageURL(url): '.tif', '.tiff', '.webp', '.gif', ] - + def str2bool(text, test=True): """ Test if a string 'looks' like a boolean value. @@ -137,10 +137,10 @@ def isNull(text): """ Test if a string 'looks' like a null value. This is useful for querying the API against a null key. - + Args: text: Input text - + Returns: True if the text looks like a null value """ @@ -157,7 +157,7 @@ def normalize(d): d = Decimal(d) d = d.normalize() - + # Ref: https://docs.python.org/3/library/decimal.html return d.quantize(Decimal(1)) if d == d.to_integral() else d.normalize() @@ -165,14 +165,14 @@ def normalize(d): def increment(n): """ Attempt to increment an integer (or a string that looks like an integer!) - + e.g. 001 -> 002 2 -> 3 AB01 -> AB02 QQQ -> QQQ - + """ value = str(n).strip() @@ -314,7 +314,7 @@ def MakeBarcode(object_name, object_pk, object_data={}, **kwargs): def GetExportFormats(): """ Return a list of allowable file formats for exporting data """ - + return [ 'csv', 'tsv', @@ -327,7 +327,7 @@ def GetExportFormats(): def DownloadFile(data, filename, content_type='application/text'): """ Create a dynamic file for the user to download. - + Args: data: Raw file data (string or bytes) filename: Filename for the file download @@ -525,7 +525,7 @@ def addUserPermission(user, permission): """ Shortcut function for adding a certain permission to a user. """ - + perm = Permission.objects.get(codename=permission) user.user_permissions.add(perm) @@ -576,7 +576,7 @@ def getOldestMigrationFile(app, exclude_extension=True, ignore_initial=True): continue num = int(f.split('_')[0]) - + if oldest_file is None or num < oldest_num: oldest_num = num oldest_file = f @@ -585,7 +585,7 @@ def getOldestMigrationFile(app, exclude_extension=True, ignore_initial=True): oldest_file = oldest_file.replace('.py', '') return oldest_file - + def getNewestMigrationFile(app, exclude_extension=True): """ diff --git a/InvenTree/InvenTree/middleware.py b/InvenTree/InvenTree/middleware.py index f30d77ad3b..b905e86795 100644 --- a/InvenTree/InvenTree/middleware.py +++ b/InvenTree/InvenTree/middleware.py @@ -77,12 +77,20 @@ class AuthRequiredMiddleware(object): if request.path_info == reverse_lazy('logout'): return HttpResponseRedirect(reverse_lazy('login')) - login = reverse_lazy('login') + path = request.path_info - if not request.path_info == login and not request.path_info.startswith('/api/'): + # List of URL endpoints we *do not* want to redirect to + urls = [ + reverse_lazy('login'), + reverse_lazy('logout'), + reverse_lazy('admin:login'), + reverse_lazy('admin:logout'), + ] + + if path not in urls and not path.startswith('/api/'): # Save the 'next' parameter to pass through to the login view - return redirect('%s?next=%s' % (login, request.path)) + return redirect('%s?next=%s' % (reverse_lazy('login'), request.path)) # Code to be executed for each request/response after # the view is called. diff --git a/InvenTree/InvenTree/models.py b/InvenTree/InvenTree/models.py index 8494b52a10..5822f8a19f 100644 --- a/InvenTree/InvenTree/models.py +++ b/InvenTree/InvenTree/models.py @@ -129,7 +129,7 @@ class InvenTreeTree(MPTTModel): Here an 'item' is considered to be the 'leaf' at the end of each branch, and the exact nature here will depend on the class implementation. - + The default implementation returns zero """ return 0 diff --git a/InvenTree/InvenTree/permissions.py b/InvenTree/InvenTree/permissions.py index 973395a2a0..defb370435 100644 --- a/InvenTree/InvenTree/permissions.py +++ b/InvenTree/InvenTree/permissions.py @@ -17,7 +17,7 @@ class RolePermission(permissions.BasePermission): - PUT - PATCH - DELETE - + Specify the required "role" using the role_required attribute. e.g. diff --git a/InvenTree/InvenTree/serializers.py b/InvenTree/InvenTree/serializers.py index 6b40b8eb31..fa7674723c 100644 --- a/InvenTree/InvenTree/serializers.py +++ b/InvenTree/InvenTree/serializers.py @@ -44,7 +44,7 @@ class InvenTreeModelSerializer(serializers.ModelSerializer): In addition to running validators on the serializer fields, this class ensures that the underlying model is also validated. """ - + # Run any native validation checks first (may throw an ValidationError) data = super(serializers.ModelSerializer, self).validate(data) diff --git a/InvenTree/InvenTree/static/css/inventree.css b/InvenTree/InvenTree/static/css/inventree.css index 9d322f339d..e3191405d9 100644 --- a/InvenTree/InvenTree/static/css/inventree.css +++ b/InvenTree/InvenTree/static/css/inventree.css @@ -466,6 +466,24 @@ background: #eee; } +/* pricing table widths */ +.table-price-two tr td:first-child { + width: 40%; +} + +.table-price-three tr td:first-child { + width: 40%; +} + +.table-price-two tr td:last-child { + width: 60%; +} + +.table-price-three tr td:last-child { + width: 30%; +} +/* !pricing table widths */ + .btn-glyph { padding-left: 6px; padding-right: 6px; @@ -489,7 +507,7 @@ padding-right: 6px; padding-top: 3px; padding-bottom: 2px; -}; +} .panel-heading .badge { float: right; @@ -550,7 +568,7 @@ } .media { - //padding-top: 15px; + /* padding-top: 15px; */ overflow: visible; } @@ -576,8 +594,8 @@ width: 160px; position: fixed; z-index: 1; - //top: 0; - //left: 0; + /* top: 0; + left: 0; */ overflow-x: hidden; padding-top: 20px; padding-right: 25px; @@ -808,7 +826,7 @@ input[type="submit"] { width: 100%; padding: 20px; z-index: 5000; - pointer-events: none; // Prevent this div from blocking links underneath + pointer-events: none; /* Prevent this div from blocking links underneath */ } .alert { @@ -918,4 +936,15 @@ input[type="submit"] { input[type="date"].form-control, input[type="time"].form-control, input[type="datetime-local"].form-control, input[type="month"].form-control { line-height: unset; -} \ No newline at end of file +} + +.clip-btn { + font-size: 10px; + padding: 0px 6px; + color: var(--label-grey); + background: none; +} + +.clip-btn:hover { + background: var(--label-grey); +} diff --git a/InvenTree/InvenTree/static/script/clipboard.min.js b/InvenTree/InvenTree/static/script/clipboard.min.js new file mode 100644 index 0000000000..95f55d7b0c --- /dev/null +++ b/InvenTree/InvenTree/static/script/clipboard.min.js @@ -0,0 +1,7 @@ +/*! + * clipboard.js v2.0.8 + * https://clipboardjs.com/ + * + * Licensed MIT © Zeno Rocha + */ +!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.ClipboardJS=e():t.ClipboardJS=e()}(this,function(){return n={134:function(t,e,n){"use strict";n.d(e,{default:function(){return r}});var e=n(279),i=n.n(e),e=n(370),a=n.n(e),e=n(817),o=n.n(e);function c(t){return(c="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(t)}function u(t,e){for(var n=0;n`; html += ``; html += ``; diff --git a/InvenTree/InvenTree/status.py b/InvenTree/InvenTree/status.py index 5531d4c270..970e88831d 100644 --- a/InvenTree/InvenTree/status.py +++ b/InvenTree/InvenTree/status.py @@ -64,7 +64,7 @@ def is_email_configured(): if not settings.EMAIL_HOST_USER: configured = False - + # Display warning unless in test mode if not settings.TESTING: logger.warning("EMAIL_HOST_USER is not configured") diff --git a/InvenTree/InvenTree/status_codes.py b/InvenTree/InvenTree/status_codes.py index 6294eba06e..c73ef10018 100644 --- a/InvenTree/InvenTree/status_codes.py +++ b/InvenTree/InvenTree/status_codes.py @@ -16,7 +16,7 @@ class StatusCode: # If the key cannot be found, pass it back if key not in cls.options.keys(): return key - + value = cls.options.get(key, key) color = cls.colors.get(key, 'grey') diff --git a/InvenTree/InvenTree/test_api.py b/InvenTree/InvenTree/test_api.py index 52765db2a7..8435d756fb 100644 --- a/InvenTree/InvenTree/test_api.py +++ b/InvenTree/InvenTree/test_api.py @@ -119,7 +119,7 @@ class APITests(InvenTreeAPITestCase): self.assertNotIn('add', roles[rule]) self.assertNotIn('change', roles[rule]) self.assertNotIn('delete', roles[rule]) - + def test_with_superuser(self): """ Superuser should have *all* roles assigned diff --git a/InvenTree/InvenTree/test_tasks.py b/InvenTree/InvenTree/test_tasks.py index 02e8d14e5e..e9c9d9f01c 100644 --- a/InvenTree/InvenTree/test_tasks.py +++ b/InvenTree/InvenTree/test_tasks.py @@ -37,7 +37,7 @@ class ScheduledTaskTests(TestCase): # Attempt to schedule the same task again InvenTree.tasks.schedule_task(task, schedule_type=Schedule.MINUTES, minutes=5) self.assertEqual(self.get_tasks(task).count(), 1) - + # But the 'minutes' should have been updated t = Schedule.objects.get(func=task) self.assertEqual(t.minutes, 5) diff --git a/InvenTree/InvenTree/tests.py b/InvenTree/InvenTree/tests.py index af812fe8a3..d65829cf8e 100644 --- a/InvenTree/InvenTree/tests.py +++ b/InvenTree/InvenTree/tests.py @@ -97,7 +97,7 @@ class TestHelpers(TestCase): self.assertEqual(helpers.getMediaUrl('xx/yy.png'), '/media/xx/yy.png') def testDecimal2String(self): - + self.assertEqual(helpers.decimal2string(Decimal('1.2345000')), '1.2345') self.assertEqual(helpers.decimal2string('test'), 'test') @@ -205,7 +205,7 @@ class TestMPTT(TestCase): child = StockLocation.objects.get(pk=5) parent.parent = child - + with self.assertRaises(InvalidMove): parent.save() @@ -223,7 +223,7 @@ class TestMPTT(TestCase): drawer.save() self.assertNotEqual(tree, drawer.tree_id) - + class TestSerialNumberExtraction(TestCase): """ Tests for serial number extraction code """ diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index c57e82addc..ab2ced7d5e 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -81,7 +81,7 @@ settings_urls = [ url(r'^user/?', SettingsView.as_view(template_name='InvenTree/settings/user.html'), name='settings-user'), url(r'^appearance/?', AppearanceSelectView.as_view(), name='settings-appearance'), url(r'^i18n/?', include('django.conf.urls.i18n')), - + url(r'^global/?', SettingsView.as_view(template_name='InvenTree/settings/global.html'), name='settings-global'), url(r'^report/?', SettingsView.as_view(template_name='InvenTree/settings/report.html'), name='settings-report'), url(r'^category/?', SettingCategorySelectView.as_view(), name='settings-category'), @@ -137,7 +137,7 @@ urlpatterns = [ url(r'^login/?', auth_views.LoginView.as_view(), name='login'), url(r'^logout/', auth_views.LogoutView.as_view(template_name='registration/logged_out.html'), name='logout'), - + url(r'^settings/', include(settings_urls)), url(r'^edit-user/', EditUserView.as_view(), name='edit-user'), diff --git a/InvenTree/InvenTree/validators.py b/InvenTree/InvenTree/validators.py index f8199ef20b..1b6a6b3f0b 100644 --- a/InvenTree/InvenTree/validators.py +++ b/InvenTree/InvenTree/validators.py @@ -74,7 +74,7 @@ def validate_build_order_reference(value): match = re.search(pattern, value) if match is None: - raise ValidationError(_('Reference must match pattern') + f" '{pattern}'") + raise ValidationError(_('Reference must match pattern {pattern}').format(pattern=pattern)) def validate_purchase_order_reference(value): @@ -88,7 +88,7 @@ def validate_purchase_order_reference(value): match = re.search(pattern, value) if match is None: - raise ValidationError(_('Reference must match pattern') + f" '{pattern}'") + raise ValidationError(_('Reference must match pattern {pattern}').format(pattern=pattern)) def validate_sales_order_reference(value): @@ -102,7 +102,7 @@ def validate_sales_order_reference(value): match = re.search(pattern, value) if match is None: - raise ValidationError(_('Reference must match pattern') + f" '{pattern}'") + raise ValidationError(_('Reference must match pattern {pattern}').format(pattern=pattern)) def validate_tree_name(value): @@ -130,7 +130,7 @@ def validate_overage(value): if i < 0: raise ValidationError(_("Overage value must not be negative")) - + # Looks like an integer! return True except ValueError: diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py index def4b34781..d285efae36 100644 --- a/InvenTree/InvenTree/views.py +++ b/InvenTree/InvenTree/views.py @@ -176,7 +176,7 @@ class InvenTreeRoleMixin(PermissionRequiredMixin): if role not in RuleSet.RULESET_NAMES: raise ValueError(f"Role '{role}' is not a valid role") - + if permission not in RuleSet.RULESET_PERMISSIONS: raise ValueError(f"Permission '{permission}' is not a valid permission") @@ -223,7 +223,7 @@ class InvenTreeRoleMixin(PermissionRequiredMixin): Return the 'permission_class' required for the current View. Must be one of: - + - view - change - add @@ -389,7 +389,7 @@ class QRCodeView(AjaxView): """ ajax_template_name = "qr_code.html" - + def get(self, request, *args, **kwargs): self.request = request self.pk = self.kwargs['pk'] @@ -398,7 +398,7 @@ class QRCodeView(AjaxView): def get_qr_data(self): """ Returns the text object to render to a QR code. The actual rendering will be handled by the template """ - + return None def get_context_data(self): @@ -406,7 +406,7 @@ class QRCodeView(AjaxView): Explicity passes the parameter 'qr_data' """ - + context = {} qr = self.get_qr_data() @@ -415,7 +415,7 @@ class QRCodeView(AjaxView): context['qr_data'] = qr else: context['error_msg'] = 'Error generating QR code' - + return context @@ -507,7 +507,7 @@ class AjaxUpdateView(AjaxMixin, UpdateView): """ super(UpdateView, self).get(request, *args, **kwargs) - + return self.renderJsonResponse(request, self.get_form(), context=self.get_context_data()) def save(self, object, form, **kwargs): @@ -673,7 +673,7 @@ class SetPasswordView(AjaxUpdateView): p1 = request.POST.get('enter_password', '') p2 = request.POST.get('confirm_password', '') - + if valid: # Passwords must match @@ -712,7 +712,7 @@ class IndexView(TemplateView): # Generate a list of orderable parts which have stock below their minimum values # TODO - Is there a less expensive way to get these from the database # context['to_order'] = [part for part in Part.objects.filter(purchaseable=True) if part.need_to_restock()] - + # Generate a list of assembly parts which have stock below their minimum values # TODO - Is there a less expensive way to get these from the database # context['to_build'] = [part for part in Part.objects.filter(assembly=True) if part.need_to_restock()] @@ -752,7 +752,7 @@ class DynamicJsView(TemplateView): template_name = "" content_type = 'text/javascript' - + class SettingsView(TemplateView): """ View for configuring User settings @@ -830,7 +830,7 @@ class AppearanceSelectView(FormView): if form.is_valid(): theme_selected = form.cleaned_data['name'] - + # Set color theme to form selection user_theme.name = theme_selected user_theme.save() @@ -893,7 +893,7 @@ class DatabaseStatsView(AjaxView): # Part stats ctx['part_count'] = Part.objects.count() ctx['part_cat_count'] = PartCategory.objects.count() - + # Stock stats ctx['stock_item_count'] = StockItem.objects.count() ctx['stock_loc_count'] = StockLocation.objects.count() diff --git a/InvenTree/barcodes/api.py b/InvenTree/barcodes/api.py index e6b3ea84e3..6ab848c3f6 100644 --- a/InvenTree/barcodes/api.py +++ b/InvenTree/barcodes/api.py @@ -73,7 +73,7 @@ class BarcodeScan(APIView): # A plugin has been found! if plugin is not None: - + # Try to associate with a stock item item = plugin.getStockItem() @@ -133,7 +133,7 @@ class BarcodeScan(APIView): class BarcodeAssign(APIView): """ Endpoint for assigning a barcode to a stock item. - + - This only works if the barcode is not already associated with an object in the database - If the barcode does not match an object, then the barcode hash is assigned to the StockItem """ @@ -178,7 +178,7 @@ class BarcodeAssign(APIView): # Matching plugin was found if plugin is not None: - + hash = plugin.hash() response['hash'] = hash response['plugin'] = plugin.name @@ -234,7 +234,7 @@ class BarcodeAssign(APIView): barcode_api_urls = [ url(r'^link/$', BarcodeAssign.as_view(), name='api-barcode-link'), - + # Catch-all performs barcode 'scan' url(r'^.*$', BarcodeScan.as_view(), name='api-barcode-scan'), ] diff --git a/InvenTree/barcodes/barcode.py b/InvenTree/barcodes/barcode.py index a00e91d7e4..7ab9f3716a 100644 --- a/InvenTree/barcodes/barcode.py +++ b/InvenTree/barcodes/barcode.py @@ -21,7 +21,7 @@ def hash_barcode(barcode_data): HACK: Remove any 'non printable' characters from the hash, as it seems browers will remove special control characters... - + TODO: Work out a way around this! """ diff --git a/InvenTree/barcodes/tests.py b/InvenTree/barcodes/tests.py index 5f178d923c..1d8f53ec4c 100644 --- a/InvenTree/barcodes/tests.py +++ b/InvenTree/barcodes/tests.py @@ -92,7 +92,7 @@ class BarcodeAPITest(APITestCase): data = response.data self.assertEqual(response.status_code, status.HTTP_200_OK) - + self.assertIn('stockitem', data) pk = data['stockitem']['pk'] @@ -121,7 +121,7 @@ class BarcodeAPITest(APITestCase): data = response.data self.assertEqual(response.status_code, status.HTTP_200_OK) - + self.assertIn('success', data) hash = data['hash'] diff --git a/InvenTree/build/api.py b/InvenTree/build/api.py index 10cc7e2024..160642281a 100644 --- a/InvenTree/build/api.py +++ b/InvenTree/build/api.py @@ -20,7 +20,7 @@ from .serializers import BuildSerializer, BuildItemSerializer class BuildList(generics.ListCreateAPIView): """ API endpoint for accessing a list of Build objects. - + - GET: Return list of objects (with filters) - POST: Create a new Build object """ @@ -65,7 +65,7 @@ class BuildList(generics.ListCreateAPIView): queryset = BuildSerializer.annotate_queryset(queryset) return queryset - + def filter_queryset(self, queryset): queryset = super().filter_queryset(queryset) diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index c5da505f43..a278b4e17c 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -118,7 +118,7 @@ class Build(MPTTModel): def get_absolute_url(self): return reverse('build-detail', kwargs={'pk': self.id}) - + reference = models.CharField( unique=True, max_length=64, @@ -168,7 +168,7 @@ class Build(MPTTModel): null=True, blank=True, help_text=_('SalesOrder to which this build is allocated') ) - + take_from = models.ForeignKey( 'stock.StockLocation', verbose_name=_('Source Location'), @@ -177,7 +177,7 @@ class Build(MPTTModel): null=True, blank=True, help_text=_('Select location to take stock from for this build (leave blank to take from any stock location)') ) - + destination = models.ForeignKey( 'stock.StockLocation', verbose_name=_('Destination Location'), @@ -207,7 +207,7 @@ class Build(MPTTModel): validators=[MinValueValidator(0)], help_text=_('Build status code') ) - + batch = models.CharField( verbose_name=_('Batch Code'), max_length=100, @@ -215,9 +215,9 @@ class Build(MPTTModel): null=True, help_text=_('Batch code for this build output') ) - + creation_date = models.DateField(auto_now_add=True, editable=False, verbose_name=_('Creation Date')) - + target_date = models.DateField( null=True, blank=True, verbose_name=_('Target completion date'), @@ -251,7 +251,7 @@ class Build(MPTTModel): help_text=_('User responsible for this build order'), related_name='builds_responsible', ) - + link = InvenTree.fields.InvenTreeURLField( verbose_name=_('External Link'), blank=True, help_text=_('Link to external URL') @@ -272,7 +272,7 @@ class Build(MPTTModel): else: descendants = self.get_descendants(include_self=True) Build.objects.filter(parent__pk__in=[d.pk for d in descendants]) - + def sub_build_count(self, cascade=True): """ Return the number of sub builds under this one. @@ -295,7 +295,7 @@ class Build(MPTTModel): query = query.filter(Build.OVERDUE_FILTER) return query.exists() - + @property def active(self): """ @@ -441,7 +441,7 @@ class Build(MPTTModel): # Extract the "most recent" build order reference builds = cls.objects.exclude(reference=None) - + if not builds.exists(): return None @@ -543,7 +543,7 @@ class Build(MPTTModel): - The sub_item in the BOM line must *not* be trackable - There is only a single stock item available (which has not already been allocated to this build) - The stock item has an availability greater than zero - + Returns: A list object containing the StockItem objects to be allocated (and the quantities). Each item in the list is a dict as follows: @@ -648,7 +648,7 @@ class Build(MPTTModel): """ Deletes all stock allocations for this build. """ - + allocations = BuildItem.objects.filter(build=self) allocations.delete() @@ -1145,7 +1145,7 @@ class BuildItem(models.Model): """ self.validate_unique() - + super().clean() errors = {} @@ -1159,7 +1159,7 @@ class BuildItem(models.Model): # Allocated part must be in the BOM for the master part if self.stock_item.part not in self.build.part.getRequiredParts(recursive=False): errors['stock_item'] = [_("Selected stock item not found in BOM for part '{p}'").format(p=self.build.part.full_name)] - + # Allocated quantity cannot exceed available stock quantity if self.quantity > self.stock_item.quantity: errors['quantity'] = [_("Allocated quantity ({n}) must not exceed available quantity ({q})").format( diff --git a/InvenTree/build/templates/build/allocate.html b/InvenTree/build/templates/build/allocate.html index dee90a26a0..de07614c8e 100644 --- a/InvenTree/build/templates/build/allocate.html +++ b/InvenTree/build/templates/build/allocate.html @@ -86,7 +86,7 @@ } ); }); - + $("#btn-order-parts").click(function() { launchModalForm("/order/purchase-order/order-parts/", { data: { @@ -94,7 +94,7 @@ }, }); }); - + {% endif %} {% endblock %} diff --git a/InvenTree/build/templates/build/build_base.html b/InvenTree/build/templates/build/build_base.html index c0f6e400b6..177fad8d6c 100644 --- a/InvenTree/build/templates/build/build_base.html +++ b/InvenTree/build/templates/build/build_base.html @@ -230,5 +230,5 @@ src="{% static 'img/blank_image.png' %}" } ); }); - + {% endblock %} \ No newline at end of file diff --git a/InvenTree/build/templates/build/index.html b/InvenTree/build/templates/build/index.html index fbed17bfa3..d5e6484d56 100644 --- a/InvenTree/build/templates/build/index.html +++ b/InvenTree/build/templates/build/index.html @@ -17,9 +17,9 @@
- +
- +
@@ -66,7 +66,7 @@ + + + @@ -174,7 +177,7 @@ $(document).ready(function () { {% endblock %} inventreeDocReady(); - + showCachedAlerts(); {% if barcodes %} diff --git a/InvenTree/templates/clip.html b/InvenTree/templates/clip.html new file mode 100644 index 0000000000..a56ece838c --- /dev/null +++ b/InvenTree/templates/clip.html @@ -0,0 +1,5 @@ +{% load i18n %} + + + + \ No newline at end of file diff --git a/InvenTree/templates/js/barcode.js b/InvenTree/templates/js/barcode.js index 990adc1a0b..8308086afb 100644 --- a/InvenTree/templates/js/barcode.js +++ b/InvenTree/templates/js/barcode.js @@ -75,7 +75,7 @@ function postBarcodeData(barcode_data, options={}) { enableBarcodeInput(modal, true); if (status == 'success') { - + if ('success' in response) { if (options.onScan) { options.onScan(response); @@ -125,7 +125,7 @@ function enableBarcodeInput(modal, enabled=true) { var barcode = $(modal + ' #barcode'); barcode.prop('disabled', !enabled); - + modalEnable(modal, enabled); barcode.focus(); @@ -157,14 +157,14 @@ function barcodeDialog(title, options={}) { var barcode = getBarcodeData(modal); if (barcode && barcode.length > 0) { - + postBarcodeData(barcode, options); } } $(modal).on('shown.bs.modal', function() { $(modal + ' .modal-form-content').scrollTop(0); - + var barcode = $(modal + ' #barcode'); // Handle 'enter' key on barcode @@ -210,10 +210,10 @@ function barcodeDialog(title, options={}) { var content = ''; content += `
{% trans "Scan barcode data below" %}
`; - + content += `
`; content += `
`; - + // Optional content before barcode input content += `
`; content += options.headerContent || ''; @@ -254,14 +254,14 @@ function barcodeScanDialog() { */ var modal = '#modal-form'; - + barcodeDialog( "Scan Barcode", { onScan: function(response) { if ('url' in response) { $(modal).modal('hide'); - + // Redirect to the URL! window.location.href = response.url; } else { @@ -399,7 +399,7 @@ function barcodeCheckIn(location_id, options={}) { break; } } - + if (match) { reloadTable(); } @@ -429,9 +429,9 @@ function barcodeCheckIn(location_id, options={}) { onSubmit: function() { // Called when the 'check-in' button is pressed - + var data = {location: location_id}; - + // Extract 'notes' field data.notes = $(modal + ' #notes').val(); diff --git a/InvenTree/templates/js/bom.js b/InvenTree/templates/js/bom.js index 5b62955499..462db6eba4 100644 --- a/InvenTree/templates/js/bom.js +++ b/InvenTree/templates/js/bom.js @@ -33,7 +33,7 @@ function removeRowFromBomWizard(e) { var colNum = 0; table.find('tr').each(function() { - + colNum++; if (colNum >= 3) { @@ -111,9 +111,9 @@ function loadBomTable(table, options) { if (options.part_detail) { params.part_detail = true; } - + params.sub_part_detail = true; - + var filters = {}; if (!options.disableFilters) { @@ -173,7 +173,7 @@ function loadBomTable(table, options) { // Display an extra icon if this part is an assembly if (sub_part.assembly) { var text = ``; - + html += renderLink(text, `/part/${row.sub_part}/bom/`); } @@ -182,7 +182,7 @@ function loadBomTable(table, options) { } ); - + // Part description cols.push( { @@ -325,7 +325,7 @@ function loadBomTable(table, options) { sortable: true, } ) - + // Part notes cols.push( { @@ -348,18 +348,18 @@ function loadBomTable(table, options) { if (row.part == options.parent_id) { var bValidate = ``; - + var bValid = ``; var bEdit = ``; var bDelt = ``; - + var html = "
"; - + html += bEdit; html += bDelt; - + if (!row.validated) { html += bValidate; } else { @@ -394,7 +394,7 @@ function loadBomTable(table, options) { { success: function(response) { for (var idx = 0; idx < response.length; idx++) { - + response[idx].parentId = bom_pk; if (response[idx].sub_part_detail.assembly) { @@ -412,7 +412,7 @@ function loadBomTable(table, options) { } ) } - + table.inventreeTable({ treeEnable: !options.editable, rootParentId: parent_id, @@ -497,7 +497,7 @@ function loadBomTable(table, options) { var pk = $(this).attr('pk'); var url = `/part/bom/${pk}/delete/`; - + launchModalForm( url, { @@ -509,7 +509,7 @@ function loadBomTable(table, options) { }); table.on('click', '.bom-edit-button', function() { - + var pk = $(this).attr('pk'); var url = `/part/bom/${pk}/edit/`; @@ -524,7 +524,7 @@ function loadBomTable(table, options) { }); table.on('click', '.bom-validate-button', function() { - + var pk = $(this).attr('pk'); var url = `/api/bom/${pk}/validate/`; diff --git a/InvenTree/templates/js/build.js b/InvenTree/templates/js/build.js index d4ceb9c909..0233974741 100644 --- a/InvenTree/templates/js/build.js +++ b/InvenTree/templates/js/build.js @@ -49,7 +49,7 @@ function makeBuildOutputActionButtons(output, buildInfo, lines) { function reloadTable() { $(panel).find(`#allocation-table-${outputId}`).bootstrapTable('refresh'); } - + // Find the div where the buttons will be displayed var buildActions = $(panel).find(`#output-actions-${outputId}`); @@ -82,7 +82,7 @@ function makeBuildOutputActionButtons(output, buildInfo, lines) { //disabled: true } ); - + // Add a button to "delete" the particular build output html += makeIconButton( 'fa-trash-alt icon-red', 'button-output-delete', outputId, @@ -171,7 +171,7 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { var partId = buildInfo.part; var outputId = null; - + if (output) { outputId = output.pk; } else { @@ -179,7 +179,7 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { } var table = options.table; - + if (options.table == null) { table = `#allocation-table-${outputId}`; } @@ -187,7 +187,7 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { // If an "output" is specified, then only "trackable" parts are allocated // Otherwise, only "untrackable" parts are allowed var trackable = ! !output; - + function reloadTable() { // Reload the entire build allocation table $(table).bootstrapTable('refresh'); @@ -492,7 +492,7 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { if (row.stock_item_detail.location) { var text = row.stock_item_detail.location_name; var url = `/stock/location/${row.stock_item_detail.location}/`; - + return renderLink(text, url); } else { return '{% trans "No location set" %}'; @@ -600,7 +600,7 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { qA *= output.quantity; qB *= output.quantity; - + // Handle the case where both numerators are zero if ((aA == 0) && (aB == 0)) { return (qA > qB) ? 1 : -1; @@ -610,7 +610,7 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { if ((qA == 0) || (qB == 0)) { return 1; } - + var progressA = parseFloat(aA) / qA; var progressB = parseFloat(aB) / qB; @@ -618,7 +618,7 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { if (progressA == progressB) { return (qA < qB) ? 1 : -1; } - + return (progressA < progressB) ? 1 : -1; } }, @@ -670,7 +670,7 @@ function loadBuildTable(table, options) { var filters = {}; params['part_detail'] = true; - + if (!options.disableFilters) { filters = loadTableFilters("build"); } @@ -801,11 +801,11 @@ function loadBuildTable(table, options) { function updateAllocationTotal(id, count, required) { - + count = parseFloat(count); - + $('#allocation-total-'+id).html(count); - + var el = $("#allocation-panel-" + id); el.removeClass('part-allocation-pass part-allocation-underallocated part-allocation-overallocated'); @@ -819,7 +819,7 @@ function updateAllocationTotal(id, count, required) { } function loadAllocationTable(table, part_id, part, url, required, button) { - + // Load the allocation table table.bootstrapTable({ url: url, @@ -848,9 +848,9 @@ function loadAllocationTable(table, part_id, part, url, required, button) { var bEdit = ""; var bDel = ""; - + html += "
" + bEdit + bDel + "
"; - + return html; } } @@ -992,7 +992,7 @@ function loadBuildPartsTable(table, options={}) { // Display an extra icon if this part is an assembly if (sub_part.assembly) { var text = ``; - + html += renderLink(text, `/part/${row.sub_part}/bom/`); } diff --git a/InvenTree/templates/js/company.js b/InvenTree/templates/js/company.js index d258c8bab1..be4b707466 100644 --- a/InvenTree/templates/js/company.js +++ b/InvenTree/templates/js/company.js @@ -39,11 +39,11 @@ function loadCompanyTable(table, url, options={}) { if (row.is_customer) { html += ``; } - + if (row.is_manufacturer) { html += ``; } - + if (row.is_supplier) { html += ``; } diff --git a/InvenTree/templates/js/filters.js b/InvenTree/templates/js/filters.js index 612af8e03c..a27e91d5dc 100644 --- a/InvenTree/templates/js/filters.js +++ b/InvenTree/templates/js/filters.js @@ -185,11 +185,11 @@ function getFilterOptionList(tableKey, filterKey) { function generateAvailableFilterList(tableKey) { var remaining = getRemainingTableFilters(tableKey); - + var id = 'filter-tag-' + tableKey.toLowerCase(); var html = `'; + $('.js-modal-form').append(input); + callback(); + }); } @@ -393,11 +402,11 @@ function removeRowFromModalForm(e) { function renderErrorMessage(xhr) { - + var html = '' + xhr.statusText + '
'; - + html += 'Error Code - ' + xhr.status + '

'; - + html += `
@@ -410,7 +419,7 @@ function renderErrorMessage(xhr) {
`; html += xhr.responseText; - + html += `
@@ -590,7 +599,7 @@ function insertNewItemButton(modal, options) { var html = ""; html += "
" + options.title + ""; + html += ""; + + $(modal).find('#modal-footer-buttons').append(html); +} + +function attachButtons(modal, buttons) { + /* Attach a provided list of buttons */ + + for (var i = 0; i < buttons.length; i++) { + insertActionButton(modal, buttons[i]); + } +} + function attachFieldCallback(modal, callback) { /* Attach a 'callback' function to a given field in the modal form. @@ -768,7 +796,7 @@ function handleModalForm(url, options) { }, error: function(xhr, ajaxOptions, thrownError) { // There was an error submitting form data via POST - + $(modal).modal('hide'); showAlertDialog('{% trans "Error posting form data" %}', renderErrorMessage(xhr)); }, @@ -808,6 +836,9 @@ function launchModalForm(url, options = {}) { var submit_text = options.submit_text || '{% trans "Submit" %}'; var close_text = options.close_text || '{% trans "Close" %}'; + // Clean custom action buttons + $(modal).find('#modal-footer-buttons').html(''); + // Form the ajax request to retrieve the django form data ajax_data = { url: url, @@ -852,6 +883,10 @@ function launchModalForm(url, options = {}) { handleModalForm(url, options); } + if (options.buttons) { + attachButtons(modal, options.buttons); + } + } else { $(modal).modal('hide'); showAlertDialog('{% trans "Invalid server response" %}', '{% trans "JSON response missing form data" %}'); diff --git a/InvenTree/templates/js/part.js b/InvenTree/templates/js/part.js index e3e7190952..b7b9058769 100644 --- a/InvenTree/templates/js/part.js +++ b/InvenTree/templates/js/part.js @@ -59,7 +59,7 @@ function makePartIcons(part, options={}) { if (part.is_template) { html += makeIconBadge('fa-clone', '{% trans "Template part" %}'); } - + if (part.assembly) { html += makeIconBadge('fa-tools', '{% trans "Assembled part" %}'); } @@ -71,13 +71,13 @@ function makePartIcons(part, options={}) { if (part.salable) { html += makeIconBadge('fa-dollar-sign', title='{% trans "Salable part" %}'); } - + if (!part.active) { html += `{% trans "Inactive" %}`; } return html; - + } @@ -118,14 +118,14 @@ function loadPartVariantTable(table, partId, options={}) { name += row.IPN; name += ' | '; } - + name += value; - + if (row.revision) { name += ' | '; name += row.revision; } - + if (row.is_template) { name = '' + name + ''; } @@ -144,7 +144,7 @@ function loadPartVariantTable(table, partId, options={}) { if (row.is_template) { html += makeIconBadge('fa-clone', '{% trans "Template part" %}'); } - + if (row.assembly) { html += makeIconBadge('fa-tools', '{% trans "Assembled part" %}'); } @@ -242,7 +242,7 @@ function loadParametricPartTable(table, options={}) { } else { name += row.name; } - + return renderLink(name, '/part/' + row.pk + '/'); } }); @@ -297,7 +297,7 @@ function loadPartTable(table, url, options={}) { var params = options.params || {}; var filters = {}; - + if (!options.disableFilters) { filters = loadTableFilters("parts"); } @@ -359,7 +359,7 @@ function loadPartTable(table, url, options={}) { } var display = imageHoverIcon(row.thumbnail) + renderLink(name, '/part/' + row.pk + '/'); - + display += makePartIcons(row); return display; @@ -378,7 +378,7 @@ function loadPartTable(table, url, options={}) { return value; } }); - + columns.push({ sortable: true, field: 'category_detail', @@ -400,7 +400,7 @@ function loadPartTable(table, url, options={}) { sortable: true, formatter: function(value, row, index, field) { var link = "stock"; - + if (value) { // There IS stock available for this part @@ -421,7 +421,7 @@ function loadPartTable(table, url, options={}) { // There is no stock available value = "0{% trans "No Stock" %}"; } - + return renderLink(value, '/part/' + row.pk + "/" + link + "/"); } }); @@ -596,7 +596,7 @@ function loadPartTestTemplateTable(table, options) { /* * Load PartTestTemplate table. */ - + var params = options.params || {}; var part = options.part || null; @@ -671,7 +671,7 @@ function loadPartTestTemplateTable(table, options) { if (row.part == part) { var html = `
`; - + html += makeIconButton('fa-edit icon-blue', 'button-test-edit', pk, '{% trans "Edit test result" %}'); html += makeIconButton('fa-trash-alt icon-red', 'button-test-delete', pk, '{% trans "Delete test result" %}'); diff --git a/InvenTree/templates/js/report.js b/InvenTree/templates/js/report.js index 741692c56c..370978be04 100644 --- a/InvenTree/templates/js/report.js +++ b/InvenTree/templates/js/report.js @@ -65,7 +65,7 @@ function selectReport(reports, items, options={}) { openModal({ modal: modal, }); - + modalEnable(modal, true); modalSetTitle(modal, '{% trans "Select Test Report Template" %}'); modalSetContent(modal, html); diff --git a/InvenTree/templates/js/stock.js b/InvenTree/templates/js/stock.js index c10e432f5e..4e5d78d8cd 100644 --- a/InvenTree/templates/js/stock.js +++ b/InvenTree/templates/js/stock.js @@ -75,7 +75,7 @@ function loadStockTestResultsTable(table, options) { html += makeIconButton('fa-edit icon-blue', 'button-test-edit', pk, '{% trans "Edit test result" %}'); html += makeIconButton('fa-trash-alt icon-red', 'button-test-delete', pk, '{% trans "Delete test result" %}'); } - + html += "
"; return html; @@ -185,11 +185,11 @@ function loadStockTestResultsTable(table, options) { // Try to associate this result with a test row tableData.forEach(function(row, index) { - - + + // The result matches the test template row if (key == row.key) { - + // Force the names to be the same! item.test_name = row.test_name; item.required = row.required; @@ -250,7 +250,7 @@ function loadStockTable(table, options) { var filters = {}; var filterKey = options.filterKey || options.name || "stock"; - + if (!options.disableFilters) { filters = loadTableFilters(filterKey); } @@ -286,7 +286,7 @@ function loadStockTable(table, options) { // URL (optional) var url = ''; - + if (row.is_building && row.build) { // StockItem is currently being built! text = '{% trans "In production" %}'; @@ -532,7 +532,7 @@ function loadStockTable(table, options) { var name = row.part_detail.full_name; html = imageHoverIcon(thumb) + renderLink(name, url); - + html += makePartIcons(row.part_detail); return html; @@ -574,7 +574,7 @@ function loadStockTable(table, options) { } var html = renderLink(val, `/stock/item/${row.pk}/`); - + if (row.is_building) { html += makeIconBadge('fa-tools', '{% trans "Stock item is in production" %}'); } @@ -848,7 +848,7 @@ function loadStockTable(table, options) { var status_code = label.val(); closeModal(modal); - + if (!status_code) { showAlertDialog( '{% trans "Select Status Code" %}', @@ -1130,7 +1130,7 @@ function createNewStockItem(options) { `/api/part/${value}/`, {}, { success: function(response) { - + // Disable serial number field if the part is not trackable enableField('serial_numbers', response.trackable); clearField('serial_numbers'); @@ -1141,7 +1141,7 @@ function createNewStockItem(options) { clearField('expiry_date'); } else { var expiry = moment().add(response.default_expiry, 'days'); - + setFieldValue('expiry_date', expiry.format("YYYY-MM-DD")); } } @@ -1295,7 +1295,7 @@ function loadInstalledInTable(table, options) { // Add some buttons yo! html += `
`; - + html += makeIconButton('fa-unlink', 'button-uninstall', pk, "{% trans 'Uninstall stock item' %}"); html += `
`; @@ -1343,17 +1343,17 @@ function loadInstalledInTable(table, options) { title: '{% trans "Part" %}', sortable: true, formatter: function(value, row, index, field) { - + var url = `/part/${row.sub_part}/`; var thumb = row.sub_part_detail.thumbnail; var name = row.sub_part_detail.full_name; - + html = imageHoverIcon(thumb) + renderLink(name, url); if (row.not_in_bom) { html = `${html}` } - + return html; } }, @@ -1403,13 +1403,13 @@ function loadInstalledInTable(table, options) { }, { success: function(stock_items) { - + var table_data = table.bootstrapTable('getData'); stock_items.forEach(function(item) { var match = false; - + for (var idx = 0; idx < table_data.length; idx++) { var row = table_data[idx]; @@ -1419,16 +1419,16 @@ function loadInstalledInTable(table, options) { // Match on "sub_part" if (row.sub_part == item.part) { - + // First time? if (row.installed_count == null) { row.installed_count = 0; row.installed_items = []; } - + row.installed_count += item.quantity; row.installed_items.push(item); - + // Push the row back into the table table.bootstrapTable('updateRow', idx, row, true); @@ -1445,7 +1445,7 @@ function loadInstalledInTable(table, options) { if (!match) { // The stock item did *not* match any items in the BOM! // Add a new row to the table... - + // Contruct a new "row" to add to the table var new_row = { sub_part: item.part, diff --git a/InvenTree/templates/js/tables.js b/InvenTree/templates/js/tables.js index 21e673d5ed..26d7ee4da9 100644 --- a/InvenTree/templates/js/tables.js +++ b/InvenTree/templates/js/tables.js @@ -94,7 +94,7 @@ function reloadTable(table, filters) { } options.queryParams = function(tableParams) { - + for (key in params) { tableParams[key] = params[key]; } @@ -145,7 +145,7 @@ $.fn.inventreeTable = function(options) { var filters = options.queryParams || options.filters || {}; options.queryParams = function(params) { - + // Override the way that we ask the server to sort results // It seems bootstrap-table does not offer a "native" way to do this... if ('sort' in params) { @@ -200,7 +200,7 @@ $.fn.inventreeTable = function(options) { // Callback when a column is changed options.onColumnSwitch = function(field, checked) { - + var columns = table.bootstrapTable('getVisibleColumns'); var text = visibleColumnString(columns); @@ -225,7 +225,7 @@ $.fn.inventreeTable = function(options) { if (visible && Array.isArray(visible)) { visible.forEach(function(column) { - + // Visible field should *not* be visible! (hide it!) if (column.switchable && !columns.includes(column.field)) { table.bootstrapTable('hideColumn', column.field); @@ -301,7 +301,7 @@ function customGroupSorter(sortName, sortOrder, sortData) { bb = bb.toString(); var cmp = aa.localeCompare(bb); - + if (cmp === -1) { return order * -1; } else if (cmp === 1) { diff --git a/InvenTree/templates/modal_form.html b/InvenTree/templates/modal_form.html index 94bf032579..b423c23ba5 100644 --- a/InvenTree/templates/modal_form.html +++ b/InvenTree/templates/modal_form.html @@ -20,10 +20,10 @@ {% csrf_token %} {% load crispy_forms_tags %} - + {% block form_data %} {% endblock %} - + {% crispy form %} diff --git a/InvenTree/templates/modals.html b/InvenTree/templates/modals.html index 9850f482c5..e0cae3e580 100644 --- a/InvenTree/templates/modals.html +++ b/InvenTree/templates/modals.html @@ -25,6 +25,7 @@
@@ -49,13 +50,14 @@
- + diff --git a/InvenTree/templates/registration/login.html b/InvenTree/templates/registration/login.html index 2032f62773..3b59dde2fc 100644 --- a/InvenTree/templates/registration/login.html +++ b/InvenTree/templates/registration/login.html @@ -37,7 +37,7 @@ - +
- +
@@ -78,15 +78,15 @@
- + {% if form.errors %}
{% trans "Username / password combination is incorrect" %}
{% endif %} - +
- + diff --git a/InvenTree/templates/registration/password_reset_done.html b/InvenTree/templates/registration/password_reset_done.html index 17a786f2a4..03d98846a0 100644 --- a/InvenTree/templates/registration/password_reset_done.html +++ b/InvenTree/templates/registration/password_reset_done.html @@ -62,5 +62,5 @@ - + \ No newline at end of file diff --git a/InvenTree/templates/two_column.html b/InvenTree/templates/two_column.html index 76521ea367..ce91dcfffb 100644 --- a/InvenTree/templates/two_column.html +++ b/InvenTree/templates/two_column.html @@ -11,7 +11,7 @@ {% block header_panel %}
- + {% block header_pre_content %} {% endblock %} diff --git a/InvenTree/users/admin.py b/InvenTree/users/admin.py index d8406bfddd..91fed49830 100644 --- a/InvenTree/users/admin.py +++ b/InvenTree/users/admin.py @@ -127,7 +127,7 @@ class RoleGroupAdmin(admin.ModelAdmin): if rule_set.can_delete: permission_level = append_permission_level(permission_level, 'D') - + return permission_level def admin(self, obj): diff --git a/InvenTree/users/models.py b/InvenTree/users/models.py index cd222cb2a2..f26a7ee64b 100644 --- a/InvenTree/users/models.py +++ b/InvenTree/users/models.py @@ -185,7 +185,7 @@ class RuleSet(models.Model): can_change = models.BooleanField(verbose_name=_('Change'), default=False, help_text=_('Permissions to edit items')) can_delete = models.BooleanField(verbose_name=_('Delete'), default=False, help_text=_('Permission to delete items')) - + @classmethod def check_table_permission(cls, user, table, permission): """ @@ -373,7 +373,7 @@ def update_group_roles(group, debug=False): # Add any required permissions to the group for perm in permissions_to_add: - + # Ignore if permission is already in the group if perm in group_permissions: continue @@ -541,7 +541,7 @@ class Owner(models.Model): pass return owner - + return owner def get_related_owners(self, include_group=False): @@ -562,7 +562,7 @@ class Owner(models.Model): Q(owner_id=self.owner.id, owner_type=ContentType.objects.get_for_model(Group).id) else: query = Q(owner_id__in=users, owner_type=ContentType.objects.get_for_model(user_model).id) - + related_owners = Owner.objects.filter(query) elif type(self.owner) is user_model: diff --git a/InvenTree/users/test_migrations.py b/InvenTree/users/test_migrations.py index 7e4c24b2dc..7bb17d0070 100644 --- a/InvenTree/users/test_migrations.py +++ b/InvenTree/users/test_migrations.py @@ -24,7 +24,7 @@ class TestForwardMigrations(MigratorTestCase): email='fred@fred.com', password='password' ) - + User.objects.create( username='brad', email='brad@fred.com', diff --git a/InvenTree/users/tests.py b/InvenTree/users/tests.py index 895d0a84af..e8ec6b3c74 100644 --- a/InvenTree/users/tests.py +++ b/InvenTree/users/tests.py @@ -17,7 +17,7 @@ class RuleSetModelTest(TestCase): def test_ruleset_models(self): keys = RuleSet.RULESET_MODELS.keys() - + # Check if there are any rulesets which do not have models defined missing = [name for name in RuleSet.RULESET_NAMES if name not in keys] @@ -88,7 +88,7 @@ class RuleSetModelTest(TestCase): extra_models = set() defined_models = set() - + for model in assigned_models: defined_models.add(model) @@ -198,7 +198,7 @@ class OwnerModelTest(TestCase): self.user.delete() user_as_owner = Owner.get_owner(self.user) self.assertEqual(user_as_owner, None) - + # Delete group and verify owner was deleted too self.group.delete() group_as_owner = Owner.get_owner(self.group) diff --git a/docker/nginx.conf b/docker/nginx.conf index a9eff10fc5..754a7321bb 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -10,17 +10,17 @@ server { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $host; - + proxy_redirect off; - + client_max_body_size 100M; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-Proto $scheme; - + proxy_buffering off; proxy_request_buffering off; - + } # Redirect any requests for static files diff --git a/tasks.py b/tasks.py index 3065d97243..88bd5e42e4 100644 --- a/tasks.py +++ b/tasks.py @@ -65,7 +65,7 @@ def manage(c, cmd, pty=False): cmd - django command to run """ - c.run('cd {path} && python3 manage.py {cmd}'.format( + c.run('cd "{path}" && python3 manage.py {cmd}'.format( path=managePyDir(), cmd=cmd ), pty=pty) @@ -185,7 +185,7 @@ def translate(c): """ # Translate applicable .py / .html / .js files - manage(c, "makemessages --all -e py,html,js") + manage(c, "makemessages --all -e py,html,js --no-wrap") manage(c, "compilemessages") path = os.path.join('InvenTree', 'script', 'translation_stats.py') @@ -295,7 +295,7 @@ def export_records(c, filename='data.json'): for entry in data: if "model" in entry: - + # Clear out any permissions specified for a group if entry["model"] == "auth.group": entry["fields"]["permissions"] = [] @@ -335,7 +335,7 @@ def import_records(c, filename='data.json'): for entry in data: if "model" in entry: - + # Clear out any permissions specified for a group if entry["model"] == "auth.group": entry["fields"]["permissions"] = [] @@ -370,7 +370,7 @@ def import_fixtures(c): fixtures = [ # Build model 'build', - + # Common models 'settings',