From f2b0717d10db0b831d6a8045fed3aa4febbdf18c Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 6 May 2021 12:11:38 +0200 Subject: [PATCH 1/7] removes all lines consisting only of spaces this really bothers me for some reason - nothing technical --- InvenTree/InvenTree/api_tester.py | 2 +- InvenTree/InvenTree/context.py | 2 +- InvenTree/InvenTree/exchange.py | 2 +- InvenTree/InvenTree/fields.py | 2 +- InvenTree/InvenTree/helpers.py | 24 ++--- InvenTree/InvenTree/models.py | 2 +- InvenTree/InvenTree/permissions.py | 2 +- InvenTree/InvenTree/serializers.py | 2 +- InvenTree/InvenTree/status.py | 2 +- InvenTree/InvenTree/status_codes.py | 2 +- InvenTree/InvenTree/test_api.py | 2 +- InvenTree/InvenTree/test_tasks.py | 2 +- InvenTree/InvenTree/tests.py | 6 +- InvenTree/InvenTree/urls.py | 4 +- InvenTree/InvenTree/validators.py | 2 +- InvenTree/InvenTree/views.py | 24 ++--- InvenTree/barcodes/api.py | 8 +- InvenTree/barcodes/barcode.py | 2 +- InvenTree/barcodes/tests.py | 4 +- InvenTree/build/api.py | 4 +- InvenTree/build/models.py | 28 ++--- InvenTree/build/test_api.py | 4 +- InvenTree/build/test_build.py | 10 +- InvenTree/build/tests.py | 16 +-- InvenTree/build/views.py | 40 +++---- InvenTree/common/admin.py | 2 +- InvenTree/common/models.py | 12 +-- InvenTree/common/settings.py | 2 +- InvenTree/common/test_views.py | 2 +- InvenTree/common/tests.py | 2 +- InvenTree/common/views.py | 2 +- InvenTree/company/api.py | 10 +- InvenTree/company/forms.py | 2 +- InvenTree/company/models.py | 20 ++-- InvenTree/company/serializers.py | 6 +- InvenTree/company/test_migrations.py | 12 +-- InvenTree/company/test_views.py | 12 +-- InvenTree/company/tests.py | 10 +- InvenTree/company/urls.py | 2 +- InvenTree/company/views.py | 22 ++-- InvenTree/label/api.py | 6 +- InvenTree/label/apps.py | 2 +- InvenTree/label/models.py | 4 +- InvenTree/label/test_api.py | 2 +- InvenTree/order/forms.py | 2 +- InvenTree/order/models.py | 18 ++-- InvenTree/order/serializers.py | 16 +-- InvenTree/order/test_api.py | 4 +- InvenTree/order/test_sales_order.py | 8 +- InvenTree/order/test_views.py | 10 +- InvenTree/order/tests.py | 6 +- InvenTree/order/views.py | 50 ++++----- InvenTree/part/admin.py | 6 +- InvenTree/part/api.py | 26 ++--- InvenTree/part/apps.py | 2 +- InvenTree/part/bom.py | 22 ++-- InvenTree/part/forms.py | 6 +- InvenTree/part/models.py | 46 ++++---- InvenTree/part/serializers.py | 8 +- .../part/templatetags/inventree_extras.py | 6 +- InvenTree/part/test_api.py | 14 +-- InvenTree/part/test_bom_item.py | 6 +- InvenTree/part/test_category.py | 6 +- InvenTree/part/test_migrations.py | 2 +- InvenTree/part/test_param.py | 2 +- InvenTree/part/test_part.py | 6 +- InvenTree/part/test_views.py | 14 +-- InvenTree/part/urls.py | 12 +-- InvenTree/part/views.py | 102 +++++++++--------- InvenTree/plugins/action/action.py | 4 +- InvenTree/report/api.py | 18 ++-- InvenTree/report/models.py | 6 +- InvenTree/report/templatetags/barcode.py | 2 +- InvenTree/report/templatetags/report.py | 2 +- InvenTree/script/translate.py | 2 +- InvenTree/script/translation_stats.py | 2 +- InvenTree/stock/admin.py | 4 +- InvenTree/stock/api.py | 30 +++--- InvenTree/stock/forms.py | 24 ++--- InvenTree/stock/models.py | 24 ++--- InvenTree/stock/serializers.py | 4 +- InvenTree/stock/test_api.py | 16 +-- InvenTree/stock/test_views.py | 12 +-- InvenTree/stock/tests.py | 6 +- InvenTree/stock/urls.py | 2 +- InvenTree/stock/views.py | 76 ++++++------- InvenTree/users/admin.py | 2 +- InvenTree/users/models.py | 8 +- InvenTree/users/test_migrations.py | 2 +- InvenTree/users/tests.py | 6 +- tasks.py | 6 +- 91 files changed, 494 insertions(+), 494 deletions(-) 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/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/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..622d1511ec 100644 --- a/InvenTree/InvenTree/validators.py +++ b/InvenTree/InvenTree/validators.py @@ -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/test_api.py b/InvenTree/build/test_api.py index 02bcde6bb4..a1d0c3df9f 100644 --- a/InvenTree/build/test_api.py +++ b/InvenTree/build/test_api.py @@ -30,7 +30,7 @@ class BuildAPITest(InvenTreeAPITestCase): 'build.change', 'build.add' ] - + def setUp(self): super().setUp() @@ -54,7 +54,7 @@ class BuildListTest(BuildAPITest): builds = self.get(self.url, data={'active': True}) self.assertEqual(len(builds.data), 1) - + builds = self.get(self.url, data={'status': BuildStatus.COMPLETE}) self.assertEqual(len(builds.data), 4) diff --git a/InvenTree/build/test_build.py b/InvenTree/build/test_build.py index 0beab60d52..a3b69646dd 100644 --- a/InvenTree/build/test_build.py +++ b/InvenTree/build/test_build.py @@ -114,7 +114,7 @@ class BuildTest(TestCase): # Perform some basic tests before we start the ball rolling self.assertEqual(StockItem.objects.count(), 6) - + # Build is PENDING self.assertEqual(self.build.status, status.BuildStatus.PENDING) @@ -142,7 +142,7 @@ class BuildTest(TestCase): # Create a BuiltItem which points to an invalid StockItem b = BuildItem(stock_item=stock, build=self.build, quantity=10) - + with self.assertRaises(ValidationError): b.save() @@ -339,7 +339,7 @@ class BuildTest(TestCase): self.assertTrue(self.build.can_complete) self.build.complete_build(None) - + self.assertEqual(self.build.status, status.BuildStatus.COMPLETE) # the original BuildItem objects should have been deleted! @@ -351,12 +351,12 @@ class BuildTest(TestCase): # This stock item has been depleted! with self.assertRaises(StockItem.DoesNotExist): StockItem.objects.get(pk=self.stock_1_1.pk) - + # This stock item has *not* been depleted x = StockItem.objects.get(pk=self.stock_2_1.pk) self.assertEqual(x.quantity, 4970) - + # And 10 new stock items created for the build output outputs = StockItem.objects.filter(build=self.build) diff --git a/InvenTree/build/tests.py b/InvenTree/build/tests.py index e16ef9a282..9a440e0b93 100644 --- a/InvenTree/build/tests.py +++ b/InvenTree/build/tests.py @@ -251,7 +251,7 @@ class TestBuildViews(TestCase): content = str(response.content) self.assertIn(build.title, content) - + def test_build_create(self): """ Test the build creation view (ajax form) """ @@ -260,7 +260,7 @@ class TestBuildViews(TestCase): # Create build without specifying part response = self.client.get(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest') self.assertEqual(response.status_code, 200) - + # Create build with valid part response = self.client.get(url, {'part': 1}, HTTP_X_REQUESTED_WITH='XMLHttpRequest') self.assertEqual(response.status_code, 200) @@ -281,7 +281,7 @@ class TestBuildViews(TestCase): # Get the page in editing mode response = self.client.get(url, {'edit': 1}) self.assertEqual(response.status_code, 200) - + def test_build_item_create(self): """ Test the BuildItem creation view (ajax form) """ @@ -305,7 +305,7 @@ class TestBuildViews(TestCase): def test_build_item_edit(self): """ Test the BuildItem edit view (ajax form) """ - + # TODO # url = reverse('build-item-edit') pass @@ -323,7 +323,7 @@ class TestBuildViews(TestCase): # Test without confirmation response = self.client.post(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest') self.assertEqual(response.status_code, 200) - + data = json.loads(response.content) self.assertFalse(data['form_valid']) @@ -353,7 +353,7 @@ class TestBuildViews(TestCase): # Test with confirmation, invalid location response = self.client.post(url, {'confirm': 1, 'location': 9999}, HTTP_X_REQUESTED_WITH='XMLHttpRequest') self.assertEqual(response.status_code, 200) - + data = json.loads(response.content) self.assertFalse(data['form_valid']) @@ -365,7 +365,7 @@ class TestBuildViews(TestCase): # Test without confirmation response = self.client.post(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest') self.assertEqual(response.status_code, 200) - + data = json.loads(response.content) self.assertFalse(data['form_valid']) @@ -393,7 +393,7 @@ class TestBuildViews(TestCase): data = json.loads(response.content) self.assertFalse(data['form_valid']) - + # Test with confirmation response = self.client.post(url, {'confirm': 1}, HTTP_X_REQUESTED_WITH='XMLHttpRequest') self.assertEqual(response.status_code, 200) diff --git a/InvenTree/build/views.py b/InvenTree/build/views.py index 66815cae70..6e72f7f3e6 100644 --- a/InvenTree/build/views.py +++ b/InvenTree/build/views.py @@ -159,7 +159,7 @@ class BuildOutputCreate(AjaxUpdateView): if quantity: build = self.get_object() - + # Check that requested output don't exceed build remaining quantity maximum_output = int(build.remaining - build.incomplete_count) if quantity > maximum_output: @@ -318,7 +318,7 @@ class BuildUnallocate(AjaxUpdateView): form_class = forms.UnallocateBuildForm ajax_form_title = _("Unallocate Stock") ajax_template_name = "build/unallocate.html" - + def get_initial(self): initials = super().get_initial() @@ -341,7 +341,7 @@ class BuildUnallocate(AjaxUpdateView): build = self.get_object() form = self.get_form() - + confirm = request.POST.get('confirm', False) output_id = request.POST.get('output_id', None) @@ -382,7 +382,7 @@ class BuildUnallocate(AjaxUpdateView): # Unallocate "untracked" parts else: build.unallocateUntracked(part=part) - + data = { 'form_valid': valid, } @@ -401,7 +401,7 @@ class BuildComplete(AjaxUpdateView): model = Build form_class = forms.CompleteBuildForm - + ajax_form_title = _('Complete Build Order') ajax_template_name = 'build/complete.html' @@ -437,9 +437,9 @@ class BuildOutputComplete(AjaxUpdateView): context_object_name = "build" ajax_form_title = _("Complete Build Output") ajax_template_name = "build/complete_output.html" - + def get_form(self): - + build = self.get_object() form = super().get_form() @@ -500,7 +500,7 @@ class BuildOutputComplete(AjaxUpdateView): - If the part being built has a default location, pre-select that location """ - + initials = super().get_initial() build = self.get_object() @@ -585,7 +585,7 @@ class BuildOutputComplete(AjaxUpdateView): location=location, status=stock_status, ) - + def get_data(self): """ Provide feedback data back to the form """ return { @@ -600,7 +600,7 @@ class BuildNotes(InvenTreeRoleMixin, UpdateView): context_object_name = 'build' template_name = 'build/notes.html' model = Build - + # Override the default permission role for this View role_required = 'build.view' @@ -612,7 +612,7 @@ class BuildNotes(InvenTreeRoleMixin, UpdateView): def get_context_data(self, **kwargs): ctx = super().get_context_data(**kwargs) - + ctx['editing'] = str2bool(self.request.GET.get('edit', '')) return ctx @@ -746,7 +746,7 @@ class BuildCreate(AjaxCreateView): class BuildUpdate(AjaxUpdateView): """ View for editing a Build object """ - + model = Build form_class = forms.EditBuildForm context_object_name = 'build' @@ -804,7 +804,7 @@ class BuildItemDelete(AjaxDeleteView): ajax_template_name = 'build/delete_build_item.html' ajax_form_title = _('Unallocate Stock') context_object_name = 'item' - + def get_data(self): return { 'danger': _('Removed parts from build allocation') @@ -826,7 +826,7 @@ class BuildItemCreate(AjaxCreateView): # The "part" which is being allocated to the output part = None - + available_stock = None def get_context_data(self): @@ -906,7 +906,7 @@ class BuildItemCreate(AjaxCreateView): if part_id: try: self.part = Part.objects.get(pk=part_id) - + except (ValueError, Part.DoesNotExist): pass @@ -958,7 +958,7 @@ class BuildItemCreate(AjaxCreateView): # Reference to a StockItem object item = None - + # Reference to a Build object build = None @@ -999,7 +999,7 @@ class BuildItemCreate(AjaxCreateView): quantity = float(quantity) elif required_quantity is not None: quantity = required_quantity - + item_id = self.get_param('item') # If the request specifies a particular StockItem @@ -1035,7 +1035,7 @@ class BuildItemEdit(AjaxUpdateView): ajax_template_name = 'build/edit_build_item.html' form_class = forms.EditBuildItemForm ajax_form_title = _('Edit Stock Allocation') - + def get_data(self): return { 'info': _('Updated Build Item'), @@ -1068,7 +1068,7 @@ class BuildAttachmentCreate(AjaxCreateView): model = BuildOrderAttachment form_class = forms.EditBuildAttachmentForm ajax_form_title = _('Add Build Order Attachment') - + def save(self, form, **kwargs): """ Add information on the user that uploaded the attachment @@ -1105,7 +1105,7 @@ class BuildAttachmentCreate(AjaxCreateView): form = super().get_form() form.fields['build'].widget = HiddenInput() - + return form diff --git a/InvenTree/common/admin.py b/InvenTree/common/admin.py index 3edcd1fa8d..c2da1ddd63 100644 --- a/InvenTree/common/admin.py +++ b/InvenTree/common/admin.py @@ -9,7 +9,7 @@ from .models import InvenTreeSetting class SettingsAdmin(ImportExportModelAdmin): - + list_display = ('key', 'value') diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index bc2ca4214b..356e58e8e9 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -40,7 +40,7 @@ class InvenTreeSetting(models.Model): The key of each item is the name of the value as it appears in the database. Each global setting has the following parameters: - + - name: Translatable string name of the setting (required) - description: Translatable string description of the setting (required) - default: Default value (optional) @@ -412,7 +412,7 @@ class InvenTreeSetting(models.Model): # Evaluate the function (we expect it will return a list of tuples...) return choices() """ - + return choices @classmethod @@ -522,7 +522,7 @@ class InvenTreeSetting(models.Model): # Enforce standard boolean representation if setting.is_bool(): value = InvenTree.helpers.str2bool(value) - + setting.value = str(value) setting.save() @@ -664,7 +664,7 @@ class InvenTreeSetting(models.Model): if validator == int: return True - + if type(validator) in [list, tuple]: for v in validator: if v == int: @@ -675,7 +675,7 @@ class InvenTreeSetting(models.Model): def as_int(self): """ Return the value of this setting converted to a boolean value. - + If an error occurs, return the default value """ @@ -685,7 +685,7 @@ class InvenTreeSetting(models.Model): value = self.default_value() return value - + class PriceBreak(models.Model): """ diff --git a/InvenTree/common/settings.py b/InvenTree/common/settings.py index 134d3f3f7c..4d98bc495b 100644 --- a/InvenTree/common/settings.py +++ b/InvenTree/common/settings.py @@ -19,7 +19,7 @@ def currency_code_default(): if code not in CURRENCIES: code = 'USD' - + return code diff --git a/InvenTree/common/test_views.py b/InvenTree/common/test_views.py index 0cd902d083..8dc5830108 100644 --- a/InvenTree/common/test_views.py +++ b/InvenTree/common/test_views.py @@ -117,7 +117,7 @@ class SettingsViewTest(TestCase): """ Test for binary value """ - + setting = InvenTreeSetting.get_setting_object('PART_COMPONENT') self.assertTrue(setting.as_bool()) diff --git a/InvenTree/common/tests.py b/InvenTree/common/tests.py index d8777785f9..d20f76baa0 100644 --- a/InvenTree/common/tests.py +++ b/InvenTree/common/tests.py @@ -19,7 +19,7 @@ class SettingsTest(TestCase): def setUp(self): user = get_user_model() - + self.user = user.objects.create_user('username', 'user@email.com', 'password') self.user.is_staff = True self.user.save() diff --git a/InvenTree/common/views.py b/InvenTree/common/views.py index 31d11e30cc..8cc344c9ab 100644 --- a/InvenTree/common/views.py +++ b/InvenTree/common/views.py @@ -48,7 +48,7 @@ class SettingEdit(AjaxUpdateView): """ form = super().get_form() - + setting = self.get_object() choices = setting.choices() diff --git a/InvenTree/company/api.py b/InvenTree/company/api.py index 494b3652b2..ff8b6d667b 100644 --- a/InvenTree/company/api.py +++ b/InvenTree/company/api.py @@ -41,7 +41,7 @@ class CompanyList(generics.ListCreateAPIView): queryset = CompanySerializer.annotate_queryset(queryset) return queryset - + filter_backends = [ DjangoFilterBackend, filters.SearchFilter, @@ -116,7 +116,7 @@ class ManufacturerPartList(generics.ListCreateAPIView): kwargs['pretty'] = str2bool(self.request.query_params.get('pretty', None)) except AttributeError: pass - + kwargs['context'] = self.get_serializer_context() return self.serializer_class(*args, **kwargs) @@ -167,7 +167,7 @@ class ManufacturerPartList(generics.ListCreateAPIView): 'part__name', 'part__description', ] - + class ManufacturerPartDetail(generics.RetrieveUpdateDestroyAPIView): """ API endpoint for detail view of ManufacturerPart object @@ -255,7 +255,7 @@ class SupplierPartList(generics.ListCreateAPIView): kwargs['part_detail'] = str2bool(self.request.query_params.get('part_detail', None)) except AttributeError: pass - + try: kwargs['supplier_detail'] = str2bool(self.request.query_params.get('supplier_detail', None)) except AttributeError: @@ -270,7 +270,7 @@ class SupplierPartList(generics.ListCreateAPIView): kwargs['pretty'] = str2bool(self.request.query_params.get('pretty', None)) except AttributeError: pass - + kwargs['context'] = self.get_serializer_context() return self.serializer_class(*args, **kwargs) diff --git a/InvenTree/company/forms.py b/InvenTree/company/forms.py index 8ad8c6bfea..62a46fcd6f 100644 --- a/InvenTree/company/forms.py +++ b/InvenTree/company/forms.py @@ -158,7 +158,7 @@ class EditSupplierPartForm(HelperForm): empty_choice = [('', '----------')] manufacturers = [(manufacturer.id, manufacturer.name) for manufacturer in Company.objects.filter(is_manufacturer=True)] - + return empty_choice + manufacturers def __init__(self, *args, **kwargs): diff --git a/InvenTree/company/models.py b/InvenTree/company/models.py index 89a3f6c9bf..8b6a45556e 100644 --- a/InvenTree/company/models.py +++ b/InvenTree/company/models.py @@ -152,7 +152,7 @@ class Company(models.Model): def currency_code(self): """ Return the currency code associated with this company. - + - If the currency code is invalid, use the default currency - If the currency code is not specified, use the default currency """ @@ -187,7 +187,7 @@ class Company(models.Model): return getMediaUrl(self.image.thumbnail.url) else: return getBlankThumbnail() - + @property def manufactured_part_count(self): """ The number of parts manufactured by this company """ @@ -302,7 +302,7 @@ class ManufacturerPart(models.Model): class Meta: unique_together = ('part', 'manufacturer', 'MPN') - + part = models.ForeignKey('part.Part', on_delete=models.CASCADE, related_name='manufacturer_parts', verbose_name=_('Base Part'), @@ -311,7 +311,7 @@ class ManufacturerPart(models.Model): }, help_text=_('Select part'), ) - + manufacturer = models.ForeignKey( Company, on_delete=models.CASCADE, @@ -359,7 +359,7 @@ class ManufacturerPart(models.Model): if not manufacturer_part: manufacturer_part = ManufacturerPart(part=part, manufacturer=manufacturer, MPN=mpn, description=description, link=link) manufacturer_part.save() - + return manufacturer_part def __str__(self): @@ -414,7 +414,7 @@ class SupplierPart(models.Model): MPN = kwargs.pop('MPN') else: MPN = None - + if manufacturer or MPN: if not self.manufacturer_part: # Create ManufacturerPart @@ -429,7 +429,7 @@ class SupplierPart(models.Model): manufacturer_part_id = self.manufacturer_part.id except AttributeError: manufacturer_part_id = None - + if manufacturer_part_id: try: (manufacturer_part, created) = ManufacturerPart.objects.update_or_create(part=self.part, @@ -504,7 +504,7 @@ class SupplierPart(models.Model): base_cost = models.DecimalField(max_digits=10, decimal_places=3, default=0, validators=[MinValueValidator(0)], verbose_name=_('base cost'), help_text=_('Minimum charge (e.g. stocking fee)')) packaging = models.CharField(max_length=50, blank=True, null=True, verbose_name=_('Packaging'), help_text=_('Part packaging')) - + multiple = models.PositiveIntegerField(default=1, validators=[MinValueValidator(1)], verbose_name=_('multiple'), help_text=_('Order multiple')) # TODO - Reimplement lead-time as a charfield with special validation (pattern matching). @@ -613,7 +613,7 @@ class SupplierPart(models.Model): pb_cost = pb_min.convert_to(currency) # Trigger cost calculation using smallest price break pb_found = True - + # Convert quantity to decimal.Decimal format quantity = decimal.Decimal(f'{quantity}') @@ -669,7 +669,7 @@ class SupplierPart(models.Model): if self.manufacturer_string: s = s + ' | ' + self.manufacturer_string - + return s diff --git a/InvenTree/company/serializers.py b/InvenTree/company/serializers.py index 335a351583..4b1019656e 100644 --- a/InvenTree/company/serializers.py +++ b/InvenTree/company/serializers.py @@ -51,7 +51,7 @@ class CompanySerializer(InvenTreeModelSerializer): return queryset url = serializers.CharField(source='get_absolute_url', read_only=True) - + image = serializers.CharField(source='get_thumbnail_url', read_only=True) parts_supplied = serializers.IntegerField(read_only=True) @@ -157,9 +157,9 @@ class SupplierPartSerializer(InvenTreeModelSerializer): self.fields.pop('pretty_name') supplier = serializers.PrimaryKeyRelatedField(queryset=Company.objects.filter(is_supplier=True)) - + manufacturer = serializers.PrimaryKeyRelatedField(source='manufacturer_part.manufacturer', read_only=True) - + MPN = serializers.StringRelatedField(source='manufacturer_part.MPN') manufacturer_part = ManufacturerPartSerializer(read_only=True) diff --git a/InvenTree/company/test_migrations.py b/InvenTree/company/test_migrations.py index bf6e212f7a..882d54260f 100644 --- a/InvenTree/company/test_migrations.py +++ b/InvenTree/company/test_migrations.py @@ -48,7 +48,7 @@ class TestManufacturerField(MigratorTestCase): - Company object (supplier) - SupplierPart object """ - + Part = self.old_state.apps.get_model('part', 'part') Company = self.old_state.apps.get_model('company', 'company') SupplierPart = self.old_state.apps.get_model('company', 'supplierpart') @@ -123,7 +123,7 @@ class TestManufacturerPart(MigratorTestCase): - Company object (supplier) - SupplierPart object """ - + Part = self.old_state.apps.get_model('part', 'part') Company = self.old_state.apps.get_model('company', 'company') SupplierPart = self.old_state.apps.get_model('company', 'supplierpart') @@ -220,7 +220,7 @@ class TestManufacturerPart(MigratorTestCase): # Check on the SupplierPart objects SupplierPart = self.new_state.apps.get_model('company', 'supplierpart') - + supplier_parts = SupplierPart.objects.all() self.assertEqual(supplier_parts.count(), 6) @@ -229,10 +229,10 @@ class TestManufacturerPart(MigratorTestCase): # Check on the ManufacturerPart objects ManufacturerPart = self.new_state.apps.get_model('company', 'manufacturerpart') - + manufacturer_parts = ManufacturerPart.objects.all() self.assertEqual(manufacturer_parts.count(), 4) - + manufacturer_part = manufacturer_parts.first() self.assertEqual(manufacturer_part.MPN, 'MUR-CAP-123456') @@ -293,7 +293,7 @@ class TestCurrencyMigration(MigratorTestCase): self.assertIsNone(pb.price) def test_currency_migration(self): - + PB = self.new_state.apps.get_model('company', 'supplierpricebreak') for pb in PB.objects.all(): diff --git a/InvenTree/company/test_views.py b/InvenTree/company/test_views.py index e6eb54e0bf..cdb2d32af9 100644 --- a/InvenTree/company/test_views.py +++ b/InvenTree/company/test_views.py @@ -30,7 +30,7 @@ class CompanyViewTestBase(TestCase): # Create a user user = get_user_model() - + self.user = user.objects.create_user( username='username', email='user@email.com', @@ -83,7 +83,7 @@ class SupplierPartViewTests(CompanyViewTestBase): def test_supplier_part_create(self): """ Test the SupplierPartCreate view. - + This view allows some additional functionality, specifically it allows the user to create a single-quantity price break automatically, when saving the new SupplierPart model. @@ -171,7 +171,7 @@ class SupplierPartViewTests(CompanyViewTestBase): 'confirm_delete': True }, HTTP_X_REQUESTED_WITH='XMLHttpRequest') - + self.assertEqual(response.status_code, 200) self.assertEqual(n - 2, SupplierPart.objects.count()) @@ -213,7 +213,7 @@ class ManufacturerPartViewTests(CompanyViewTestBase): """ Test the ManufacturerPartCreate view. """ - + url = reverse('manufacturer-part-create') # First check that we can GET the form @@ -252,7 +252,7 @@ class ManufacturerPartViewTests(CompanyViewTestBase): """ Test that the SupplierPartCreate view creates Manufacturer Part. """ - + url = reverse('supplier-part-create') # First check that we can GET the form @@ -297,7 +297,7 @@ class ManufacturerPartViewTests(CompanyViewTestBase): 'confirm_delete': True }, HTTP_X_REQUESTED_WITH='XMLHttpRequest') - + self.assertEqual(response.status_code, 200) # Check that the ManufacturerPart was deleted diff --git a/InvenTree/company/tests.py b/InvenTree/company/tests.py index ec56503815..5dd3bf81ab 100644 --- a/InvenTree/company/tests.py +++ b/InvenTree/company/tests.py @@ -71,7 +71,7 @@ class CompanySimpleTest(TestCase): acme = Company.objects.get(pk=1) appel = Company.objects.get(pk=2) zerg = Company.objects.get(pk=3) - + self.assertTrue(acme.has_parts) self.assertEqual(acme.supplied_part_count, 4) @@ -82,7 +82,7 @@ class CompanySimpleTest(TestCase): self.assertEqual(zerg.supplied_part_count, 2) def test_price_breaks(self): - + self.assertTrue(self.acme0001.has_price_breaks) self.assertTrue(self.acme0002.has_price_breaks) self.assertTrue(self.zergm312.has_price_breaks) @@ -121,7 +121,7 @@ class CompanySimpleTest(TestCase): pmin, pmax = m2x4.get_price_range(5) self.assertEqual(pmin, 35) self.assertEqual(pmax, 37.5) - + m3x12 = Part.objects.get(name='M3x12 SHCS') self.assertEqual(m3x12.get_price_info(0.3), Decimal('2.4')) @@ -187,14 +187,14 @@ class ManufacturerPartSimpleTest(TestCase): # Create a manufacturer part self.part = Part.objects.get(pk=1) manufacturer = Company.objects.get(pk=1) - + self.mp = ManufacturerPart.create( part=self.part, manufacturer=manufacturer, mpn='PART_NUMBER', description='THIS IS A MANUFACTURER PART', ) - + # Create a supplier part supplier = Company.objects.get(pk=5) supplier_part = SupplierPart.objects.create( diff --git a/InvenTree/company/urls.py b/InvenTree/company/urls.py index b87b0626ae..11f1be5339 100644 --- a/InvenTree/company/urls.py +++ b/InvenTree/company/urls.py @@ -55,7 +55,7 @@ price_break_urls = [ manufacturer_part_detail_urls = [ url(r'^edit/?', views.ManufacturerPartEdit.as_view(), name='manufacturer-part-edit'), - + url(r'^suppliers/', views.ManufacturerPartDetail.as_view(template_name='company/manufacturer_part_suppliers.html'), name='manufacturer-part-suppliers'), url('^.*$', views.ManufacturerPartDetail.as_view(template_name='company/manufacturer_part_suppliers.html'), name='manufacturer-part-detail'), diff --git a/InvenTree/company/views.py b/InvenTree/company/views.py index be7d326c36..120832a31c 100644 --- a/InvenTree/company/views.py +++ b/InvenTree/company/views.py @@ -96,7 +96,7 @@ class CompanyIndex(InvenTreeRoleMixin, ListView): if self.request.path == item: context = lookup[item] break - + if context is None: context = default @@ -279,7 +279,7 @@ class CompanyCreate(AjaxCreateView): if url == reverse('supplier-create'): return _("Create new Supplier") - + if url == reverse('manufacturer-create'): return _('Create new Manufacturer') @@ -298,7 +298,7 @@ class CompanyCreate(AjaxCreateView): initials['is_supplier'] = True initials['is_customer'] = False initials['is_manufacturer'] = False - + elif url == reverse('manufacturer-create'): initials['is_manufacturer'] = True initials['is_supplier'] = True @@ -319,7 +319,7 @@ class CompanyCreate(AjaxCreateView): class CompanyDelete(AjaxDeleteView): """ View for deleting a Company object """ - + model = Company success_url = '/company/' ajax_template_name = 'company/delete.html' @@ -415,7 +415,7 @@ class ManufacturerPartCreate(AjaxCreateView): initials['manufacturer'] = Company.objects.get(pk=manufacturer_id) except (ValueError, Company.DoesNotExist): pass - + if part_id: try: initials['part'] = Part.objects.get(pk=part_id) @@ -427,7 +427,7 @@ class ManufacturerPartCreate(AjaxCreateView): class ManufacturerPartDelete(AjaxDeleteView): """ Delete view for removing a ManufacturerPart. - + ManufacturerParts can be deleted using a variety of 'selectors'. - ?part= -> Delete a single ManufacturerPart object @@ -561,7 +561,7 @@ class SupplierPartEdit(AjaxUpdateView): initials = super(SupplierPartEdit, self).get_initial().copy() supplier_part = self.get_object() - + if supplier_part.manufacturer_part: initials['manufacturer'] = supplier_part.manufacturer_part.manufacturer.id initials['MPN'] = supplier_part.manufacturer_part.MPN @@ -686,7 +686,7 @@ class SupplierPartCreate(AjaxCreateView): initials['MPN'] = manufacturer_part_obj.MPN except (ValueError, ManufacturerPart.DoesNotExist, Part.DoesNotExist, Company.DoesNotExist): pass - + if part_id: try: initials['part'] = Part.objects.get(pk=part_id) @@ -703,13 +703,13 @@ class SupplierPartCreate(AjaxCreateView): if currency_code: initials['single_pricing'] = ('', currency) - + return initials class SupplierPartDelete(AjaxDeleteView): """ Delete view for removing a SupplierPart. - + SupplierParts can be deleted using a variety of 'selectors'. - ?part= -> Delete a single SupplierPart object @@ -840,7 +840,7 @@ class PriceBreakCreate(AjaxCreateView): # Extract the currency object associated with the code currency = CURRENCIES.get(currency_code, None) - + if currency: initials['price'] = [1.0, currency] diff --git a/InvenTree/label/api.py b/InvenTree/label/api.py index 0d0d71b50d..fc082e4a50 100644 --- a/InvenTree/label/api.py +++ b/InvenTree/label/api.py @@ -159,7 +159,7 @@ class StockItemLabelList(LabelListView, StockItemLabelMixin): """ Filter the StockItem label queryset. """ - + queryset = super().filter_queryset(queryset) # List of StockItem objects to match against @@ -178,7 +178,7 @@ class StockItemLabelList(LabelListView, StockItemLabelMixin): # Keep track of which labels match every specified stockitem valid_label_ids = set() - + for label in queryset.all(): matches = True @@ -293,7 +293,7 @@ class StockLocationLabelList(LabelListView, StockLocationLabelMixin): """ queryset = super().filter_queryset(queryset) - + # List of StockLocation objects to match against locations = self.get_locations() diff --git a/InvenTree/label/apps.py b/InvenTree/label/apps.py index 2b99921703..e51767d5f0 100644 --- a/InvenTree/label/apps.py +++ b/InvenTree/label/apps.py @@ -139,7 +139,7 @@ class LabelConfig(AppConfig): except: # Database might not yet be ready return - + src_dir = os.path.join( os.path.dirname(os.path.realpath(__file__)), 'templates', diff --git a/InvenTree/label/models.py b/InvenTree/label/models.py index 5c1b104670..71ccc73ac9 100644 --- a/InvenTree/label/models.py +++ b/InvenTree/label/models.py @@ -44,7 +44,7 @@ def rename_label(instance, filename): def validate_stock_item_filters(filters): - + filters = validateFilterString(filters, model=stock.models.StockItem) return filters @@ -82,7 +82,7 @@ class LabelTemplate(models.Model): # Each class of label files will be stored in a separate subdirectory SUBDIR = "label" - + # Object we will be printing against (will be filled out later) object_to_print = None diff --git a/InvenTree/label/test_api.py b/InvenTree/label/test_api.py index 92e7733891..af4c0782ec 100644 --- a/InvenTree/label/test_api.py +++ b/InvenTree/label/test_api.py @@ -40,7 +40,7 @@ class TestReportTests(InvenTreeAPITestCase): return response.data def test_list(self): - + response = self.do_list() # TODO - Add some report templates to the fixtures diff --git a/InvenTree/order/forms.py b/InvenTree/order/forms.py index 4c9caf3b53..e7336f5b0d 100644 --- a/InvenTree/order/forms.py +++ b/InvenTree/order/forms.py @@ -64,7 +64,7 @@ class CancelSalesOrderForm(HelperForm): fields = [ 'confirm', ] - + class ShipSalesOrderForm(HelperForm): diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index d3b09dec1e..97e69cf562 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -309,7 +309,7 @@ class PurchaseOrder(Order): """ A PurchaseOrder can only be cancelled under the following circumstances: """ - + return self.status in [ PurchaseOrderStatus.PLACED, PurchaseOrderStatus.PENDING @@ -378,7 +378,7 @@ class PurchaseOrder(Order): # Has this order been completed? if len(self.pending_line_items()) == 0: - + self.received_by = user self.complete_order() # This will save the model @@ -419,7 +419,7 @@ class SalesOrder(Order): except (ValueError, TypeError): # Date processing error, return queryset unchanged return queryset - + # Construct a queryset for "completed" orders within the range completed = Q(status__in=SalesOrderStatus.COMPLETE) & Q(shipment_date__gte=min_date) & Q(shipment_date__lte=max_date) @@ -495,7 +495,7 @@ class SalesOrder(Order): for line in self.lines.all(): if not line.is_fully_allocated(): return False - + return True def is_over_allocated(self): @@ -590,11 +590,11 @@ class SalesOrderAttachment(InvenTreeAttachment): class OrderLineItem(models.Model): """ Abstract model for an order line item - + Attributes: quantity: Number of items note: Annotation for the item - + """ class Meta: @@ -603,13 +603,13 @@ class OrderLineItem(models.Model): quantity = RoundingDecimalField(max_digits=15, decimal_places=5, validators=[MinValueValidator(0)], default=1, verbose_name=_('Quantity'), help_text=_('Item quantity')) reference = models.CharField(max_length=100, blank=True, verbose_name=_('Reference'), help_text=_('Line item reference')) - + notes = models.CharField(max_length=500, blank=True, verbose_name=_('Notes'), help_text=_('Line item notes')) class PurchaseOrderLineItem(OrderLineItem): """ Model for a purchase order line item. - + Attributes: order: Reference to a PurchaseOrder object @@ -637,7 +637,7 @@ class PurchaseOrderLineItem(OrderLineItem): def get_base_part(self): """ Return the base-part for the line item """ return self.part.part - + # TODO - Function callback for when the SupplierPart is deleted? part = models.ForeignKey( diff --git a/InvenTree/order/serializers.py b/InvenTree/order/serializers.py index a04798c303..cd2dca828f 100644 --- a/InvenTree/order/serializers.py +++ b/InvenTree/order/serializers.py @@ -61,7 +61,7 @@ class POSerializer(InvenTreeModelSerializer): return queryset supplier_detail = CompanyBriefSerializer(source='supplier', many=False, read_only=True) - + line_items = serializers.IntegerField(read_only=True) status_text = serializers.CharField(source='get_status_display', read_only=True) @@ -70,7 +70,7 @@ class POSerializer(InvenTreeModelSerializer): class Meta: model = PurchaseOrder - + fields = [ 'pk', 'issue_date', @@ -89,7 +89,7 @@ class POSerializer(InvenTreeModelSerializer): 'target_date', 'notes', ] - + read_only_fields = [ 'reference', 'status' @@ -110,10 +110,10 @@ class POLineItemSerializer(InvenTreeModelSerializer): quantity = serializers.FloatField() received = serializers.FloatField() - + part_detail = PartBriefSerializer(source='get_base_part', many=False, read_only=True) supplier_part_detail = SupplierPartSerializer(source='part', many=False, read_only=True) - + purchase_price_string = serializers.CharField(source='purchase_price', read_only=True) class Meta: @@ -144,7 +144,7 @@ class POAttachmentSerializer(InvenTreeModelSerializer): class Meta: model = PurchaseOrderAttachment - + fields = [ 'pk', 'order', @@ -270,7 +270,7 @@ class SOLineItemSerializer(InvenTreeModelSerializer): if allocations is not True: self.fields.pop('allocations') - + order_detail = SalesOrderSerializer(source='order', many=False, read_only=True) part_detail = PartBriefSerializer(source='part', many=False, read_only=True) allocations = SalesOrderAllocationSerializer(many=True, read_only=True) @@ -306,7 +306,7 @@ class SOAttachmentSerializer(InvenTreeModelSerializer): class Meta: model = SalesOrderAttachment - + fields = [ 'pk', 'order', diff --git a/InvenTree/order/test_api.py b/InvenTree/order/test_api.py index cb92b8b384..0ef63e485a 100644 --- a/InvenTree/order/test_api.py +++ b/InvenTree/order/test_api.py @@ -94,7 +94,7 @@ class PurchaseOrderTest(OrderTest): url = '/api/order/po/1/' response = self.get(url) - + self.assertEqual(response.status_code, 200) data = response.data @@ -109,7 +109,7 @@ class PurchaseOrderTest(OrderTest): response = self.get(url) self.assertEqual(response.status_code, status.HTTP_200_OK) - + class SalesOrderTest(OrderTest): """ diff --git a/InvenTree/order/test_sales_order.py b/InvenTree/order/test_sales_order.py index 0b37b96409..8337ff4b57 100644 --- a/InvenTree/order/test_sales_order.py +++ b/InvenTree/order/test_sales_order.py @@ -73,7 +73,7 @@ class SalesOrderTest(TestCase): def test_add_duplicate_line_item(self): # Adding a duplicate line item to a SalesOrder is accepted - + for ii in range(1, 5): SalesOrderLineItem.objects.create(order=self.order, part=self.part, quantity=ii) @@ -107,7 +107,7 @@ class SalesOrderTest(TestCase): self.assertTrue(self.order.is_fully_allocated()) self.assertTrue(self.line.is_fully_allocated()) self.assertEqual(self.line.allocated_quantity(), 50) - + def test_order_cancel(self): # Allocate line items then cancel the order @@ -154,7 +154,7 @@ class SalesOrderTest(TestCase): for item in outputs.all(): self.assertEqual(item.quantity, 25) - + self.assertEqual(sa.sales_order, None) self.assertEqual(sb.sales_order, None) @@ -162,7 +162,7 @@ class SalesOrderTest(TestCase): self.assertEqual(SalesOrderAllocation.objects.count(), 0) self.assertEqual(self.order.status, status.SalesOrderStatus.SHIPPED) - + self.assertTrue(self.order.is_fully_allocated()) self.assertTrue(self.line.is_fully_allocated()) self.assertEqual(self.line.fulfilled_quantity(), 50) diff --git a/InvenTree/order/test_views.py b/InvenTree/order/test_views.py index 51981af105..23122d079d 100644 --- a/InvenTree/order/test_views.py +++ b/InvenTree/order/test_views.py @@ -17,7 +17,7 @@ import json class OrderViewTestCase(TestCase): - + fixtures = [ 'category', 'part', @@ -193,7 +193,7 @@ class POTests(OrderViewTestCase): # Test without confirmation response = self.client.post(url, {'confirm': 0}, HTTP_X_REQUESTED_WITH='XMLHttpRequest') self.assertEqual(response.status_code, 200) - + data = json.loads(response.content) self.assertFalse(data['form_valid']) @@ -221,7 +221,7 @@ class POTests(OrderViewTestCase): # GET the form (pass the correct info) response = self.client.get(url, {'order': 1}, HTTP_X_REQUESTED_WITH='XMLHttpRequest') - + post_data = { 'part': 100, 'quantity': 45, @@ -303,7 +303,7 @@ class TestPOReceive(OrderViewTestCase): self.client.get(self.url, data, HTTP_X_REQUESTED_WITH='XMLHttpRequest') def test_receive_lines(self): - + post_data = { } @@ -330,7 +330,7 @@ class TestPOReceive(OrderViewTestCase): # Receive negative number post_data['line-1'] = -100 - + self.post(post_data, validate=False) # Receive 75 items diff --git a/InvenTree/order/tests.py b/InvenTree/order/tests.py index ed6a4ebb6a..1f35e54fb5 100644 --- a/InvenTree/order/tests.py +++ b/InvenTree/order/tests.py @@ -36,7 +36,7 @@ class OrderTest(TestCase): self.assertEqual(order.get_absolute_url(), '/order/purchase-order/1/') self.assertEqual(str(order), 'PO0001 - ACME') - + line = PurchaseOrderLineItem.objects.get(pk=1) self.assertEqual(str(line), "100 x ACME0001 from ACME (for PO0001 - ACME)") @@ -113,7 +113,7 @@ class OrderTest(TestCase): # Try to order a supplier part from the wrong supplier sku = SupplierPart.objects.get(SKU='ZERG-WIDGET') - + with self.assertRaises(django_exceptions.ValidationError): order.add_line_item(sku, 99) @@ -153,7 +153,7 @@ class OrderTest(TestCase): with self.assertRaises(django_exceptions.ValidationError): order.receive_line_item(line, loc, 'not a number', user=None) - + # Receive the rest of the items order.receive_line_item(line, loc, 50, user=None) diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py index c62a3816d5..154b5ebd9c 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -152,7 +152,7 @@ class SalesOrderAttachmentCreate(AjaxCreateView): """ Save the user that uploaded the attachment """ - + attachment = form.save(commit=False) attachment.user = self.request.user attachment.save() @@ -330,7 +330,7 @@ class PurchaseOrderCreate(AjaxCreateView): order = form.save(commit=False) order.created_by = self.request.user - + return super().save(form) @@ -365,7 +365,7 @@ class SalesOrderCreate(AjaxCreateView): order = form.save(commit=False) order.created_by = self.request.user - + return super().save(form) @@ -414,7 +414,7 @@ class PurchaseOrderCancel(AjaxUpdateView): form_class = order_forms.CancelPurchaseOrderForm def validate(self, order, form, **kwargs): - + confirm = str2bool(form.cleaned_data.get('confirm', False)) if not confirm: @@ -536,11 +536,11 @@ class SalesOrderShip(AjaxUpdateView): order = self.get_object() self.object = order - + form = self.get_form() confirm = str2bool(request.POST.get('confirm', False)) - + valid = False if not confirm: @@ -823,7 +823,7 @@ class OrderParts(AjaxView): for supplier in self.suppliers: supplier.order_items = [] - + suppliers[supplier.name] = supplier for part in self.parts: @@ -844,9 +844,9 @@ class OrderParts(AjaxView): supplier.selected_purchase_order = orders.first().id else: supplier.selected_purchase_order = None - + suppliers[supplier.name] = supplier - + suppliers[supplier.name].order_items.append(part) self.suppliers = [suppliers[key] for key in suppliers.keys()] @@ -864,7 +864,7 @@ class OrderParts(AjaxView): if 'stock[]' in self.request.GET: stock_id_list = self.request.GET.getlist('stock[]') - + """ Get a list of all the parts associated with the stock items. - Base part must be purchaseable. - Return a set of corresponding Part IDs @@ -907,7 +907,7 @@ class OrderParts(AjaxView): parts = build.required_parts for part in parts: - + # If ordering from a Build page, ignore parts that we have enough of if part.quantity_to_order <= 0: continue @@ -963,19 +963,19 @@ class OrderParts(AjaxView): # Extract part information from the form for item in self.request.POST: - + if item.startswith('part-supplier-'): - + pk = item.replace('part-supplier-', '') - + # Check that the part actually exists try: part = Part.objects.get(id=pk) except (Part.DoesNotExist, ValueError): continue - + supplier_part_id = self.request.POST[item] - + quantity = self.request.POST.get('part-quantity-' + str(pk), 0) # Ensure a valid supplier has been passed @@ -1377,7 +1377,7 @@ class SalesOrderAssignSerials(AjaxView, FormMixin): self.form.fields['line'].widget = HiddenInput() else: self.form.add_error('line', _('Select line item')) - + if self.part: self.form.fields['part'].widget = HiddenInput() else: @@ -1412,7 +1412,7 @@ class SalesOrderAssignSerials(AjaxView, FormMixin): continue # Now we have a valid stock item - but can it be added to the sales order? - + # If not in stock, cannot be added to the order if not stock_item.in_stock: self.form.add_error( @@ -1480,7 +1480,7 @@ class SalesOrderAllocationCreate(AjaxCreateView): model = SalesOrderAllocation form_class = order_forms.CreateSalesOrderAllocationForm ajax_form_title = _('Allocate Stock to Order') - + def get_initial(self): initials = super().get_initial().copy() @@ -1495,10 +1495,10 @@ class SalesOrderAllocationCreate(AjaxCreateView): items = StockItem.objects.filter(part=line.part) quantity = line.quantity - line.allocated_quantity() - + if quantity < 0: quantity = 0 - + if items.count() == 1: item = items.first() initials['item'] = item @@ -1514,7 +1514,7 @@ class SalesOrderAllocationCreate(AjaxCreateView): return initials def get_form(self): - + form = super().get_form() line_id = form['line'].value() @@ -1542,10 +1542,10 @@ class SalesOrderAllocationCreate(AjaxCreateView): # Hide the 'line' field form.fields['line'].widget = HiddenInput() - + except (ValueError, SalesOrderLineItem.DoesNotExist): pass - + return form @@ -1554,7 +1554,7 @@ class SalesOrderAllocationEdit(AjaxUpdateView): model = SalesOrderAllocation form_class = order_forms.EditSalesOrderAllocationForm ajax_form_title = _('Edit Allocation Quantity') - + def get_form(self): form = super().get_form() diff --git a/InvenTree/part/admin.py b/InvenTree/part/admin.py index af2f615803..3945b56d7c 100644 --- a/InvenTree/part/admin.py +++ b/InvenTree/part/admin.py @@ -25,13 +25,13 @@ class PartResource(ModelResource): # ForeignKey fields category = Field(attribute='category', widget=widgets.ForeignKeyWidget(PartCategory)) - + default_location = Field(attribute='default_location', widget=widgets.ForeignKeyWidget(StockLocation)) default_supplier = Field(attribute='default_supplier', widget=widgets.ForeignKeyWidget(SupplierPart)) category_name = Field(attribute='category__name', readonly=True) - + variant_of = Field(attribute='variant_of', widget=widgets.ForeignKeyWidget(Part)) suppliers = Field(attribute='supplier_count', readonly=True) @@ -73,7 +73,7 @@ class PartResource(ModelResource): class PartAdmin(ImportExportModelAdmin): - + resource_class = PartResource list_display = ('full_name', 'description', 'total_stock', 'category') diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index 6b26365b27..de6ac5f273 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -41,7 +41,7 @@ class PartCategoryTree(TreeSerializer): model = PartCategory queryset = PartCategory.objects.all() - + @property def root_url(self): return reverse('part-index') @@ -79,7 +79,7 @@ class CategoryList(generics.ListCreateAPIView): pass # Look for top-level categories elif isNull(cat_id): - + if not cascade: queryset = queryset.filter(parent=None) @@ -166,9 +166,9 @@ class CategoryParameters(generics.ListAPIView): parent_categories = category.get_ancestors() for parent in parent_categories: category_list.append(parent.pk) - + queryset = queryset.filter(category__in=category_list) - + return queryset @@ -264,7 +264,7 @@ class PartThumbs(generics.ListAPIView): # Get all Parts which have an associated image queryset = queryset.exclude(image='') - + return queryset def list(self, request, *args, **kwargs): @@ -301,7 +301,7 @@ class PartDetail(generics.RetrieveUpdateDestroyAPIView): queryset = Part.objects.all() serializer_class = part_serializers.PartSerializer - + starred_parts = None def get_queryset(self, *args, **kwargs): @@ -482,7 +482,7 @@ class PartList(generics.ListCreateAPIView): def get_queryset(self, *args, **kwargs): queryset = super().get_queryset(*args, **kwargs) - + queryset = part_serializers.PartSerializer.prefetch_queryset(queryset) queryset = part_serializers.PartSerializer.annotate_queryset(queryset) @@ -576,7 +576,7 @@ class PartList(generics.ListCreateAPIView): if cat_id is None: # No category filtering if category is not specified pass - + else: # Category has been specified! if isNull(cat_id): @@ -780,10 +780,10 @@ class BomList(generics.ListCreateAPIView): kwargs['sub_part_detail'] = str2bool(self.request.GET.get('sub_part_detail', None)) except AttributeError: pass - + # Ensure the request context is passed through! kwargs['context'] = self.get_serializer_context() - + return self.serializer_class(*args, **kwargs) def get_queryset(self, *args, **kwargs): @@ -867,7 +867,7 @@ class BomList(generics.ListCreateAPIView): # Work out which lines have actually been validated pks = [] - + for bom_item in queryset.all(): if bom_item.is_line_valid: pks.append(bom_item.pk) @@ -915,7 +915,7 @@ class BomItemValidate(generics.UpdateAPIView): valid = request.data.get('valid', False) instance = self.get_object() - + serializer = self.get_serializer(instance, data=request.data, partial=partial) serializer.is_valid(raise_exception=True) @@ -949,7 +949,7 @@ part_api_urls = [ url(r'^sale-price/', include([ url(r'^.*$', PartSalePriceList.as_view(), name='api-part-sale-price-list'), ])), - + # Base URL for PartParameter API endpoints url(r'^parameter/', include([ url(r'^template/$', PartParameterTemplateList.as_view(), name='api-part-param-template-list'), diff --git a/InvenTree/part/apps.py b/InvenTree/part/apps.py index 531b74442a..1b233bccac 100644 --- a/InvenTree/part/apps.py +++ b/InvenTree/part/apps.py @@ -43,7 +43,7 @@ class PartConfig(AppConfig): if part.image: url = part.image.thumbnail.name loc = os.path.join(settings.MEDIA_ROOT, url) - + if not os.path.exists(loc): logger.info("InvenTree: Generating thumbnail for Part '{p}'".format(p=part.name)) try: diff --git a/InvenTree/part/bom.py b/InvenTree/part/bom.py index b0f53c55eb..42f49f9dde 100644 --- a/InvenTree/part/bom.py +++ b/InvenTree/part/bom.py @@ -69,7 +69,7 @@ def ExportBom(part, fmt='csv', cascade=False, max_levels=None, parameter_data=Fa for item in items: item.level = str(int(level)) - + # Avoid circular BOM references if item.pk in uids: continue @@ -79,7 +79,7 @@ def ExportBom(part, fmt='csv', cascade=False, max_levels=None, parameter_data=Fa if item.sub_part.assembly: if max_levels is None or level < max_levels: add_items(item.sub_part.bom_items.all().order_by('id'), level + 1) - + if cascade: # Cascading (multi-level) BOM @@ -124,7 +124,7 @@ def ExportBom(part, fmt='csv', cascade=False, max_levels=None, parameter_data=Fa parameter_cols[name].update({b_idx: value}) except KeyError: parameter_cols[name] = {b_idx: value} - + # Add parameter columns to dataset parameter_cols_ordered = OrderedDict(sorted(parameter_cols.items(), key=lambda x: x[0])) add_columns_to_dataset(parameter_cols_ordered, len(bom_items)) @@ -185,7 +185,7 @@ def ExportBom(part, fmt='csv', cascade=False, max_levels=None, parameter_data=Fa # Filter manufacturer parts manufacturer_parts = ManufacturerPart.objects.filter(part__pk=b_part.pk) manufacturer_parts = manufacturer_parts.prefetch_related('supplier_parts') - + # Process manufacturer part for manufacturer_idx, manufacturer_part in enumerate(manufacturer_parts): @@ -250,7 +250,7 @@ def ExportBom(part, fmt='csv', cascade=False, max_levels=None, parameter_data=Fa # Filter supplier parts manufacturer_parts = ManufacturerPart.objects.filter(part__pk=b_part.pk) - + for idx, manufacturer_part in enumerate(manufacturer_parts): if manufacturer_part: @@ -295,7 +295,7 @@ def ExportBom(part, fmt='csv', cascade=False, max_levels=None, parameter_data=Fa # Filter supplier parts supplier_parts = SupplierPart.objects.filter(part__pk=b_part.pk) - + for idx, supplier_part in enumerate(supplier_parts): if supplier_part.supplier: @@ -326,7 +326,7 @@ def ExportBom(part, fmt='csv', cascade=False, max_levels=None, parameter_data=Fa filename = '{n}_BOM.{fmt}'.format(n=part.full_name, fmt=fmt) return DownloadFile(data, filename) - + class BomUploadManager: """ Class for managing an uploaded BOM file """ @@ -342,7 +342,7 @@ class BomUploadManager: 'Part_IPN', 'Part_ID', ] - + # Fields which would be helpful but are not required OPTIONAL_HEADERS = [ 'Reference', @@ -360,7 +360,7 @@ class BomUploadManager: def __init__(self, bom_file): """ Initialize the BomUpload class with a user-uploaded file object """ - + self.process(bom_file) def process(self, bom_file): @@ -387,7 +387,7 @@ class BomUploadManager: def guess_header(self, header, threshold=80): """ Try to match a header (from the file) to a list of known headers - + Args: header - Header name to look for threshold - Match threshold for fuzzy search @@ -421,7 +421,7 @@ class BomUploadManager: return matches[0]['header'] return None - + def columns(self): """ Return a list of headers for the thingy """ headers = [] diff --git a/InvenTree/part/forms.py b/InvenTree/part/forms.py index 131c6aeac7..8f6e3d8898 100644 --- a/InvenTree/part/forms.py +++ b/InvenTree/part/forms.py @@ -95,11 +95,11 @@ class BomExportForm(forms.Form): parameter_data = forms.BooleanField(label=_("Include Parameter Data"), required=False, initial=False, help_text=_("Include part parameters data in exported BOM")) stock_data = forms.BooleanField(label=_("Include Stock Data"), required=False, initial=False, help_text=_("Include part stock data in exported BOM")) - + manufacturer_data = forms.BooleanField(label=_("Include Manufacturer Data"), required=False, initial=True, help_text=_("Include part manufacturer data in exported BOM")) supplier_data = forms.BooleanField(label=_("Include Supplier Data"), required=False, initial=True, help_text=_("Include part supplier data in exported BOM")) - + def get_choices(self): """ BOM export format choices """ @@ -324,7 +324,7 @@ class EditCategoryParameterTemplateForm(HelperForm): add_to_all_categories = forms.BooleanField(required=False, initial=False, help_text=_('Add parameter template to all categories')) - + class Meta: model = PartCategoryParameterTemplate fields = [ diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 137781ba2b..b3955bed24 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -349,7 +349,7 @@ class Part(MPTTModel): context['available'] = self.available_stock context['on_order'] = self.on_order - + context['required'] = context['required_build_order_quantity'] + context['required_sales_order_quantity'] context['allocated'] = context['allocated_build_order_quantity'] + context['allocated_sales_order_quantity'] @@ -434,7 +434,7 @@ class Part(MPTTModel): a) The parent part is the same as this one b) The parent part is used in the BOM for *this* part c) The parent part is used in the BOM for any child parts under this one - + Failing this check raises a ValidationError! """ @@ -506,7 +506,7 @@ class Part(MPTTModel): parts = Part.objects.filter(tree_id=self.tree_id) stock = StockModels.StockItem.objects.filter(part__in=parts).exclude(serial=None) - + # There are no matchin StockItem objects (skip further tests) if not stock.exists(): return None @@ -578,7 +578,7 @@ class Part(MPTTModel): if self.IPN: elements.append(self.IPN) - + elements.append(self.name) if self.revision: @@ -663,7 +663,7 @@ class Part(MPTTModel): def clean(self): """ Perform cleaning operations for the Part model - + Update trackable status: If this part is trackable, and it is used in the BOM for a parent part which is *not* trackable, @@ -946,7 +946,7 @@ class Part(MPTTModel): quantity = 0 for build in builds: - + bom_item = None # List the bom lines required to make the build (including inherited ones!) @@ -958,7 +958,7 @@ class Part(MPTTModel): build_quantity = build.quantity * bom_item.quantity quantity += build_quantity - + return quantity def requiring_sales_orders(self): @@ -1008,7 +1008,7 @@ class Part(MPTTModel): def quantity_to_order(self): """ Return the quantity needing to be ordered for this part. - + Here, an "order" could be one of: - Build Order - Sales Order @@ -1019,7 +1019,7 @@ class Part(MPTTModel): Required for orders = self.required_order_quantity() Currently on order = self.on_order Currently building = self.quantity_being_built - + """ # Total requirement @@ -1114,7 +1114,7 @@ class Part(MPTTModel): if total is None: total = 0 - + return max(total, 0) @property @@ -1238,7 +1238,7 @@ class Part(MPTTModel): @property def total_stock(self): """ Return the total stock quantity for this part. - + - Part may be stored in multiple locations - If this part is a "template" (variants exist) then these are counted too """ @@ -1463,7 +1463,7 @@ class Part(MPTTModel): # Start with a list of all parts designated as 'sub components' parts = Part.objects.filter(component=True) - + # Exclude this part parts = parts.exclude(id=self.id) @@ -1496,7 +1496,7 @@ class Part(MPTTModel): def get_price_info(self, quantity=1, buy=True, bom=True): """ Return a simplified pricing string for this part - + Args: quantity: Number of units to calculate price for buy: Include supplier pricing (default = True) @@ -1519,7 +1519,7 @@ class Part(MPTTModel): return "{a} - {b}".format(a=min_price, b=max_price) def get_supplier_price_range(self, quantity=1): - + min_price = None max_price = None @@ -1586,7 +1586,7 @@ class Part(MPTTModel): return (min_price, max_price) def get_price_range(self, quantity=1, buy=True, bom=True): - + """ Return the price range for this part. This price can be either: - Supplier price (if purchased from suppliers) @@ -1645,7 +1645,7 @@ class Part(MPTTModel): @transaction.atomic def copy_parameters_from(self, other, **kwargs): - + clear = kwargs.get('clear', True) if clear: @@ -1692,7 +1692,7 @@ class Part(MPTTModel): # Copy the parameters data if kwargs.get('parameters', True): self.copy_parameters_from(other) - + # Copy the fields that aren't available in the duplicate form self.salable = other.salable self.assembly = other.assembly @@ -1722,7 +1722,7 @@ class Part(MPTTModel): tests = tests.filter(required=required) return tests - + def getRequiredTests(self): # Return the tests which are required by this part return self.getTestTemplates(required=True) @@ -1868,7 +1868,7 @@ class PartAttachment(InvenTreeAttachment): """ Model for storing file attachments against a Part object """ - + def getSubdir(self): return os.path.join("part_files", str(self.part.id)) @@ -2227,7 +2227,7 @@ class BomItem(models.Model): def validate_hash(self, valid=True): """ Mark this item as 'valid' (store the checksum hash). - + Args: valid: If true, validate the hash, otherwise invalidate it (default = True) """ @@ -2265,7 +2265,7 @@ class BomItem(models.Model): # Check for circular BOM references if self.sub_part: self.sub_part.checkAddToBOM(self.part) - + # If the sub_part is 'trackable' then the 'quantity' field must be an integer if self.sub_part.trackable: if not self.quantity == int(self.quantity): @@ -2301,7 +2301,7 @@ class BomItem(models.Model): """ query = self.sub_part.stock_items.all() - + query = query.prefetch_related([ 'sub_part__stock_items', ]) @@ -2358,7 +2358,7 @@ class BomItem(models.Model): def get_required_quantity(self, build_quantity): """ Calculate the required part quantity, based on the supplier build_quantity. Includes overage estimate in the returned value. - + Args: build_quantity: Number of parts to build diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index 58df62283a..7ab385249c 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -134,7 +134,7 @@ class PartBriefSerializer(InvenTreeModelSerializer): """ Serializer for Part (brief detail) """ thumbnail = serializers.CharField(source='get_thumbnail_url', read_only=True) - + stock = serializers.FloatField(source='total_stock') class Meta: @@ -232,7 +232,7 @@ class PartSerializer(InvenTreeModelSerializer): output_field=models.DecimalField(), ) ) - + # Filter to limit orders to "open" order_filter = Q( order__status__in=PurchaseOrderStatus.OPEN @@ -259,7 +259,7 @@ class PartSerializer(InvenTreeModelSerializer): output_field=models.DecimalField(), ), ) - + return queryset def get_starred(self, part): @@ -358,7 +358,7 @@ class BomItemSerializer(InvenTreeModelSerializer): quantity = serializers.FloatField() part = serializers.PrimaryKeyRelatedField(queryset=Part.objects.filter(assembly=True)) - + part_detail = PartBriefSerializer(source='part', many=False, read_only=True) sub_part = serializers.PrimaryKeyRelatedField(queryset=Part.objects.filter(component=True)) diff --git a/InvenTree/part/templatetags/inventree_extras.py b/InvenTree/part/templatetags/inventree_extras.py index 536f25cb5b..cd2313e09c 100644 --- a/InvenTree/part/templatetags/inventree_extras.py +++ b/InvenTree/part/templatetags/inventree_extras.py @@ -47,7 +47,7 @@ def str2bool(x, *args, **kwargs): def inrange(n, *args, **kwargs): """ Return range(n) for iterating through a numeric quantity """ return range(n) - + @register.simple_tag() def multiply(x, y, *args, **kwargs): @@ -59,7 +59,7 @@ def multiply(x, y, *args, **kwargs): def add(x, y, *args, **kwargs): """ Add two numbers together """ return x + y - + @register.simple_tag() def part_allocation_count(build, part, *args, **kwargs): @@ -177,7 +177,7 @@ def authorized_owners(group): except TypeError: # group.get_users returns None pass - + return owners diff --git a/InvenTree/part/test_api.py b/InvenTree/part/test_api.py index 4389003544..0f5f59d3a3 100644 --- a/InvenTree/part/test_api.py +++ b/InvenTree/part/test_api.py @@ -41,12 +41,12 @@ class PartAPITest(InvenTreeAPITestCase): Test that we can retrieve list of part categories, with various filtering options. """ - + url = reverse('api-part-category-list') - + # Request *all* part categories response = self.client.get(url, format='json') - + self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(len(response.data), 8) @@ -95,7 +95,7 @@ class PartAPITest(InvenTreeAPITestCase): url = reverse('api-part-category-list') response = self.client.post(url, data, format='json') self.assertEqual(response.status_code, status.HTTP_201_CREATED) - + parent = response.data['pk'] # Add some sub-categories to the top-level 'Animals' category @@ -289,7 +289,7 @@ class PartAPITest(InvenTreeAPITestCase): self.assertIn('count', data) self.assertIn('results', data) - + self.assertEqual(len(data['results']), n) @@ -354,7 +354,7 @@ class PartAPIAggregationTest(InvenTreeAPITestCase): self.assertEqual(data['in_stock'], 600) self.assertEqual(data['stock_item_count'], 4) - + # Add some more stock items!! for i in range(100): StockItem.objects.create(part=self.part, quantity=5) @@ -463,7 +463,7 @@ class PartParameterTest(InvenTreeAPITestCase): response = self.client.patch(url, {'data': '15'}, format='json') self.assertEqual(response.status_code, 200) - + # Check that the data changed! response = self.client.get(url, format='json') diff --git a/InvenTree/part/test_bom_item.py b/InvenTree/part/test_bom_item.py index a518ca1ddc..7e553be73a 100644 --- a/InvenTree/part/test_bom_item.py +++ b/InvenTree/part/test_bom_item.py @@ -64,7 +64,7 @@ class BomItemTest(TestCase): """ Test that BOM line overages are calculated correctly """ item = BomItem.objects.get(part=100, sub_part=50) - + q = 300 item.quantity = q @@ -77,7 +77,7 @@ class BomItemTest(TestCase): item.overage = 'asf234?' n = item.get_overage_quantity(q) self.assertEqual(n, 0) - + # Test absolute overage item.overage = '3' n = item.get_overage_quantity(q) @@ -100,7 +100,7 @@ class BomItemTest(TestCase): """ Test BOM item hash encoding """ item = BomItem.objects.get(part=100, sub_part=50) - + h1 = item.get_item_hash() # Change data - the hash must change diff --git a/InvenTree/part/test_category.py b/InvenTree/part/test_category.py index b3aa7f1202..e616fc2054 100644 --- a/InvenTree/part/test_category.py +++ b/InvenTree/part/test_category.py @@ -59,7 +59,7 @@ class CategoryTest(TestCase): def test_unique_parents(self): """ Test the 'unique_parents' functionality """ - + parents = [item.pk for item in self.transceivers.getUniqueParents()] self.assertIn(self.electronics.id, parents) @@ -128,9 +128,9 @@ class CategoryTest(TestCase): with self.assertRaises(ValidationError) as err: cat.full_clean() cat.save() - + self.assertIn('Illegal character in name', str(err.exception.error_dict.get('name'))) - + cat.name = 'good name' cat.save() diff --git a/InvenTree/part/test_migrations.py b/InvenTree/part/test_migrations.py index 41fead4b30..9da287519e 100644 --- a/InvenTree/part/test_migrations.py +++ b/InvenTree/part/test_migrations.py @@ -34,7 +34,7 @@ class TestForwardMigrations(MigratorTestCase): # Initially some fields are not present with self.assertRaises(AttributeError): print(p.has_variants) - + with self.assertRaises(AttributeError): print(p.is_template) diff --git a/InvenTree/part/test_param.py b/InvenTree/part/test_param.py index 24eee44d89..4e6556e63d 100644 --- a/InvenTree/part/test_param.py +++ b/InvenTree/part/test_param.py @@ -32,7 +32,7 @@ class TestParams(TestCase): self.assertEqual(str(c1), 'Mechanical | Length | 2.8') def test_validate(self): - + n = PartParameterTemplate.objects.all().count() t1 = PartParameterTemplate(name='abcde', units='dd') diff --git a/InvenTree/part/test_part.py b/InvenTree/part/test_part.py index 030d7faf4e..cd8726ccf4 100644 --- a/InvenTree/part/test_part.py +++ b/InvenTree/part/test_part.py @@ -91,7 +91,7 @@ class PartTest(TestCase): def test_rename_img(self): img = rename_part_image(self.r1, 'hello.png') self.assertEqual(img, os.path.join('part_images', 'hello.png')) - + def test_stock(self): # No stock of any resistors res = Part.objects.filter(description__contains='resistor') @@ -178,7 +178,7 @@ class PartSettingsTest(TestCase): Some fields for the Part model can have default values specified by the user. """ - + def setUp(self): # Create a user for auth user = get_user_model() @@ -251,7 +251,7 @@ class PartSettingsTest(TestCase): self.assertEqual(part.trackable, val) self.assertEqual(part.assembly, val) self.assertEqual(part.is_template, val) - + Part.objects.filter(pk=part.pk).delete() def test_duplicate_ipn(self): diff --git a/InvenTree/part/test_views.py b/InvenTree/part/test_views.py index 45fd39ceb0..c32753cbbb 100644 --- a/InvenTree/part/test_views.py +++ b/InvenTree/part/test_views.py @@ -9,7 +9,7 @@ from .models import Part, PartRelated class PartViewTestCase(TestCase): - + fixtures = [ 'category', 'part', @@ -24,7 +24,7 @@ class PartViewTestCase(TestCase): # Create a user user = get_user_model() - + self.user = user.objects.create_user( username='username', email='user@email.com', @@ -52,12 +52,12 @@ class PartListTest(PartViewTestCase): def test_part_index(self): response = self.client.get(reverse('part-index')) self.assertEqual(response.status_code, 200) - + keys = response.context.keys() self.assertIn('csrf_token', keys) self.assertIn('parts', keys) self.assertIn('user', keys) - + def test_export(self): """ Export part data to CSV """ @@ -153,7 +153,7 @@ class PartDetailTest(PartViewTestCase): response = self.client.get(reverse('bom-download', args=(1,)), HTTP_X_REQUESTED_WITH='XMLHttpRequest') self.assertEqual(response.status_code, 200) self.assertIn('streaming_content', dir(response)) - + class PartTests(PartViewTestCase): """ Tests for Part forms """ @@ -226,7 +226,7 @@ class PartRelatedTests(PartViewTestCase): response = self.client.post(reverse('part-related-create'), {'part_1': 1, 'part_2': 1}, HTTP_X_REQUESTED_WITH='XMLHttpRequest') self.assertContains(response, '"form_valid": false', status_code=200) - + # Check final count n = PartRelated.objects.all().count() self.assertEqual(n, 1) @@ -266,7 +266,7 @@ class PartQRTest(PartViewTestCase): def test_valid_part(self): response = self.client.get(reverse('part-qr', args=(1,)), HTTP_X_REQUESTED_WITH='XMLHttpRequest') self.assertEqual(response.status_code, 200) - + data = str(response.content) self.assertIn('Part QR Code', data) diff --git a/InvenTree/part/urls.py b/InvenTree/part/urls.py index b90b11b568..6579af019e 100644 --- a/InvenTree/part/urls.py +++ b/InvenTree/part/urls.py @@ -30,11 +30,11 @@ sale_price_break_urls = [ ] part_parameter_urls = [ - + url(r'^template/new/', views.PartParameterTemplateCreate.as_view(), name='part-param-template-create'), url(r'^template/(?P\d+)/edit/', views.PartParameterTemplateEdit.as_view(), name='part-param-template-edit'), url(r'^template/(?P\d+)/delete/', views.PartParameterTemplateDelete.as_view(), name='part-param-template-edit'), - + url(r'^new/', views.PartParameterCreate.as_view(), name='part-param-create'), url(r'^(?P\d+)/edit/', views.PartParameterEdit.as_view(), name='part-param-edit'), url(r'^(?P\d+)/delete/', views.PartParameterDelete.as_view(), name='part-param-delete'), @@ -49,10 +49,10 @@ part_detail_urls = [ url(r'^duplicate/', views.PartDuplicate.as_view(), name='part-duplicate'), url(r'^make-variant/', views.MakePartVariant.as_view(), name='make-part-variant'), url(r'^pricing/', views.PartPricing.as_view(), name='part-pricing'), - + url(r'^bom-upload/?', views.BomUpload.as_view(), name='upload-bom'), url(r'^bom-duplicate/?', views.BomDuplicate.as_view(), name='duplicate-bom'), - + url(r'^params/', views.PartDetail.as_view(template_name='part/params.html'), name='part-params'), url(r'^variants/?', views.PartDetail.as_view(template_name='part/variants.html'), name='part-variants'), url(r'^stock/?', views.PartDetail.as_view(template_name='part/stock.html'), name='part-stock'), @@ -70,7 +70,7 @@ part_detail_urls = [ url(r'^related-parts/?', views.PartDetail.as_view(template_name='part/related.html'), name='part-related'), url(r'^attachments/?', views.PartDetail.as_view(template_name='part/attachments.html'), name='part-attachments'), url(r'^notes/?', views.PartNotes.as_view(), name='part-notes'), - + url(r'^qr_code/?', views.PartQRCode.as_view(), name='part-qr'), # Normal thumbnail with form @@ -104,7 +104,7 @@ category_urls = [ url(r'^subcategory/', views.CategoryDetail.as_view(template_name='part/subcategory.html'), name='category-subcategory'), url(r'^parametric/', views.CategoryParametric.as_view(), name='category-parametric'), - + # Anything else url(r'^.*$', views.CategoryDetail.as_view(), name='category-detail'), ])) diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index d7c68dd6a3..001a258b0f 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -204,12 +204,12 @@ class PartAttachmentCreate(AjaxCreateView): class PartAttachmentEdit(AjaxUpdateView): """ View for editing a PartAttachment object """ - + model = PartAttachment form_class = part_forms.EditPartAttachmentForm ajax_template_name = 'modal_form.html' ajax_form_title = _('Edit attachment') - + def get_data(self): return { 'success': _('Part attachment updated') @@ -245,7 +245,7 @@ class PartTestTemplateCreate(AjaxCreateView): model = PartTestTemplate form_class = part_forms.EditPartTestTemplateForm ajax_form_title = _("Create Test Template") - + def get_initial(self): initials = super().get_initial() @@ -299,7 +299,7 @@ class PartSetCategory(AjaxUpdateView): category = None parts = [] - + def get(self, request, *args, **kwargs): """ Respond to a GET request to this view """ @@ -364,7 +364,7 @@ class PartSetCategory(AjaxUpdateView): ctx['category'] = self.category return ctx - + class MakePartVariant(AjaxCreateView): """ View for creating a new variant based on an existing template Part @@ -501,17 +501,17 @@ class PartDuplicate(AjaxCreateView): valid = form.is_valid() name = request.POST.get('name', None) - + if name: matches = match_part_names(name) if len(matches) > 0: # Display the first five closest matches context['matches'] = matches[:5] - + # Enforce display of the checkbox form.fields['confirm_creation'].widget = CheckboxInput() - + # Check if the user has checked the 'confirm_creation' input confirmed = str2bool(request.POST.get('confirm_creation', False)) @@ -565,7 +565,7 @@ class PartDuplicate(AjaxCreateView): initials = super(AjaxCreateView, self).get_initial() initials['bom_copy'] = str2bool(InvenTreeSetting.get_setting('PART_COPY_BOM', True)) - + initials['parameters_copy'] = str2bool(InvenTreeSetting.get_setting('PART_COPY_PARAMETERS', True)) return initials @@ -575,7 +575,7 @@ class PartCreate(AjaxCreateView): """ View for creating a new Part object. Options for providing initial conditions: - + - Provide a category object as initial data """ model = Part @@ -636,9 +636,9 @@ class PartCreate(AjaxCreateView): context = {} valid = form.is_valid() - + name = request.POST.get('name', None) - + if name: matches = match_part_names(name) @@ -646,17 +646,17 @@ class PartCreate(AjaxCreateView): # Limit to the top 5 matches (to prevent clutter) context['matches'] = matches[:5] - + # Enforce display of the checkbox form.fields['confirm_creation'].widget = CheckboxInput() - + # Check if the user has checked the 'confirm_creation' input confirmed = str2bool(request.POST.get('confirm_creation', False)) if not confirmed: msg = _('Possible matches exist - confirm creation of new part') form.add_error('confirm_creation', msg) - + form.pre_form_warning = msg valid = False @@ -705,7 +705,7 @@ class PartCreate(AjaxCreateView): initials['keywords'] = category.default_keywords except (PartCategory.DoesNotExist, ValueError): pass - + # Allow initial data to be passed through as arguments for label in ['name', 'IPN', 'description', 'revision', 'keywords']: if label in self.request.GET: @@ -734,7 +734,7 @@ class PartNotes(UpdateView): def get_success_url(self): """ Return the success URL for this form """ - + return reverse('part-notes', kwargs={'pk': self.get_object().id}) def get_context_data(self, **kwargs): @@ -767,7 +767,7 @@ class PartDetail(InvenTreeRoleMixin, DetailView): - If '?editing=True', set 'editing_enabled' context variable """ context = super().get_context_data(**kwargs) - + part = self.get_object() if str2bool(self.request.GET.get('edit', '')): @@ -806,7 +806,7 @@ class PartDetailFromIPN(PartDetail): pass except queryset.model.DoesNotExist: pass - + return None def get(self, request, *args, **kwargs): @@ -1017,7 +1017,7 @@ class BomDuplicate(AjaxUpdateView): ajax_form_title = _('Duplicate BOM') ajax_template_name = 'part/bom_duplicate.html' form_class = part_forms.BomDuplicateForm - + def get_form(self): form = super().get_form() @@ -1218,7 +1218,7 @@ class BomUpload(InvenTreeRoleMixin, FormView): def handleBomFileUpload(self): """ Process a BOM file upload form. - + This function validates that the uploaded file was valid, and contains tabulated data that can be extracted. If the file does not satisfy these requirements, @@ -1299,13 +1299,13 @@ class BomUpload(InvenTreeRoleMixin, FormView): - If using the Part_ID field, we can do an exact match against the PK field - If using the Part_IPN field, we can do an exact match against the IPN field - If using the Part_Name field, we can use fuzzy string matching to match "close" values - + We also extract other information from the row, for the other non-matched fields: - Quantity - Reference - Overage - Note - + """ # Initially use a quantity of zero @@ -1375,7 +1375,7 @@ class BomUpload(InvenTreeRoleMixin, FormView): # Check if there is a column corresponding to "Note" field if n_idx >= 0: row['note'] = row['data'][n_idx] - + # Supply list of part options for each row, sorted by how closely they match the part name row['part_options'] = part_options @@ -1390,7 +1390,7 @@ class BomUpload(InvenTreeRoleMixin, FormView): try: if row['part_ipn']: part_matches = [part for part in self.allowed_parts if part.IPN and row['part_ipn'].lower() == str(part.IPN.lower())] - + # Check for single match if len(part_matches) == 1: row['part_match'] = part_matches[0] @@ -1464,7 +1464,7 @@ class BomUpload(InvenTreeRoleMixin, FormView): col_id = int(s[3]) except ValueError: continue - + if row_id not in self.row_data: self.row_data[row_id] = {} @@ -1530,7 +1530,7 @@ class BomUpload(InvenTreeRoleMixin, FormView): if col in self.column_selections.values(): part_match_found = True break - + # If not, notify user if not part_match_found: for col in BomUploadManager.PART_MATCH_HEADERS: @@ -1546,7 +1546,7 @@ class BomUpload(InvenTreeRoleMixin, FormView): self.getTableDataFromPost() valid = len(self.missing_columns) == 0 and not self.duplicates - + if valid: # Try to extract meaningful data self.preFillSelections() @@ -1557,7 +1557,7 @@ class BomUpload(InvenTreeRoleMixin, FormView): return self.render_to_response(self.get_context_data(form=None)) def handlePartSelection(self): - + # Extract basic table data from POST request self.getTableDataFromPost() @@ -1595,7 +1595,7 @@ class BomUpload(InvenTreeRoleMixin, FormView): row['errors']['quantity'] = _('Enter a valid quantity') row['quantity'] = q - + except ValueError: continue @@ -1648,7 +1648,7 @@ class BomUpload(InvenTreeRoleMixin, FormView): if key.startswith(field + '_'): try: row_id = int(key.replace(field + '_', '')) - + row = self.getRowByIndex(row_id) if row: @@ -1714,7 +1714,7 @@ class BomUpload(InvenTreeRoleMixin, FormView): return self.render_to_response(ctx) def getRowByIndex(self, idx): - + for row in self.bom_rows: if row['index'] == idx: return row @@ -1732,7 +1732,7 @@ class BomUpload(InvenTreeRoleMixin, FormView): self.form = self.get_form(self.get_form_class()) # Did the user POST a file named bom_file? - + form_step = request.POST.get('form_step', None) if form_step == 'select_file': @@ -1753,7 +1753,7 @@ class PartExport(AjaxView): def get_parts(self, request): """ Extract part list from the POST parameters. Parts can be supplied as: - + - Part category - List of part PK values """ @@ -1956,10 +1956,10 @@ class PartPricing(AjaxView): form_class = part_forms.PartPriceForm role_required = ['sales_order.view', 'part.view'] - + def get_quantity(self): """ Return set quantity in decimal format """ - + return Decimal(self.request.POST.get('quantity', 1)) def get_part(self): @@ -1985,7 +1985,7 @@ class PartPricing(AjaxView): scaler = Decimal(1.0) part = self.get_part() - + ctx = { 'part': part, 'quantity': quantity, @@ -2039,7 +2039,7 @@ class PartPricing(AjaxView): if min_bom_price: ctx['min_total_bom_price'] = min_bom_price ctx['min_unit_bom_price'] = min_unit_bom_price - + if max_bom_price: ctx['max_total_bom_price'] = max_bom_price ctx['max_unit_bom_price'] = max_unit_bom_price @@ -2168,7 +2168,7 @@ class PartParameterDelete(AjaxDeleteView): model = PartParameter ajax_template_name = 'part/param_delete.html' ajax_form_title = _('Delete Part Parameter') - + class CategoryDetail(InvenTreeRoleMixin, DetailView): """ Detail view for PartCategory """ @@ -2223,7 +2223,7 @@ class CategoryEdit(AjaxUpdateView): """ Update view to edit a PartCategory """ - + model = PartCategory form_class = part_forms.EditCategoryForm ajax_template_name = 'modal_form.html' @@ -2244,9 +2244,9 @@ class CategoryEdit(AjaxUpdateView): Limit the choices for 'parent' field to those which make sense """ - + form = super(AjaxUpdateView, self).get_form() - + category = self.get_object() # Remove any invalid choices for the parent category part @@ -2262,7 +2262,7 @@ class CategoryDelete(AjaxDeleteView): """ Delete view to delete a PartCategory """ - + model = PartCategory ajax_template_name = 'part/category_delete.html' ajax_form_title = _('Delete Part Category') @@ -2346,7 +2346,7 @@ class CategoryParameterTemplateCreate(AjaxCreateView): """ form = super(AjaxCreateView, self).get_form() - + form.fields['category'].widget = HiddenInput() if form.is_valid(): @@ -2441,7 +2441,7 @@ class CategoryParameterTemplateEdit(AjaxUpdateView): """ form = super(AjaxUpdateView, self).get_form() - + form.fields['category'].widget = HiddenInput() form.fields['add_to_all_categories'].widget = HiddenInput() form.fields['add_to_same_level_categories'].widget = HiddenInput() @@ -2495,7 +2495,7 @@ class BomItemCreate(AjaxCreateView): """ Create view for making a new BomItem object """ - + model = BomItem form_class = part_forms.EditBomItemForm ajax_template_name = 'modal_form.html' @@ -2523,13 +2523,13 @@ class BomItemCreate(AjaxCreateView): try: part = Part.objects.get(id=part_id) - + # Hide the 'part' field form.fields['part'].widget = HiddenInput() # Exclude the part from its own BOM sub_part_query = sub_part_query.exclude(id=part.id) - + # Eliminate any options that are already in the BOM! sub_part_query = sub_part_query.exclude(id__in=[item.id for item in part.getRequiredParts()]) @@ -2634,7 +2634,7 @@ class PartSalePriceBreakCreate(AjaxCreateView): model = PartSellPriceBreak form_class = part_forms.EditPartSalePriceBreakForm ajax_form_title = _('Add Price Break') - + def get_data(self): return { 'success': _('Added new price break') @@ -2645,7 +2645,7 @@ class PartSalePriceBreakCreate(AjaxCreateView): part = Part.objects.get(id=self.request.GET.get('part')) except (ValueError, Part.DoesNotExist): part = None - + if part is None: try: part = Part.objects.get(id=self.request.POST.get('part')) @@ -2690,7 +2690,7 @@ class PartSalePriceBreakEdit(AjaxUpdateView): return form - + class PartSalePriceBreakDelete(AjaxDeleteView): """ View for deleting a sale price break """ diff --git a/InvenTree/plugins/action/action.py b/InvenTree/plugins/action/action.py index d61838f49b..8b9ed6aec9 100644 --- a/InvenTree/plugins/action/action.py +++ b/InvenTree/plugins/action/action.py @@ -23,10 +23,10 @@ class ActionPlugin(plugin.InvenTreePlugin): look at the PLUGIN_NAME instead. """ action = cls.ACTION_NAME - + if not action: action = cls.PLUGIN_NAME - + return action def __init__(self, user, data=None): diff --git a/InvenTree/report/api.py b/InvenTree/report/api.py index 0925cc5249..351a988601 100644 --- a/InvenTree/report/api.py +++ b/InvenTree/report/api.py @@ -62,7 +62,7 @@ class StockItemReportMixin: """ Return a list of requested stock items """ - + items = [] params = self.request.query_params @@ -101,7 +101,7 @@ class BuildReportMixin: params = self.request.query_params for key in ['build', 'build[]', 'builds', 'builds[]']: - + if key in params: builds = params.getlist(key, []) @@ -268,7 +268,7 @@ class StockItemTestReportList(ReportListView, StockItemReportMixin): serializer_class = TestReportSerializer def filter_queryset(self, queryset): - + queryset = super().filter_queryset(queryset) # List of StockItem objects to match against @@ -342,7 +342,7 @@ class StockItemTestReportPrint(generics.RetrieveAPIView, StockItemReportMixin, R items = self.get_items() return self.print(request, items) - + class BOMReportList(ReportListView, PartReportMixin): """ @@ -459,7 +459,7 @@ class BuildReportList(ReportListView, BuildReportMixin): We need to compare the 'filters' string of each report, and see if it matches against each of the specified parts - + # TODO: This code needs some refactoring! """ @@ -546,7 +546,7 @@ class POReportList(ReportListView, OrderReportMixin): valid_report_ids = set() for report in queryset.all(): - + matches = True # Filter string defined for the report object @@ -565,7 +565,7 @@ class POReportList(ReportListView, OrderReportMixin): except FieldError: matches = False break - + if matches: valid_report_ids.add(report.pk) else: @@ -629,7 +629,7 @@ class SOReportList(ReportListView, OrderReportMixin): valid_report_ids = set() for report in queryset.all(): - + matches = True # Filter string defined for the report object @@ -648,7 +648,7 @@ class SOReportList(ReportListView, OrderReportMixin): except FieldError: matches = False break - + if matches: valid_report_ids.add(report.pk) else: diff --git a/InvenTree/report/models.py b/InvenTree/report/models.py index 4e218a2c50..fa7ae2b271 100644 --- a/InvenTree/report/models.py +++ b/InvenTree/report/models.py @@ -52,7 +52,7 @@ class ReportFileUpload(FileSystemStorage): For example, a snippet or asset file is referenced in a template by filename, and we do not want that filename to change when we upload a new *version* of the snippet or asset file. - + This uploader class performs the following pseudo-code function: - If the model is *new*, proceed as normal @@ -408,7 +408,7 @@ class PurchaseOrderReport(ReportTemplateBase): @classmethod def getSubdir(cls): return 'purchaseorder' - + filters = models.CharField( blank=True, max_length=250, @@ -479,7 +479,7 @@ def rename_snippet(instance, filename): if str(filename) == str(instance.snippet): fullpath = os.path.join(settings.MEDIA_ROOT, path) fullpath = os.path.abspath(fullpath) - + if os.path.exists(fullpath): logger.info(f"Deleting existing snippet file: '{filename}'") os.remove(fullpath) diff --git a/InvenTree/report/templatetags/barcode.py b/InvenTree/report/templatetags/barcode.py index e38fab1f06..a47d45fdf0 100644 --- a/InvenTree/report/templatetags/barcode.py +++ b/InvenTree/report/templatetags/barcode.py @@ -22,7 +22,7 @@ def image_data(img, fmt='PNG'): buffered = BytesIO() img.save(buffered, format=fmt) - + img_str = base64.b64encode(buffered.getvalue()) return f"data:image/{fmt.lower()};charset=utf-8;base64," + img_str.decode() diff --git a/InvenTree/report/templatetags/report.py b/InvenTree/report/templatetags/report.py index 8ff15ccdee..45d11c1d2d 100644 --- a/InvenTree/report/templatetags/report.py +++ b/InvenTree/report/templatetags/report.py @@ -104,7 +104,7 @@ def company_image(company): path = os.path.abspath(path) return f"file://{path}" - + @register.simple_tag() def internal_link(link, text): diff --git a/InvenTree/script/translate.py b/InvenTree/script/translate.py index 3a08c0b410..b41bb96788 100644 --- a/InvenTree/script/translate.py +++ b/InvenTree/script/translate.py @@ -29,7 +29,7 @@ def manually_translate_file(filename, save=False): print("a) Directly enter a new tranlation in the target language") print("b) Leave empty to skip") print("c) Press Ctrl+C to exit") - + print("-------------------------") input("Press to start") print("") diff --git a/InvenTree/script/translation_stats.py b/InvenTree/script/translation_stats.py index 0ba969479b..f47a21f168 100644 --- a/InvenTree/script/translation_stats.py +++ b/InvenTree/script/translation_stats.py @@ -64,5 +64,5 @@ if __name__ == '__main__': percentage = 0 print(f"| {locale.ljust(4, ' ')} : {str(percentage).rjust(4, ' ')}% |") - + print("-" * 16) diff --git a/InvenTree/stock/admin.py b/InvenTree/stock/admin.py index 5f3c08839d..f59c44467e 100644 --- a/InvenTree/stock/admin.py +++ b/InvenTree/stock/admin.py @@ -87,7 +87,7 @@ class StockItemResource(ModelResource): # Date management updated = Field(attribute='updated', widget=widgets.DateWidget()) - + stocktake_date = Field(attribute='stocktake_date', widget=widgets.DateWidget()) def after_import(self, dataset, result, using_transactions, dry_run, **kwargs): @@ -127,7 +127,7 @@ class StockItemAdmin(ImportExportModelAdmin): class StockAttachmentAdmin(admin.ModelAdmin): list_display = ('stock_item', 'attachment', 'comment') - + class StockTrackingAdmin(ImportExportModelAdmin): list_display = ('item', 'date', 'title') diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index 0e64cecdbd..b70b379e69 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -117,7 +117,7 @@ class StockAdjust(APIView): A generic class for handling stocktake actions. Subclasses exist for: - + - StockCount: count stock items - StockAdd: add stock items - StockRemove: remove stock items @@ -184,7 +184,7 @@ class StockCount(StockAdjust): """ Endpoint for counting stock (performing a stocktake). """ - + def post(self, request, *args, **kwargs): self.get_items(request) @@ -225,7 +225,7 @@ class StockRemove(StockAdjust): def post(self, request, *args, **kwargs): self.get_items(request) - + n = 0 for item in self.items: @@ -292,7 +292,7 @@ class StockLocationList(generics.ListCreateAPIView): params = self.request.query_params loc_id = params.get('parent', None) - + cascade = str2bool(params.get('cascade', False)) # Do not filter by location @@ -304,7 +304,7 @@ class StockLocationList(generics.ListCreateAPIView): # If we allow "cascade" at the top-level, this essentially means *all* locations if not cascade: queryset = queryset.filter(parent=None) - + else: try: @@ -321,7 +321,7 @@ class StockLocationList(generics.ListCreateAPIView): except (ValueError, StockLocation.DoesNotExist): pass - + return queryset filter_backends = [ @@ -379,14 +379,14 @@ class StockList(generics.ListCreateAPIView): # A location was *not* specified - try to infer it if 'location' not in request.data: location = item.part.get_default_location() - + if location is not None: item.location = location item.save() # An expiry date was *not* specified - try to infer it! if 'expiry_date' not in request.data: - + if item.part.default_expiry > 0: item.expiry_date = datetime.now().date() + timedelta(days=item.part.default_expiry) item.save() @@ -399,7 +399,7 @@ class StockList(generics.ListCreateAPIView): """ Override the 'list' method, as the StockLocation objects are very expensive to serialize. - + So, we fetch and serialize the required StockLocation objects only as required. """ @@ -601,7 +601,7 @@ class StockList(generics.ListCreateAPIView): if stale_days > 0: stale_date = datetime.now().date() + timedelta(days=stale_days) - + stale_filter = StockItem.IN_STOCK_FILTER & ~Q(expiry_date=None) & Q(expiry_date__lt=stale_date) if stale: @@ -652,7 +652,7 @@ class StockList(generics.ListCreateAPIView): if serial_number_gte is not None: queryset = queryset.filter(serial__gte=serial_number_gte) - + if serial_number_lte is not None: queryset = queryset.filter(serial__lte=serial_number_lte) @@ -681,7 +681,7 @@ class StockList(generics.ListCreateAPIView): else: # Filter StockItem without build allocations or sales order allocations queryset = queryset.filter(Q(sales_order_allocations__isnull=True) & Q(allocations__isnull=True)) - + # Do we wish to filter by "active parts" active = params.get('active', None) @@ -766,7 +766,7 @@ class StockList(generics.ListCreateAPIView): queryset = queryset.filter(location__in=location.getUniqueChildren()) else: queryset = queryset.filter(location=loc_id) - + except (ValueError, StockLocation.DoesNotExist): pass @@ -994,14 +994,14 @@ class StockTrackingList(generics.ListCreateAPIView): def create(self, request, *args, **kwargs): """ Create a new StockItemTracking object - + Here we override the default 'create' implementation, to save the user information associated with the request object. """ serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) - + # Record the user who created this Part object item = serializer.save() item.user = request.user diff --git a/InvenTree/stock/forms.py b/InvenTree/stock/forms.py index c784b08a69..3fb72ebe1e 100644 --- a/InvenTree/stock/forms.py +++ b/InvenTree/stock/forms.py @@ -118,7 +118,7 @@ class CreateStockItemForm(HelperForm): serial_numbers = forms.CharField(label=_('Serial Numbers'), required=False, help_text=_('Enter unique serial numbers (or leave blank)')) def __init__(self, *args, **kwargs): - + self.field_prefix = { 'serial_numbers': 'fa-hashtag', 'link': 'fa-link', @@ -147,17 +147,17 @@ class CreateStockItemForm(HelperForm): # Custom clean to prevent complex StockItem.clean() logic from running (yet) def full_clean(self): self._errors = ErrorDict() - + if not self.is_bound: # Stop further processing. return - + self.cleaned_data = {} # If the form is permitted to be empty, and none of the form data has # changed from the initial data, short circuit any validation. if self.empty_permitted and not self.has_changed(): return - + # Don't run _post_clean() as this will run StockItem.clean() self._clean_fields() self._clean_form() @@ -167,9 +167,9 @@ class SerializeStockForm(HelperForm): """ Form for serializing a StockItem. """ destination = TreeNodeChoiceField(queryset=StockLocation.objects.all(), label=_('Destination'), required=True, help_text=_('Destination for serialized stock (by default, will remain in current location)')) - + serial_numbers = forms.CharField(label=_('Serial numbers'), required=True, help_text=_('Unique serial numbers (must match quantity)')) - + note = forms.CharField(label=_('Notes'), required=False, help_text=_('Add transaction note (optional)')) quantity = RoundingDecimalFormField(max_digits=10, decimal_places=5, label=_('Quantity')) @@ -240,7 +240,7 @@ class TestReportFormatForm(HelperForm): super().__init__(*args, **kwargs) self.fields['template'].choices = self.get_template_choices() - + def get_template_choices(self): """ Generate a list of of TestReport options for the StockItem @@ -337,7 +337,7 @@ class InstallStockForm(HelperForm): raise ValidationError({'quantity_to_install': _('Must not exceed available quantity')}) return data - + class UninstallStockForm(forms.ModelForm): """ @@ -373,11 +373,11 @@ class AdjustStockForm(forms.ModelForm): """ destination = TreeNodeChoiceField(queryset=StockLocation.objects.all(), label=_('Destination'), required=True, help_text=_('Destination stock location')) - + note = forms.CharField(label=_('Notes'), required=True, help_text=_('Add note (required)')) - + # transaction = forms.BooleanField(required=False, initial=False, label='Create Transaction', help_text='Create a stock transaction for these parts') - + confirm = forms.BooleanField(required=False, initial=False, label=_('Confirm stock adjustment'), help_text=_('Confirm movement of stock items')) set_loc = forms.BooleanField(required=False, initial=False, label=_('Set Default Location'), help_text=_('Set the destination as the default location for selected parts')) @@ -396,7 +396,7 @@ class AdjustStockForm(forms.ModelForm): class EditStockItemForm(HelperForm): """ Form for editing a StockItem object. Note that not all fields can be edited here (even if they can be specified during creation. - + location - Must be updated in a 'move' transaction quantity - Must be updated in a 'stocktake' transaction part - Cannot be edited after creation diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 7d9520a544..125a534f08 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -131,7 +131,7 @@ def before_delete_stock_location(sender, instance, using, **kwargs): class StockItem(MPTTModel): """ A StockItem object represents a quantity of physical instances of a part. - + Attributes: parent: Link to another StockItem from which this StockItem was created uid: Field containing a unique-id which is mapped to a third-party identifier (e.g. a barcode) @@ -191,7 +191,7 @@ class StockItem(MPTTModel): add_note = False user = kwargs.pop('user', None) - + add_note = add_note and kwargs.pop('note', True) super(StockItem, self).save(*args, **kwargs) @@ -226,7 +226,7 @@ class StockItem(MPTTModel): """ super(StockItem, self).validate_unique(exclude) - + # If the serial number is set, make sure it is not a duplicate if self.serial is not None: # Query to look for duplicate serial numbers @@ -421,7 +421,7 @@ class StockItem(MPTTModel): max_length=100, blank=True, null=True, help_text=_('Serial number for this item') ) - + link = InvenTreeURLField( verbose_name=_('External Link'), max_length=125, blank=True, @@ -727,7 +727,7 @@ class StockItem(MPTTModel): items = StockItem.objects.filter(belongs_to=self) for item in items: - + # Prevent duplication or recursion if item == self or item in installed: continue @@ -906,7 +906,7 @@ class StockItem(MPTTModel): Brief automated note detailing a movement or quantity change. """ - + track = StockItemTracking.objects.create( item=self, title=title, @@ -970,7 +970,7 @@ class StockItem(MPTTModel): # Create a new stock item for each unique serial number for serial in serials: - + # Create a copy of this StockItem new_item = StockItem.objects.get(pk=self.pk) new_item.quantity = 1 @@ -1001,7 +1001,7 @@ class StockItem(MPTTModel): """ Copy stock history from another StockItem """ for item in other.tracking_info.all(): - + item.item = self item.pk = None item.save() @@ -1151,7 +1151,7 @@ class StockItem(MPTTModel): @transaction.atomic def updateQuantity(self, quantity): """ Update stock quantity for this item. - + If the quantity has reached zero, this StockItem will be deleted. Returns: @@ -1174,7 +1174,7 @@ class StockItem(MPTTModel): self.quantity = quantity if quantity == 0 and self.delete_on_deplete and self.can_delete(): - + # TODO - Do not actually "delete" stock at this point - instead give it a "DELETED" flag self.delete() return False @@ -1584,7 +1584,7 @@ class StockItemTestResult(models.Model): with automated testing setups. Multiple results can be recorded against any given test, allowing tests to be run many times. - + Attributes: stock_item: Link to StockItem test: Test name (simple string matching) @@ -1613,7 +1613,7 @@ class StockItemTestResult(models.Model): for template in templates: if key == template.key: - + if template.requires_value: if not self.value: raise ValidationError({ diff --git a/InvenTree/stock/serializers.py b/InvenTree/stock/serializers.py index 5b00c1dd17..4991a44e6f 100644 --- a/InvenTree/stock/serializers.py +++ b/InvenTree/stock/serializers.py @@ -140,7 +140,7 @@ class StockItemSerializer(InvenTreeModelSerializer): return queryset status_text = serializers.CharField(source='get_status_display', read_only=True) - + supplier_part_detail = SupplierPartSerializer(source='supplier_part', many=False, read_only=True) part_detail = PartBriefSerializer(source='part', many=False, read_only=True) @@ -150,7 +150,7 @@ class StockItemSerializer(InvenTreeModelSerializer): tracking_items = serializers.IntegerField(source='tracking_info_count', read_only=True, required=False) quantity = serializers.FloatField() - + allocated = serializers.FloatField(source='allocation_count', required=False) expired = serializers.BooleanField(required=False, read_only=True) diff --git a/InvenTree/stock/test_api.py b/InvenTree/stock/test_api.py index ae0d6fd862..e1fb616335 100644 --- a/InvenTree/stock/test_api.py +++ b/InvenTree/stock/test_api.py @@ -38,7 +38,7 @@ class StockAPITestCase(InvenTreeAPITestCase): ] def setUp(self): - + super().setUp() @@ -105,7 +105,7 @@ class StockItemListTest(StockAPITestCase): """ response = self.get_stock(part=25) - + self.assertEqual(len(response), 8) response = self.get_stock(part=10004) @@ -339,7 +339,7 @@ class StockItemTest(StockAPITestCase): ) self.assertContains(response, 'This field is required', status_code=status.HTTP_400_BAD_REQUEST) - + # POST with an invalid part reference response = self.client.post( @@ -384,10 +384,10 @@ class StockItemTest(StockAPITestCase): - Otherwise, check if the referenced part has a default_expiry defined - If so, use that! - Otherwise, no expiry - + Notes: - Part <25> has a default_expiry of 10 days - + """ # First test - create a new StockItem without an expiry date @@ -460,7 +460,7 @@ class StocktakeTest(StockAPITestCase): data['items'] = [{ 'pk': 10 }] - + response = self.post(url, data) self.assertContains(response, 'must contain a valid pk', status_code=status.HTTP_400_BAD_REQUEST) @@ -478,12 +478,12 @@ class StocktakeTest(StockAPITestCase): response = self.post(url, data) self.assertContains(response, 'must contain a valid quantity', status_code=status.HTTP_400_BAD_REQUEST) - + data['items'] = [{ 'pk': 1234, 'quantity': "-1.234" }] - + response = self.post(url, data) self.assertContains(response, 'must not be less than zero', status_code=status.HTTP_400_BAD_REQUEST) diff --git a/InvenTree/stock/test_views.py b/InvenTree/stock/test_views.py index 261598ae22..c565532739 100644 --- a/InvenTree/stock/test_views.py +++ b/InvenTree/stock/test_views.py @@ -29,7 +29,7 @@ class StockViewTestCase(TestCase): # Create a user user = get_user_model() - + self.user = user.objects.create_user( username='username', email='user@email.com', @@ -91,7 +91,7 @@ class StockLocationTest(StockViewTestCase): # Create with an invalid parent response = self.client.get(reverse('stock-location-create'), {'location': 999}, HTTP_X_REQUESTED_WITH='XMLHttpRequest') self.assertEqual(response.status_code, 200) - + class StockItemTest(StockViewTestCase): """" Tests for StockItem views """ @@ -211,7 +211,7 @@ class StockItemTest(StockViewTestCase): 'serial_numbers': 'dd-23-adf', 'destination': 'blorg' } - + # POST response = self.client.post(url, data_valid, HTTP_X_REQUESTED_WITH='XMLHttpRequest') self.assertEqual(response.status_code, 200) @@ -247,7 +247,7 @@ class StockOwnershipTest(StockViewTestCase): # Create a new user user = get_user_model() - + self.new_user = user.objects.create_user( username='john', email='john@email.com', @@ -314,7 +314,7 @@ class StockOwnershipTest(StockViewTestCase): response = self.client.post(reverse('stock-location-edit', args=(test_location_id,)), {'name': 'Office', 'owner': new_user_group_owner.pk}, HTTP_X_REQUESTED_WITH='XMLHttpRequest') - + # Make sure the location's owner is unchanged location = StockLocation.objects.get(pk=test_location_id) self.assertEqual(location.owner, user_group_owner) @@ -366,7 +366,7 @@ class StockOwnershipTest(StockViewTestCase): response = self.client.post(reverse('stock-location-create'), new_location, HTTP_X_REQUESTED_WITH='XMLHttpRequest') self.assertContains(response, '"form_valid": true', status_code=200) - + # Retrieve created location location_created = StockLocation.objects.get(name=new_location['name']) diff --git a/InvenTree/stock/tests.py b/InvenTree/stock/tests.py index f3ca949bbf..08fa727547 100644 --- a/InvenTree/stock/tests.py +++ b/InvenTree/stock/tests.py @@ -144,7 +144,7 @@ class StockTest(TestCase): self.drawer3.save() self.assertNotEqual(self.drawer3.parent, self.office) - + self.assertEqual(self.drawer3.pathstring, 'Home/Drawer_3') def test_children(self): @@ -486,7 +486,7 @@ class VariantTest(StockTest): # Attempt to create the same serial number but for a variant (should fail!) item.pk = None item.part = Part.objects.get(pk=10004) - + with self.assertRaises(ValidationError): item.save() @@ -542,7 +542,7 @@ class TestResultTest(StockTest): test='sew cushion', result=True ) - + # Still should be failing at this point, # as the most recent "apply paint" test was False self.assertFalse(item.passedAllRequiredTests()) diff --git a/InvenTree/stock/urls.py b/InvenTree/stock/urls.py index 24e609fa4f..fe5472003f 100644 --- a/InvenTree/stock/urls.py +++ b/InvenTree/stock/urls.py @@ -14,7 +14,7 @@ location_urls = [ url(r'^edit/?', views.StockLocationEdit.as_view(), name='stock-location-edit'), url(r'^delete/?', views.StockLocationDelete.as_view(), name='stock-location-delete'), url(r'^qr_code/?', views.StockLocationQRCode.as_view(), name='stock-location-qr'), - + url(r'sublocation/', views.StockLocationDetail.as_view(template_name='stock/sublocation.html'), name='stock-location-sublocation'), # Anything else diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py index 72395a54d0..0984405055 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -121,7 +121,7 @@ class StockLocationEdit(AjaxUpdateView): context_object_name = 'location' ajax_template_name = 'modal_form.html' ajax_form_title = _('Edit Stock Location') - + def get_form(self): """ Customize form data for StockLocation editing. @@ -182,7 +182,7 @@ class StockLocationEdit(AjaxUpdateView): """ self.object = form.save() - + # Is ownership control enabled? stock_ownership_control = InvenTreeSetting.get_setting('STOCK_OWNERSHIP_CONTROL') @@ -267,7 +267,7 @@ class StockItemAttachmentCreate(AjaxCreateView): def save(self, form, **kwargs): """ Record the user that uploaded the attachment """ - + attachment = form.save(commit=False) attachment.user = self.request.user attachment.save() @@ -297,7 +297,7 @@ class StockItemAttachmentCreate(AjaxCreateView): form = super().get_form() form.fields['stock_item'].widget = HiddenInput() - + return form @@ -309,7 +309,7 @@ class StockItemAttachmentEdit(AjaxUpdateView): model = StockItemAttachment form_class = StockForms.EditStockItemAttachmentForm ajax_form_title = _("Edit Stock Item Attachment") - + def get_form(self): form = super().get_form() @@ -327,7 +327,7 @@ class StockItemAttachmentDelete(AjaxDeleteView): ajax_form_title = _("Delete Stock Item Attachment") ajax_template_name = "attachment_delete.html" context_object_name = "attachment" - + def get_data(self): return { 'danger': _("Deleted attachment"), @@ -376,7 +376,7 @@ class StockItemReturnToStock(AjaxUpdateView): ajax_form_title = _("Return to Stock") context_object_name = "item" form_class = StockForms.ReturnStockItemForm - + def validate(self, item, form, **kwargs): location = form.cleaned_data.get('location', None) @@ -405,7 +405,7 @@ class StockItemDeleteTestData(AjaxUpdateView): model = StockItem form_class = ConfirmForm ajax_form_title = _("Delete All Test Data") - + role_required = ['stock.change', 'stock.delete'] def get_form(self): @@ -417,7 +417,7 @@ class StockItemDeleteTestData(AjaxUpdateView): stock_item = StockItem.objects.get(pk=self.kwargs['pk']) form = self.get_form() - + confirm = str2bool(request.POST.get('confirm', False)) if confirm is not True: @@ -488,7 +488,7 @@ class StockItemTestResultEdit(AjaxUpdateView): form = super().get_form() form.fields['stock_item'].widget = HiddenInput() - + return form @@ -545,11 +545,11 @@ class StockExport(AjaxView): def get(self, request, *args, **kwargs): export_format = request.GET.get('format', 'csv').lower() - + # Check if a particular location was specified loc_id = request.GET.get('location', None) location = None - + if loc_id: try: location = StockLocation.objects.get(pk=loc_id) @@ -646,9 +646,9 @@ class StockItemInstall(AjaxUpdateView): In contrast to the StockItemUninstall view, only a single stock item can be installed at once. - + The "part" to be installed must be provided in the GET query parameters. - + """ model = StockItem @@ -664,7 +664,7 @@ class StockItemInstall(AjaxUpdateView): Requirements: - Items must be in stock - + Filters: - Items can be filtered by Part reference """ @@ -702,7 +702,7 @@ class StockItemInstall(AjaxUpdateView): if self.part: initials['part'] = self.part - + return initials def get_form(self): @@ -875,13 +875,13 @@ class StockItemUninstall(AjaxView, FormMixin): class StockAdjust(AjaxView, FormMixin): """ View for enacting simple stock adjustments: - + - Take items from stock - Add items to stock - Count items - Move stock - Delete stock items - + """ ajax_template_name = 'stock/stock_adjust.html' @@ -942,7 +942,7 @@ class StockAdjust(AjaxView, FormMixin): for item in self.request.POST: if item.startswith('stock-id-'): - + pk = item.replace('stock-id-', '') q = self.request.POST[item] @@ -1022,9 +1022,9 @@ class StockAdjust(AjaxView, FormMixin): form = self.get_form() valid = form.is_valid() - + for item in self.stock_items: - + try: item.new_quantity = Decimal(item.new_quantity) except ValueError: @@ -1067,7 +1067,7 @@ class StockAdjust(AjaxView, FormMixin): # Was the entire stock taken? item = self.stock_items[0] - + if item.quantity == 0: # Instruct the form to redirect data['url'] = reverse('stock-index') @@ -1107,7 +1107,7 @@ class StockAdjust(AjaxView, FormMixin): return _('No action performed') def do_add(self): - + count = 0 note = self.request.POST['note'] @@ -1137,12 +1137,12 @@ class StockAdjust(AjaxView, FormMixin): return _('Removed stock from {n} items').format(n=count) def do_count(self): - + count = 0 note = self.request.POST['note'] for item in self.stock_items: - + item.stocktake(item.new_quantity, self.request.user, notes=note) count += 1 @@ -1165,7 +1165,7 @@ class StockAdjust(AjaxView, FormMixin): if set_loc: item.part.default_location = destination item.part.save() - + # Do not move to the same location (unless the quantity is different) if destination == item.location and item.new_quantity == item.quantity: continue @@ -1188,7 +1188,7 @@ class StockAdjust(AjaxView, FormMixin): if count == 0: return _('No items were moved') - + else: return _('Moved {n} items to {dest}').format( n=count, @@ -1201,7 +1201,7 @@ class StockAdjust(AjaxView, FormMixin): # note = self.request.POST['note'] for item in self.stock_items: - + # TODO - In the future, StockItems should not be 'deleted' # TODO - Instead, they should be marked as "inactive" @@ -1475,7 +1475,7 @@ class StockItemSerialize(AjaxUpdateView): return initials def get(self, request, *args, **kwargs): - + return super().get(request, *args, **kwargs) def post(self, request, *args, **kwargs): @@ -1503,13 +1503,13 @@ class StockItemSerialize(AjaxUpdateView): form.add_error('serial_numbers', e.messages) valid = False numbers = [] - + if valid: try: item.serializeStock(quantity, numbers, user, notes=notes, location=destination) except ValidationError as e: messages = e.message_dict - + for k in messages.keys(): if k in ['quantity', 'destination', 'serial_numbers']: form.add_error(k, messages[k]) @@ -1595,7 +1595,7 @@ class StockItemCreate(AjaxCreateView): if not part.purchaseable: form.fields.pop('purchase_price') - + # Hide the 'part' field (as a valid part is selected) # form.fields['part'].widget = HiddenInput() @@ -1658,7 +1658,7 @@ class StockItemCreate(AjaxCreateView): if type(location_owner.owner) is Group: user_as_owner = Owner.get_owner(self.request.user) queryset = location_owner.get_related_owners() - + if user_as_owner in queryset: form.fields['owner'].initial = user_as_owner @@ -1668,7 +1668,7 @@ class StockItemCreate(AjaxCreateView): # If location's owner is a user: automatically set owner field and disable it form.fields['owner'].disabled = True form.fields['owner'].initial = location_owner - + return form def get_initial(self): @@ -1746,7 +1746,7 @@ class StockItemCreate(AjaxCreateView): data = form.cleaned_data part = data.get('part', None) - + quantity = data.get('quantity', None) owner = data.get('owner', None) @@ -1831,7 +1831,7 @@ class StockItemCreate(AjaxCreateView): ) item.save(user=self.request.user) - + # Create a single StockItem of the specified quantity else: form._post_clean() @@ -1841,12 +1841,12 @@ class StockItemCreate(AjaxCreateView): item.save(user=self.request.user) return item - + # Non-trackable part else: form._post_clean() - + item = form.save(commit=False) item.user = self.request.user item.save(user=self.request.user) 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/tasks.py b/tasks.py index 37cbc71eab..88bd5e42e4 100644 --- a/tasks.py +++ b/tasks.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', From a77d9d9de76ec4d689f9f4acab35b666b07cfc7b Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 6 May 2021 14:33:03 +0200 Subject: [PATCH 2/7] same treatment for html --- InvenTree/build/templates/build/allocate.html | 4 ++-- .../build/templates/build/build_base.html | 2 +- InvenTree/build/templates/build/index.html | 12 +++++----- InvenTree/build/templates/build/navbar.html | 2 +- InvenTree/build/templates/build/notes.html | 4 ++-- .../company/templates/company/detail.html | 2 +- .../company/manufacturer_part_delete.html | 4 ++-- .../company/manufacturer_part_detail.html | 2 +- .../company/manufacturer_part_suppliers.html | 2 +- .../company/templates/company/navbar.html | 2 +- .../company/templates/company/notes.html | 6 ++--- .../company/supplier_part_delete.html | 2 +- .../company/supplier_part_detail.html | 2 +- .../company/supplier_part_orders.html | 2 +- .../company/supplier_part_pricing.html | 2 +- .../order/templates/order/po_attachments.html | 2 +- .../order/purchase_order_detail.html | 4 ++-- .../templates/order/purchase_orders.html | 6 ++--- .../templates/order/sales_order_detail.html | 22 +++++++++---------- .../order/templates/order/sales_orders.html | 4 ++-- InvenTree/part/templates/part/build.html | 2 +- InvenTree/part/templates/part/category.html | 2 +- InvenTree/part/templates/part/detail.html | 2 +- .../part/templates/part/manufacturer.html | 2 +- InvenTree/part/templates/part/notes.html | 4 ++-- InvenTree/part/templates/part/params.html | 2 +- InvenTree/part/templates/part/part_base.html | 12 +++++----- .../part/templates/part/select_image.html | 2 +- .../part/templates/part/set_category.html | 8 +++---- .../part/templates/part/subcategory.html | 2 +- InvenTree/part/templates/part/supplier.html | 2 +- .../report/inventree_build_order_base.html | 2 +- .../report/inventree_report_base.html | 4 ++-- InvenTree/stock/templates/stock/item.html | 2 +- .../stock/templates/stock/item_base.html | 2 +- .../stock/templates/stock/item_tests.html | 2 +- InvenTree/stock/templates/stock/location.html | 10 ++++----- InvenTree/stock/templates/stock/navbar.html | 2 +- .../stock/templates/stock/stock_adjust.html | 4 ++-- .../stock/templates/stock/sublocation.html | 2 +- InvenTree/templates/InvenTree/index.html | 2 +- InvenTree/templates/InvenTree/search.html | 6 ++--- .../InvenTree/settings/category.html | 4 ++-- .../templates/InvenTree/settings/part.html | 2 +- .../InvenTree/settings/settings.html | 2 +- InvenTree/templates/base.html | 12 +++++----- InvenTree/templates/modal_form.html | 4 ++-- InvenTree/templates/modals.html | 2 +- InvenTree/templates/registration/login.html | 10 ++++----- .../registration/password_reset_done.html | 2 +- InvenTree/templates/two_column.html | 2 +- 51 files changed, 103 insertions(+), 103 deletions(-) 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..c336ab9fc1 100644 --- a/InvenTree/build/templates/build/index.html +++ b/InvenTree/build/templates/build/index.html @@ -17,9 +17,9 @@
- +
- +
@@ -66,7 +66,7 @@