removes all lines consisting only of spaces

this really bothers me for some reason - nothing technical
This commit is contained in:
Matthias 2021-05-06 12:11:38 +02:00
parent ecc9eec084
commit f2b0717d10
91 changed files with 494 additions and 494 deletions

View File

@ -83,7 +83,7 @@ class InvenTreeAPITestCase(APITestCase):
self.assertEqual(response.status_code, code) self.assertEqual(response.status_code, code)
return response return response
def post(self, url, data): def post(self, url, data):
""" """
Issue a POST request Issue a POST request

View File

@ -71,7 +71,7 @@ def status_codes(request):
def user_roles(request): def user_roles(request):
""" """
Return a map of the current roles assigned to the user. Return a map of the current roles assigned to the user.
Roles are denoted by their simple names, and then the permission type. Roles are denoted by their simple names, and then the permission type.
Permissions can be access as follows: Permissions can be access as follows:

View File

@ -17,5 +17,5 @@ class InvenTreeManualExchangeBackend(BaseExchangeBackend):
""" """
Do not get any rates... Do not get any rates...
""" """
return {} return {}

View File

@ -102,5 +102,5 @@ class RoundingDecimalField(models.DecimalField):
} }
defaults.update(kwargs) defaults.update(kwargs)
return super().formfield(**kwargs) return super().formfield(**kwargs)

View File

@ -35,7 +35,7 @@ def generateTestKey(test_name):
""" """
Generate a test 'key' for a given 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. 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. Tests must be named such that they will have unique keys.
""" """
@ -102,7 +102,7 @@ def TestIfImageURL(url):
'.tif', '.tiff', '.tif', '.tiff',
'.webp', '.gif', '.webp', '.gif',
] ]
def str2bool(text, test=True): def str2bool(text, test=True):
""" Test if a string 'looks' like a boolean value. """ 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. Test if a string 'looks' like a null value.
This is useful for querying the API against a null key. This is useful for querying the API against a null key.
Args: Args:
text: Input text text: Input text
Returns: Returns:
True if the text looks like a null value True if the text looks like a null value
""" """
@ -157,7 +157,7 @@ def normalize(d):
d = Decimal(d) d = Decimal(d)
d = d.normalize() d = d.normalize()
# Ref: https://docs.python.org/3/library/decimal.html # Ref: https://docs.python.org/3/library/decimal.html
return d.quantize(Decimal(1)) if d == d.to_integral() else d.normalize() return d.quantize(Decimal(1)) if d == d.to_integral() else d.normalize()
@ -165,14 +165,14 @@ def normalize(d):
def increment(n): def increment(n):
""" """
Attempt to increment an integer (or a string that looks like an integer!) Attempt to increment an integer (or a string that looks like an integer!)
e.g. e.g.
001 -> 002 001 -> 002
2 -> 3 2 -> 3
AB01 -> AB02 AB01 -> AB02
QQQ -> QQQ QQQ -> QQQ
""" """
value = str(n).strip() value = str(n).strip()
@ -314,7 +314,7 @@ def MakeBarcode(object_name, object_pk, object_data={}, **kwargs):
def GetExportFormats(): def GetExportFormats():
""" Return a list of allowable file formats for exporting data """ """ Return a list of allowable file formats for exporting data """
return [ return [
'csv', 'csv',
'tsv', 'tsv',
@ -327,7 +327,7 @@ def GetExportFormats():
def DownloadFile(data, filename, content_type='application/text'): def DownloadFile(data, filename, content_type='application/text'):
""" Create a dynamic file for the user to download. """ Create a dynamic file for the user to download.
Args: Args:
data: Raw file data (string or bytes) data: Raw file data (string or bytes)
filename: Filename for the file download filename: Filename for the file download
@ -525,7 +525,7 @@ def addUserPermission(user, permission):
""" """
Shortcut function for adding a certain permission to a user. Shortcut function for adding a certain permission to a user.
""" """
perm = Permission.objects.get(codename=permission) perm = Permission.objects.get(codename=permission)
user.user_permissions.add(perm) user.user_permissions.add(perm)
@ -576,7 +576,7 @@ def getOldestMigrationFile(app, exclude_extension=True, ignore_initial=True):
continue continue
num = int(f.split('_')[0]) num = int(f.split('_')[0])
if oldest_file is None or num < oldest_num: if oldest_file is None or num < oldest_num:
oldest_num = num oldest_num = num
oldest_file = f oldest_file = f
@ -585,7 +585,7 @@ def getOldestMigrationFile(app, exclude_extension=True, ignore_initial=True):
oldest_file = oldest_file.replace('.py', '') oldest_file = oldest_file.replace('.py', '')
return oldest_file return oldest_file
def getNewestMigrationFile(app, exclude_extension=True): def getNewestMigrationFile(app, exclude_extension=True):
""" """

View File

@ -129,7 +129,7 @@ class InvenTreeTree(MPTTModel):
Here an 'item' is considered to be the 'leaf' at the end of each branch, 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. and the exact nature here will depend on the class implementation.
The default implementation returns zero The default implementation returns zero
""" """
return 0 return 0

View File

@ -17,7 +17,7 @@ class RolePermission(permissions.BasePermission):
- PUT - PUT
- PATCH - PATCH
- DELETE - DELETE
Specify the required "role" using the role_required attribute. Specify the required "role" using the role_required attribute.
e.g. e.g.

View File

@ -44,7 +44,7 @@ class InvenTreeModelSerializer(serializers.ModelSerializer):
In addition to running validators on the serializer fields, In addition to running validators on the serializer fields,
this class ensures that the underlying model is also validated. this class ensures that the underlying model is also validated.
""" """
# Run any native validation checks first (may throw an ValidationError) # Run any native validation checks first (may throw an ValidationError)
data = super(serializers.ModelSerializer, self).validate(data) data = super(serializers.ModelSerializer, self).validate(data)

View File

@ -64,7 +64,7 @@ def is_email_configured():
if not settings.EMAIL_HOST_USER: if not settings.EMAIL_HOST_USER:
configured = False configured = False
# Display warning unless in test mode # Display warning unless in test mode
if not settings.TESTING: if not settings.TESTING:
logger.warning("EMAIL_HOST_USER is not configured") logger.warning("EMAIL_HOST_USER is not configured")

View File

@ -16,7 +16,7 @@ class StatusCode:
# If the key cannot be found, pass it back # If the key cannot be found, pass it back
if key not in cls.options.keys(): if key not in cls.options.keys():
return key return key
value = cls.options.get(key, key) value = cls.options.get(key, key)
color = cls.colors.get(key, 'grey') color = cls.colors.get(key, 'grey')

View File

@ -119,7 +119,7 @@ class APITests(InvenTreeAPITestCase):
self.assertNotIn('add', roles[rule]) self.assertNotIn('add', roles[rule])
self.assertNotIn('change', roles[rule]) self.assertNotIn('change', roles[rule])
self.assertNotIn('delete', roles[rule]) self.assertNotIn('delete', roles[rule])
def test_with_superuser(self): def test_with_superuser(self):
""" """
Superuser should have *all* roles assigned Superuser should have *all* roles assigned

View File

@ -37,7 +37,7 @@ class ScheduledTaskTests(TestCase):
# Attempt to schedule the same task again # Attempt to schedule the same task again
InvenTree.tasks.schedule_task(task, schedule_type=Schedule.MINUTES, minutes=5) InvenTree.tasks.schedule_task(task, schedule_type=Schedule.MINUTES, minutes=5)
self.assertEqual(self.get_tasks(task).count(), 1) self.assertEqual(self.get_tasks(task).count(), 1)
# But the 'minutes' should have been updated # But the 'minutes' should have been updated
t = Schedule.objects.get(func=task) t = Schedule.objects.get(func=task)
self.assertEqual(t.minutes, 5) self.assertEqual(t.minutes, 5)

View File

@ -97,7 +97,7 @@ class TestHelpers(TestCase):
self.assertEqual(helpers.getMediaUrl('xx/yy.png'), '/media/xx/yy.png') self.assertEqual(helpers.getMediaUrl('xx/yy.png'), '/media/xx/yy.png')
def testDecimal2String(self): def testDecimal2String(self):
self.assertEqual(helpers.decimal2string(Decimal('1.2345000')), '1.2345') self.assertEqual(helpers.decimal2string(Decimal('1.2345000')), '1.2345')
self.assertEqual(helpers.decimal2string('test'), 'test') self.assertEqual(helpers.decimal2string('test'), 'test')
@ -205,7 +205,7 @@ class TestMPTT(TestCase):
child = StockLocation.objects.get(pk=5) child = StockLocation.objects.get(pk=5)
parent.parent = child parent.parent = child
with self.assertRaises(InvalidMove): with self.assertRaises(InvalidMove):
parent.save() parent.save()
@ -223,7 +223,7 @@ class TestMPTT(TestCase):
drawer.save() drawer.save()
self.assertNotEqual(tree, drawer.tree_id) self.assertNotEqual(tree, drawer.tree_id)
class TestSerialNumberExtraction(TestCase): class TestSerialNumberExtraction(TestCase):
""" Tests for serial number extraction code """ """ Tests for serial number extraction code """

View File

@ -81,7 +81,7 @@ settings_urls = [
url(r'^user/?', SettingsView.as_view(template_name='InvenTree/settings/user.html'), name='settings-user'), 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'^appearance/?', AppearanceSelectView.as_view(), name='settings-appearance'),
url(r'^i18n/?', include('django.conf.urls.i18n')), 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'^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'^report/?', SettingsView.as_view(template_name='InvenTree/settings/report.html'), name='settings-report'),
url(r'^category/?', SettingCategorySelectView.as_view(), name='settings-category'), 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'^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'^logout/', auth_views.LogoutView.as_view(template_name='registration/logged_out.html'), name='logout'),
url(r'^settings/', include(settings_urls)), url(r'^settings/', include(settings_urls)),
url(r'^edit-user/', EditUserView.as_view(), name='edit-user'), url(r'^edit-user/', EditUserView.as_view(), name='edit-user'),

View File

@ -130,7 +130,7 @@ def validate_overage(value):
if i < 0: if i < 0:
raise ValidationError(_("Overage value must not be negative")) raise ValidationError(_("Overage value must not be negative"))
# Looks like an integer! # Looks like an integer!
return True return True
except ValueError: except ValueError:

View File

@ -176,7 +176,7 @@ class InvenTreeRoleMixin(PermissionRequiredMixin):
if role not in RuleSet.RULESET_NAMES: if role not in RuleSet.RULESET_NAMES:
raise ValueError(f"Role '{role}' is not a valid role") raise ValueError(f"Role '{role}' is not a valid role")
if permission not in RuleSet.RULESET_PERMISSIONS: if permission not in RuleSet.RULESET_PERMISSIONS:
raise ValueError(f"Permission '{permission}' is not a valid permission") 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. Return the 'permission_class' required for the current View.
Must be one of: Must be one of:
- view - view
- change - change
- add - add
@ -389,7 +389,7 @@ class QRCodeView(AjaxView):
""" """
ajax_template_name = "qr_code.html" ajax_template_name = "qr_code.html"
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
self.request = request self.request = request
self.pk = self.kwargs['pk'] self.pk = self.kwargs['pk']
@ -398,7 +398,7 @@ class QRCodeView(AjaxView):
def get_qr_data(self): def get_qr_data(self):
""" Returns the text object to render to a QR code. """ Returns the text object to render to a QR code.
The actual rendering will be handled by the template """ The actual rendering will be handled by the template """
return None return None
def get_context_data(self): def get_context_data(self):
@ -406,7 +406,7 @@ class QRCodeView(AjaxView):
Explicity passes the parameter 'qr_data' Explicity passes the parameter 'qr_data'
""" """
context = {} context = {}
qr = self.get_qr_data() qr = self.get_qr_data()
@ -415,7 +415,7 @@ class QRCodeView(AjaxView):
context['qr_data'] = qr context['qr_data'] = qr
else: else:
context['error_msg'] = 'Error generating QR code' context['error_msg'] = 'Error generating QR code'
return context return context
@ -507,7 +507,7 @@ class AjaxUpdateView(AjaxMixin, UpdateView):
""" """
super(UpdateView, self).get(request, *args, **kwargs) super(UpdateView, self).get(request, *args, **kwargs)
return self.renderJsonResponse(request, self.get_form(), context=self.get_context_data()) return self.renderJsonResponse(request, self.get_form(), context=self.get_context_data())
def save(self, object, form, **kwargs): def save(self, object, form, **kwargs):
@ -673,7 +673,7 @@ class SetPasswordView(AjaxUpdateView):
p1 = request.POST.get('enter_password', '') p1 = request.POST.get('enter_password', '')
p2 = request.POST.get('confirm_password', '') p2 = request.POST.get('confirm_password', '')
if valid: if valid:
# Passwords must match # Passwords must match
@ -712,7 +712,7 @@ class IndexView(TemplateView):
# Generate a list of orderable parts which have stock below their minimum values # 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 # 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()] # 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 # 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 # 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()] # 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 = "" template_name = ""
content_type = 'text/javascript' content_type = 'text/javascript'
class SettingsView(TemplateView): class SettingsView(TemplateView):
""" View for configuring User settings """ View for configuring User settings
@ -830,7 +830,7 @@ class AppearanceSelectView(FormView):
if form.is_valid(): if form.is_valid():
theme_selected = form.cleaned_data['name'] theme_selected = form.cleaned_data['name']
# Set color theme to form selection # Set color theme to form selection
user_theme.name = theme_selected user_theme.name = theme_selected
user_theme.save() user_theme.save()
@ -893,7 +893,7 @@ class DatabaseStatsView(AjaxView):
# Part stats # Part stats
ctx['part_count'] = Part.objects.count() ctx['part_count'] = Part.objects.count()
ctx['part_cat_count'] = PartCategory.objects.count() ctx['part_cat_count'] = PartCategory.objects.count()
# Stock stats # Stock stats
ctx['stock_item_count'] = StockItem.objects.count() ctx['stock_item_count'] = StockItem.objects.count()
ctx['stock_loc_count'] = StockLocation.objects.count() ctx['stock_loc_count'] = StockLocation.objects.count()

View File

@ -73,7 +73,7 @@ class BarcodeScan(APIView):
# A plugin has been found! # A plugin has been found!
if plugin is not None: if plugin is not None:
# Try to associate with a stock item # Try to associate with a stock item
item = plugin.getStockItem() item = plugin.getStockItem()
@ -133,7 +133,7 @@ class BarcodeScan(APIView):
class BarcodeAssign(APIView): class BarcodeAssign(APIView):
""" """
Endpoint for assigning a barcode to a stock item. 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 - 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 - 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 # Matching plugin was found
if plugin is not None: if plugin is not None:
hash = plugin.hash() hash = plugin.hash()
response['hash'] = hash response['hash'] = hash
response['plugin'] = plugin.name response['plugin'] = plugin.name
@ -234,7 +234,7 @@ class BarcodeAssign(APIView):
barcode_api_urls = [ barcode_api_urls = [
url(r'^link/$', BarcodeAssign.as_view(), name='api-barcode-link'), url(r'^link/$', BarcodeAssign.as_view(), name='api-barcode-link'),
# Catch-all performs barcode 'scan' # Catch-all performs barcode 'scan'
url(r'^.*$', BarcodeScan.as_view(), name='api-barcode-scan'), url(r'^.*$', BarcodeScan.as_view(), name='api-barcode-scan'),
] ]

View File

@ -21,7 +21,7 @@ def hash_barcode(barcode_data):
HACK: Remove any 'non printable' characters from the hash, HACK: Remove any 'non printable' characters from the hash,
as it seems browers will remove special control characters... as it seems browers will remove special control characters...
TODO: Work out a way around this! TODO: Work out a way around this!
""" """

View File

@ -92,7 +92,7 @@ class BarcodeAPITest(APITestCase):
data = response.data data = response.data
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertIn('stockitem', data) self.assertIn('stockitem', data)
pk = data['stockitem']['pk'] pk = data['stockitem']['pk']
@ -121,7 +121,7 @@ class BarcodeAPITest(APITestCase):
data = response.data data = response.data
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertIn('success', data) self.assertIn('success', data)
hash = data['hash'] hash = data['hash']

View File

@ -20,7 +20,7 @@ from .serializers import BuildSerializer, BuildItemSerializer
class BuildList(generics.ListCreateAPIView): class BuildList(generics.ListCreateAPIView):
""" API endpoint for accessing a list of Build objects. """ API endpoint for accessing a list of Build objects.
- GET: Return list of objects (with filters) - GET: Return list of objects (with filters)
- POST: Create a new Build object - POST: Create a new Build object
""" """
@ -65,7 +65,7 @@ class BuildList(generics.ListCreateAPIView):
queryset = BuildSerializer.annotate_queryset(queryset) queryset = BuildSerializer.annotate_queryset(queryset)
return queryset return queryset
def filter_queryset(self, queryset): def filter_queryset(self, queryset):
queryset = super().filter_queryset(queryset) queryset = super().filter_queryset(queryset)

View File

@ -118,7 +118,7 @@ class Build(MPTTModel):
def get_absolute_url(self): def get_absolute_url(self):
return reverse('build-detail', kwargs={'pk': self.id}) return reverse('build-detail', kwargs={'pk': self.id})
reference = models.CharField( reference = models.CharField(
unique=True, unique=True,
max_length=64, max_length=64,
@ -168,7 +168,7 @@ class Build(MPTTModel):
null=True, blank=True, null=True, blank=True,
help_text=_('SalesOrder to which this build is allocated') help_text=_('SalesOrder to which this build is allocated')
) )
take_from = models.ForeignKey( take_from = models.ForeignKey(
'stock.StockLocation', 'stock.StockLocation',
verbose_name=_('Source Location'), verbose_name=_('Source Location'),
@ -177,7 +177,7 @@ class Build(MPTTModel):
null=True, blank=True, null=True, blank=True,
help_text=_('Select location to take stock from for this build (leave blank to take from any stock location)') help_text=_('Select location to take stock from for this build (leave blank to take from any stock location)')
) )
destination = models.ForeignKey( destination = models.ForeignKey(
'stock.StockLocation', 'stock.StockLocation',
verbose_name=_('Destination Location'), verbose_name=_('Destination Location'),
@ -207,7 +207,7 @@ class Build(MPTTModel):
validators=[MinValueValidator(0)], validators=[MinValueValidator(0)],
help_text=_('Build status code') help_text=_('Build status code')
) )
batch = models.CharField( batch = models.CharField(
verbose_name=_('Batch Code'), verbose_name=_('Batch Code'),
max_length=100, max_length=100,
@ -215,9 +215,9 @@ class Build(MPTTModel):
null=True, null=True,
help_text=_('Batch code for this build output') help_text=_('Batch code for this build output')
) )
creation_date = models.DateField(auto_now_add=True, editable=False, verbose_name=_('Creation Date')) creation_date = models.DateField(auto_now_add=True, editable=False, verbose_name=_('Creation Date'))
target_date = models.DateField( target_date = models.DateField(
null=True, blank=True, null=True, blank=True,
verbose_name=_('Target completion date'), verbose_name=_('Target completion date'),
@ -251,7 +251,7 @@ class Build(MPTTModel):
help_text=_('User responsible for this build order'), help_text=_('User responsible for this build order'),
related_name='builds_responsible', related_name='builds_responsible',
) )
link = InvenTree.fields.InvenTreeURLField( link = InvenTree.fields.InvenTreeURLField(
verbose_name=_('External Link'), verbose_name=_('External Link'),
blank=True, help_text=_('Link to external URL') blank=True, help_text=_('Link to external URL')
@ -272,7 +272,7 @@ class Build(MPTTModel):
else: else:
descendants = self.get_descendants(include_self=True) descendants = self.get_descendants(include_self=True)
Build.objects.filter(parent__pk__in=[d.pk for d in descendants]) Build.objects.filter(parent__pk__in=[d.pk for d in descendants])
def sub_build_count(self, cascade=True): def sub_build_count(self, cascade=True):
""" """
Return the number of sub builds under this one. Return the number of sub builds under this one.
@ -295,7 +295,7 @@ class Build(MPTTModel):
query = query.filter(Build.OVERDUE_FILTER) query = query.filter(Build.OVERDUE_FILTER)
return query.exists() return query.exists()
@property @property
def active(self): def active(self):
""" """
@ -441,7 +441,7 @@ class Build(MPTTModel):
# Extract the "most recent" build order reference # Extract the "most recent" build order reference
builds = cls.objects.exclude(reference=None) builds = cls.objects.exclude(reference=None)
if not builds.exists(): if not builds.exists():
return None return None
@ -543,7 +543,7 @@ class Build(MPTTModel):
- The sub_item in the BOM line must *not* be trackable - 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) - 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 - The stock item has an availability greater than zero
Returns: Returns:
A list object containing the StockItem objects to be allocated (and the quantities). A list object containing the StockItem objects to be allocated (and the quantities).
Each item in the list is a dict as follows: Each item in the list is a dict as follows:
@ -648,7 +648,7 @@ class Build(MPTTModel):
""" """
Deletes all stock allocations for this build. Deletes all stock allocations for this build.
""" """
allocations = BuildItem.objects.filter(build=self) allocations = BuildItem.objects.filter(build=self)
allocations.delete() allocations.delete()
@ -1145,7 +1145,7 @@ class BuildItem(models.Model):
""" """
self.validate_unique() self.validate_unique()
super().clean() super().clean()
errors = {} errors = {}
@ -1159,7 +1159,7 @@ class BuildItem(models.Model):
# Allocated part must be in the BOM for the master part # Allocated part must be in the BOM for the master part
if self.stock_item.part not in self.build.part.getRequiredParts(recursive=False): 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)] 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 # Allocated quantity cannot exceed available stock quantity
if self.quantity > self.stock_item.quantity: if self.quantity > self.stock_item.quantity:
errors['quantity'] = [_("Allocated quantity ({n}) must not exceed available quantity ({q})").format( errors['quantity'] = [_("Allocated quantity ({n}) must not exceed available quantity ({q})").format(

View File

@ -30,7 +30,7 @@ class BuildAPITest(InvenTreeAPITestCase):
'build.change', 'build.change',
'build.add' 'build.add'
] ]
def setUp(self): def setUp(self):
super().setUp() super().setUp()
@ -54,7 +54,7 @@ class BuildListTest(BuildAPITest):
builds = self.get(self.url, data={'active': True}) builds = self.get(self.url, data={'active': True})
self.assertEqual(len(builds.data), 1) self.assertEqual(len(builds.data), 1)
builds = self.get(self.url, data={'status': BuildStatus.COMPLETE}) builds = self.get(self.url, data={'status': BuildStatus.COMPLETE})
self.assertEqual(len(builds.data), 4) self.assertEqual(len(builds.data), 4)

View File

@ -114,7 +114,7 @@ class BuildTest(TestCase):
# Perform some basic tests before we start the ball rolling # Perform some basic tests before we start the ball rolling
self.assertEqual(StockItem.objects.count(), 6) self.assertEqual(StockItem.objects.count(), 6)
# Build is PENDING # Build is PENDING
self.assertEqual(self.build.status, status.BuildStatus.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 # Create a BuiltItem which points to an invalid StockItem
b = BuildItem(stock_item=stock, build=self.build, quantity=10) b = BuildItem(stock_item=stock, build=self.build, quantity=10)
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
b.save() b.save()
@ -339,7 +339,7 @@ class BuildTest(TestCase):
self.assertTrue(self.build.can_complete) self.assertTrue(self.build.can_complete)
self.build.complete_build(None) self.build.complete_build(None)
self.assertEqual(self.build.status, status.BuildStatus.COMPLETE) self.assertEqual(self.build.status, status.BuildStatus.COMPLETE)
# the original BuildItem objects should have been deleted! # the original BuildItem objects should have been deleted!
@ -351,12 +351,12 @@ class BuildTest(TestCase):
# This stock item has been depleted! # This stock item has been depleted!
with self.assertRaises(StockItem.DoesNotExist): with self.assertRaises(StockItem.DoesNotExist):
StockItem.objects.get(pk=self.stock_1_1.pk) StockItem.objects.get(pk=self.stock_1_1.pk)
# This stock item has *not* been depleted # This stock item has *not* been depleted
x = StockItem.objects.get(pk=self.stock_2_1.pk) x = StockItem.objects.get(pk=self.stock_2_1.pk)
self.assertEqual(x.quantity, 4970) self.assertEqual(x.quantity, 4970)
# And 10 new stock items created for the build output # And 10 new stock items created for the build output
outputs = StockItem.objects.filter(build=self.build) outputs = StockItem.objects.filter(build=self.build)

View File

@ -251,7 +251,7 @@ class TestBuildViews(TestCase):
content = str(response.content) content = str(response.content)
self.assertIn(build.title, content) self.assertIn(build.title, content)
def test_build_create(self): def test_build_create(self):
""" Test the build creation view (ajax form) """ """ Test the build creation view (ajax form) """
@ -260,7 +260,7 @@ class TestBuildViews(TestCase):
# Create build without specifying part # Create build without specifying part
response = self.client.get(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest') response = self.client.get(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
# Create build with valid part # Create build with valid part
response = self.client.get(url, {'part': 1}, HTTP_X_REQUESTED_WITH='XMLHttpRequest') response = self.client.get(url, {'part': 1}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
@ -281,7 +281,7 @@ class TestBuildViews(TestCase):
# Get the page in editing mode # Get the page in editing mode
response = self.client.get(url, {'edit': 1}) response = self.client.get(url, {'edit': 1})
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
def test_build_item_create(self): def test_build_item_create(self):
""" Test the BuildItem creation view (ajax form) """ """ Test the BuildItem creation view (ajax form) """
@ -305,7 +305,7 @@ class TestBuildViews(TestCase):
def test_build_item_edit(self): def test_build_item_edit(self):
""" Test the BuildItem edit view (ajax form) """ """ Test the BuildItem edit view (ajax form) """
# TODO # TODO
# url = reverse('build-item-edit') # url = reverse('build-item-edit')
pass pass
@ -323,7 +323,7 @@ class TestBuildViews(TestCase):
# Test without confirmation # Test without confirmation
response = self.client.post(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest') response = self.client.post(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
data = json.loads(response.content) data = json.loads(response.content)
self.assertFalse(data['form_valid']) self.assertFalse(data['form_valid'])
@ -353,7 +353,7 @@ class TestBuildViews(TestCase):
# Test with confirmation, invalid location # Test with confirmation, invalid location
response = self.client.post(url, {'confirm': 1, 'location': 9999}, HTTP_X_REQUESTED_WITH='XMLHttpRequest') response = self.client.post(url, {'confirm': 1, 'location': 9999}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
data = json.loads(response.content) data = json.loads(response.content)
self.assertFalse(data['form_valid']) self.assertFalse(data['form_valid'])
@ -365,7 +365,7 @@ class TestBuildViews(TestCase):
# Test without confirmation # Test without confirmation
response = self.client.post(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest') response = self.client.post(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
data = json.loads(response.content) data = json.loads(response.content)
self.assertFalse(data['form_valid']) self.assertFalse(data['form_valid'])
@ -393,7 +393,7 @@ class TestBuildViews(TestCase):
data = json.loads(response.content) data = json.loads(response.content)
self.assertFalse(data['form_valid']) self.assertFalse(data['form_valid'])
# Test with confirmation # Test with confirmation
response = self.client.post(url, {'confirm': 1}, HTTP_X_REQUESTED_WITH='XMLHttpRequest') response = self.client.post(url, {'confirm': 1}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)

View File

@ -159,7 +159,7 @@ class BuildOutputCreate(AjaxUpdateView):
if quantity: if quantity:
build = self.get_object() build = self.get_object()
# Check that requested output don't exceed build remaining quantity # Check that requested output don't exceed build remaining quantity
maximum_output = int(build.remaining - build.incomplete_count) maximum_output = int(build.remaining - build.incomplete_count)
if quantity > maximum_output: if quantity > maximum_output:
@ -318,7 +318,7 @@ class BuildUnallocate(AjaxUpdateView):
form_class = forms.UnallocateBuildForm form_class = forms.UnallocateBuildForm
ajax_form_title = _("Unallocate Stock") ajax_form_title = _("Unallocate Stock")
ajax_template_name = "build/unallocate.html" ajax_template_name = "build/unallocate.html"
def get_initial(self): def get_initial(self):
initials = super().get_initial() initials = super().get_initial()
@ -341,7 +341,7 @@ class BuildUnallocate(AjaxUpdateView):
build = self.get_object() build = self.get_object()
form = self.get_form() form = self.get_form()
confirm = request.POST.get('confirm', False) confirm = request.POST.get('confirm', False)
output_id = request.POST.get('output_id', None) output_id = request.POST.get('output_id', None)
@ -382,7 +382,7 @@ class BuildUnallocate(AjaxUpdateView):
# Unallocate "untracked" parts # Unallocate "untracked" parts
else: else:
build.unallocateUntracked(part=part) build.unallocateUntracked(part=part)
data = { data = {
'form_valid': valid, 'form_valid': valid,
} }
@ -401,7 +401,7 @@ class BuildComplete(AjaxUpdateView):
model = Build model = Build
form_class = forms.CompleteBuildForm form_class = forms.CompleteBuildForm
ajax_form_title = _('Complete Build Order') ajax_form_title = _('Complete Build Order')
ajax_template_name = 'build/complete.html' ajax_template_name = 'build/complete.html'
@ -437,9 +437,9 @@ class BuildOutputComplete(AjaxUpdateView):
context_object_name = "build" context_object_name = "build"
ajax_form_title = _("Complete Build Output") ajax_form_title = _("Complete Build Output")
ajax_template_name = "build/complete_output.html" ajax_template_name = "build/complete_output.html"
def get_form(self): def get_form(self):
build = self.get_object() build = self.get_object()
form = super().get_form() form = super().get_form()
@ -500,7 +500,7 @@ class BuildOutputComplete(AjaxUpdateView):
- If the part being built has a default location, pre-select that location - If the part being built has a default location, pre-select that location
""" """
initials = super().get_initial() initials = super().get_initial()
build = self.get_object() build = self.get_object()
@ -585,7 +585,7 @@ class BuildOutputComplete(AjaxUpdateView):
location=location, location=location,
status=stock_status, status=stock_status,
) )
def get_data(self): def get_data(self):
""" Provide feedback data back to the form """ """ Provide feedback data back to the form """
return { return {
@ -600,7 +600,7 @@ class BuildNotes(InvenTreeRoleMixin, UpdateView):
context_object_name = 'build' context_object_name = 'build'
template_name = 'build/notes.html' template_name = 'build/notes.html'
model = Build model = Build
# Override the default permission role for this View # Override the default permission role for this View
role_required = 'build.view' role_required = 'build.view'
@ -612,7 +612,7 @@ class BuildNotes(InvenTreeRoleMixin, UpdateView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs) ctx = super().get_context_data(**kwargs)
ctx['editing'] = str2bool(self.request.GET.get('edit', '')) ctx['editing'] = str2bool(self.request.GET.get('edit', ''))
return ctx return ctx
@ -746,7 +746,7 @@ class BuildCreate(AjaxCreateView):
class BuildUpdate(AjaxUpdateView): class BuildUpdate(AjaxUpdateView):
""" View for editing a Build object """ """ View for editing a Build object """
model = Build model = Build
form_class = forms.EditBuildForm form_class = forms.EditBuildForm
context_object_name = 'build' context_object_name = 'build'
@ -804,7 +804,7 @@ class BuildItemDelete(AjaxDeleteView):
ajax_template_name = 'build/delete_build_item.html' ajax_template_name = 'build/delete_build_item.html'
ajax_form_title = _('Unallocate Stock') ajax_form_title = _('Unallocate Stock')
context_object_name = 'item' context_object_name = 'item'
def get_data(self): def get_data(self):
return { return {
'danger': _('Removed parts from build allocation') 'danger': _('Removed parts from build allocation')
@ -826,7 +826,7 @@ class BuildItemCreate(AjaxCreateView):
# The "part" which is being allocated to the output # The "part" which is being allocated to the output
part = None part = None
available_stock = None available_stock = None
def get_context_data(self): def get_context_data(self):
@ -906,7 +906,7 @@ class BuildItemCreate(AjaxCreateView):
if part_id: if part_id:
try: try:
self.part = Part.objects.get(pk=part_id) self.part = Part.objects.get(pk=part_id)
except (ValueError, Part.DoesNotExist): except (ValueError, Part.DoesNotExist):
pass pass
@ -958,7 +958,7 @@ class BuildItemCreate(AjaxCreateView):
# Reference to a StockItem object # Reference to a StockItem object
item = None item = None
# Reference to a Build object # Reference to a Build object
build = None build = None
@ -999,7 +999,7 @@ class BuildItemCreate(AjaxCreateView):
quantity = float(quantity) quantity = float(quantity)
elif required_quantity is not None: elif required_quantity is not None:
quantity = required_quantity quantity = required_quantity
item_id = self.get_param('item') item_id = self.get_param('item')
# If the request specifies a particular StockItem # If the request specifies a particular StockItem
@ -1035,7 +1035,7 @@ class BuildItemEdit(AjaxUpdateView):
ajax_template_name = 'build/edit_build_item.html' ajax_template_name = 'build/edit_build_item.html'
form_class = forms.EditBuildItemForm form_class = forms.EditBuildItemForm
ajax_form_title = _('Edit Stock Allocation') ajax_form_title = _('Edit Stock Allocation')
def get_data(self): def get_data(self):
return { return {
'info': _('Updated Build Item'), 'info': _('Updated Build Item'),
@ -1068,7 +1068,7 @@ class BuildAttachmentCreate(AjaxCreateView):
model = BuildOrderAttachment model = BuildOrderAttachment
form_class = forms.EditBuildAttachmentForm form_class = forms.EditBuildAttachmentForm
ajax_form_title = _('Add Build Order Attachment') ajax_form_title = _('Add Build Order Attachment')
def save(self, form, **kwargs): def save(self, form, **kwargs):
""" """
Add information on the user that uploaded the attachment Add information on the user that uploaded the attachment
@ -1105,7 +1105,7 @@ class BuildAttachmentCreate(AjaxCreateView):
form = super().get_form() form = super().get_form()
form.fields['build'].widget = HiddenInput() form.fields['build'].widget = HiddenInput()
return form return form

View File

@ -9,7 +9,7 @@ from .models import InvenTreeSetting
class SettingsAdmin(ImportExportModelAdmin): class SettingsAdmin(ImportExportModelAdmin):
list_display = ('key', 'value') list_display = ('key', 'value')

View File

@ -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. The key of each item is the name of the value as it appears in the database.
Each global setting has the following parameters: Each global setting has the following parameters:
- name: Translatable string name of the setting (required) - name: Translatable string name of the setting (required)
- description: Translatable string description of the setting (required) - description: Translatable string description of the setting (required)
- default: Default value (optional) - default: Default value (optional)
@ -412,7 +412,7 @@ class InvenTreeSetting(models.Model):
# Evaluate the function (we expect it will return a list of tuples...) # Evaluate the function (we expect it will return a list of tuples...)
return choices() return choices()
""" """
return choices return choices
@classmethod @classmethod
@ -522,7 +522,7 @@ class InvenTreeSetting(models.Model):
# Enforce standard boolean representation # Enforce standard boolean representation
if setting.is_bool(): if setting.is_bool():
value = InvenTree.helpers.str2bool(value) value = InvenTree.helpers.str2bool(value)
setting.value = str(value) setting.value = str(value)
setting.save() setting.save()
@ -664,7 +664,7 @@ class InvenTreeSetting(models.Model):
if validator == int: if validator == int:
return True return True
if type(validator) in [list, tuple]: if type(validator) in [list, tuple]:
for v in validator: for v in validator:
if v == int: if v == int:
@ -675,7 +675,7 @@ class InvenTreeSetting(models.Model):
def as_int(self): def as_int(self):
""" """
Return the value of this setting converted to a boolean value. Return the value of this setting converted to a boolean value.
If an error occurs, return the default value If an error occurs, return the default value
""" """
@ -685,7 +685,7 @@ class InvenTreeSetting(models.Model):
value = self.default_value() value = self.default_value()
return value return value
class PriceBreak(models.Model): class PriceBreak(models.Model):
""" """

View File

@ -19,7 +19,7 @@ def currency_code_default():
if code not in CURRENCIES: if code not in CURRENCIES:
code = 'USD' code = 'USD'
return code return code

View File

@ -117,7 +117,7 @@ class SettingsViewTest(TestCase):
""" """
Test for binary value Test for binary value
""" """
setting = InvenTreeSetting.get_setting_object('PART_COMPONENT') setting = InvenTreeSetting.get_setting_object('PART_COMPONENT')
self.assertTrue(setting.as_bool()) self.assertTrue(setting.as_bool())

View File

@ -19,7 +19,7 @@ class SettingsTest(TestCase):
def setUp(self): def setUp(self):
user = get_user_model() user = get_user_model()
self.user = user.objects.create_user('username', 'user@email.com', 'password') self.user = user.objects.create_user('username', 'user@email.com', 'password')
self.user.is_staff = True self.user.is_staff = True
self.user.save() self.user.save()

View File

@ -48,7 +48,7 @@ class SettingEdit(AjaxUpdateView):
""" """
form = super().get_form() form = super().get_form()
setting = self.get_object() setting = self.get_object()
choices = setting.choices() choices = setting.choices()

View File

@ -41,7 +41,7 @@ class CompanyList(generics.ListCreateAPIView):
queryset = CompanySerializer.annotate_queryset(queryset) queryset = CompanySerializer.annotate_queryset(queryset)
return queryset return queryset
filter_backends = [ filter_backends = [
DjangoFilterBackend, DjangoFilterBackend,
filters.SearchFilter, filters.SearchFilter,
@ -116,7 +116,7 @@ class ManufacturerPartList(generics.ListCreateAPIView):
kwargs['pretty'] = str2bool(self.request.query_params.get('pretty', None)) kwargs['pretty'] = str2bool(self.request.query_params.get('pretty', None))
except AttributeError: except AttributeError:
pass pass
kwargs['context'] = self.get_serializer_context() kwargs['context'] = self.get_serializer_context()
return self.serializer_class(*args, **kwargs) return self.serializer_class(*args, **kwargs)
@ -167,7 +167,7 @@ class ManufacturerPartList(generics.ListCreateAPIView):
'part__name', 'part__name',
'part__description', 'part__description',
] ]
class ManufacturerPartDetail(generics.RetrieveUpdateDestroyAPIView): class ManufacturerPartDetail(generics.RetrieveUpdateDestroyAPIView):
""" API endpoint for detail view of ManufacturerPart object """ 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)) kwargs['part_detail'] = str2bool(self.request.query_params.get('part_detail', None))
except AttributeError: except AttributeError:
pass pass
try: try:
kwargs['supplier_detail'] = str2bool(self.request.query_params.get('supplier_detail', None)) kwargs['supplier_detail'] = str2bool(self.request.query_params.get('supplier_detail', None))
except AttributeError: except AttributeError:
@ -270,7 +270,7 @@ class SupplierPartList(generics.ListCreateAPIView):
kwargs['pretty'] = str2bool(self.request.query_params.get('pretty', None)) kwargs['pretty'] = str2bool(self.request.query_params.get('pretty', None))
except AttributeError: except AttributeError:
pass pass
kwargs['context'] = self.get_serializer_context() kwargs['context'] = self.get_serializer_context()
return self.serializer_class(*args, **kwargs) return self.serializer_class(*args, **kwargs)

View File

@ -158,7 +158,7 @@ class EditSupplierPartForm(HelperForm):
empty_choice = [('', '----------')] empty_choice = [('', '----------')]
manufacturers = [(manufacturer.id, manufacturer.name) for manufacturer in Company.objects.filter(is_manufacturer=True)] manufacturers = [(manufacturer.id, manufacturer.name) for manufacturer in Company.objects.filter(is_manufacturer=True)]
return empty_choice + manufacturers return empty_choice + manufacturers
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):

View File

@ -152,7 +152,7 @@ class Company(models.Model):
def currency_code(self): def currency_code(self):
""" """
Return the currency code associated with this company. Return the currency code associated with this company.
- If the currency code is invalid, use the default currency - If the currency code is invalid, use the default currency
- If the currency code is not specified, 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) return getMediaUrl(self.image.thumbnail.url)
else: else:
return getBlankThumbnail() return getBlankThumbnail()
@property @property
def manufactured_part_count(self): def manufactured_part_count(self):
""" The number of parts manufactured by this company """ """ The number of parts manufactured by this company """
@ -302,7 +302,7 @@ class ManufacturerPart(models.Model):
class Meta: class Meta:
unique_together = ('part', 'manufacturer', 'MPN') unique_together = ('part', 'manufacturer', 'MPN')
part = models.ForeignKey('part.Part', on_delete=models.CASCADE, part = models.ForeignKey('part.Part', on_delete=models.CASCADE,
related_name='manufacturer_parts', related_name='manufacturer_parts',
verbose_name=_('Base Part'), verbose_name=_('Base Part'),
@ -311,7 +311,7 @@ class ManufacturerPart(models.Model):
}, },
help_text=_('Select part'), help_text=_('Select part'),
) )
manufacturer = models.ForeignKey( manufacturer = models.ForeignKey(
Company, Company,
on_delete=models.CASCADE, on_delete=models.CASCADE,
@ -359,7 +359,7 @@ class ManufacturerPart(models.Model):
if not manufacturer_part: if not manufacturer_part:
manufacturer_part = ManufacturerPart(part=part, manufacturer=manufacturer, MPN=mpn, description=description, link=link) manufacturer_part = ManufacturerPart(part=part, manufacturer=manufacturer, MPN=mpn, description=description, link=link)
manufacturer_part.save() manufacturer_part.save()
return manufacturer_part return manufacturer_part
def __str__(self): def __str__(self):
@ -414,7 +414,7 @@ class SupplierPart(models.Model):
MPN = kwargs.pop('MPN') MPN = kwargs.pop('MPN')
else: else:
MPN = None MPN = None
if manufacturer or MPN: if manufacturer or MPN:
if not self.manufacturer_part: if not self.manufacturer_part:
# Create ManufacturerPart # Create ManufacturerPart
@ -429,7 +429,7 @@ class SupplierPart(models.Model):
manufacturer_part_id = self.manufacturer_part.id manufacturer_part_id = self.manufacturer_part.id
except AttributeError: except AttributeError:
manufacturer_part_id = None manufacturer_part_id = None
if manufacturer_part_id: if manufacturer_part_id:
try: try:
(manufacturer_part, created) = ManufacturerPart.objects.update_or_create(part=self.part, (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)')) 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')) 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')) 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). # 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) pb_cost = pb_min.convert_to(currency)
# Trigger cost calculation using smallest price break # Trigger cost calculation using smallest price break
pb_found = True pb_found = True
# Convert quantity to decimal.Decimal format # Convert quantity to decimal.Decimal format
quantity = decimal.Decimal(f'{quantity}') quantity = decimal.Decimal(f'{quantity}')
@ -669,7 +669,7 @@ class SupplierPart(models.Model):
if self.manufacturer_string: if self.manufacturer_string:
s = s + ' | ' + self.manufacturer_string s = s + ' | ' + self.manufacturer_string
return s return s

View File

@ -51,7 +51,7 @@ class CompanySerializer(InvenTreeModelSerializer):
return queryset return queryset
url = serializers.CharField(source='get_absolute_url', read_only=True) url = serializers.CharField(source='get_absolute_url', read_only=True)
image = serializers.CharField(source='get_thumbnail_url', read_only=True) image = serializers.CharField(source='get_thumbnail_url', read_only=True)
parts_supplied = serializers.IntegerField(read_only=True) parts_supplied = serializers.IntegerField(read_only=True)
@ -157,9 +157,9 @@ class SupplierPartSerializer(InvenTreeModelSerializer):
self.fields.pop('pretty_name') self.fields.pop('pretty_name')
supplier = serializers.PrimaryKeyRelatedField(queryset=Company.objects.filter(is_supplier=True)) supplier = serializers.PrimaryKeyRelatedField(queryset=Company.objects.filter(is_supplier=True))
manufacturer = serializers.PrimaryKeyRelatedField(source='manufacturer_part.manufacturer', read_only=True) manufacturer = serializers.PrimaryKeyRelatedField(source='manufacturer_part.manufacturer', read_only=True)
MPN = serializers.StringRelatedField(source='manufacturer_part.MPN') MPN = serializers.StringRelatedField(source='manufacturer_part.MPN')
manufacturer_part = ManufacturerPartSerializer(read_only=True) manufacturer_part = ManufacturerPartSerializer(read_only=True)

View File

@ -48,7 +48,7 @@ class TestManufacturerField(MigratorTestCase):
- Company object (supplier) - Company object (supplier)
- SupplierPart object - SupplierPart object
""" """
Part = self.old_state.apps.get_model('part', 'part') Part = self.old_state.apps.get_model('part', 'part')
Company = self.old_state.apps.get_model('company', 'company') Company = self.old_state.apps.get_model('company', 'company')
SupplierPart = self.old_state.apps.get_model('company', 'supplierpart') SupplierPart = self.old_state.apps.get_model('company', 'supplierpart')
@ -123,7 +123,7 @@ class TestManufacturerPart(MigratorTestCase):
- Company object (supplier) - Company object (supplier)
- SupplierPart object - SupplierPart object
""" """
Part = self.old_state.apps.get_model('part', 'part') Part = self.old_state.apps.get_model('part', 'part')
Company = self.old_state.apps.get_model('company', 'company') Company = self.old_state.apps.get_model('company', 'company')
SupplierPart = self.old_state.apps.get_model('company', 'supplierpart') SupplierPart = self.old_state.apps.get_model('company', 'supplierpart')
@ -220,7 +220,7 @@ class TestManufacturerPart(MigratorTestCase):
# Check on the SupplierPart objects # Check on the SupplierPart objects
SupplierPart = self.new_state.apps.get_model('company', 'supplierpart') SupplierPart = self.new_state.apps.get_model('company', 'supplierpart')
supplier_parts = SupplierPart.objects.all() supplier_parts = SupplierPart.objects.all()
self.assertEqual(supplier_parts.count(), 6) self.assertEqual(supplier_parts.count(), 6)
@ -229,10 +229,10 @@ class TestManufacturerPart(MigratorTestCase):
# Check on the ManufacturerPart objects # Check on the ManufacturerPart objects
ManufacturerPart = self.new_state.apps.get_model('company', 'manufacturerpart') ManufacturerPart = self.new_state.apps.get_model('company', 'manufacturerpart')
manufacturer_parts = ManufacturerPart.objects.all() manufacturer_parts = ManufacturerPart.objects.all()
self.assertEqual(manufacturer_parts.count(), 4) self.assertEqual(manufacturer_parts.count(), 4)
manufacturer_part = manufacturer_parts.first() manufacturer_part = manufacturer_parts.first()
self.assertEqual(manufacturer_part.MPN, 'MUR-CAP-123456') self.assertEqual(manufacturer_part.MPN, 'MUR-CAP-123456')
@ -293,7 +293,7 @@ class TestCurrencyMigration(MigratorTestCase):
self.assertIsNone(pb.price) self.assertIsNone(pb.price)
def test_currency_migration(self): def test_currency_migration(self):
PB = self.new_state.apps.get_model('company', 'supplierpricebreak') PB = self.new_state.apps.get_model('company', 'supplierpricebreak')
for pb in PB.objects.all(): for pb in PB.objects.all():

View File

@ -30,7 +30,7 @@ class CompanyViewTestBase(TestCase):
# Create a user # Create a user
user = get_user_model() user = get_user_model()
self.user = user.objects.create_user( self.user = user.objects.create_user(
username='username', username='username',
email='user@email.com', email='user@email.com',
@ -83,7 +83,7 @@ class SupplierPartViewTests(CompanyViewTestBase):
def test_supplier_part_create(self): def test_supplier_part_create(self):
""" """
Test the SupplierPartCreate view. Test the SupplierPartCreate view.
This view allows some additional functionality, This view allows some additional functionality,
specifically it allows the user to create a single-quantity price break specifically it allows the user to create a single-quantity price break
automatically, when saving the new SupplierPart model. automatically, when saving the new SupplierPart model.
@ -171,7 +171,7 @@ class SupplierPartViewTests(CompanyViewTestBase):
'confirm_delete': True 'confirm_delete': True
}, },
HTTP_X_REQUESTED_WITH='XMLHttpRequest') HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(n - 2, SupplierPart.objects.count()) self.assertEqual(n - 2, SupplierPart.objects.count())
@ -213,7 +213,7 @@ class ManufacturerPartViewTests(CompanyViewTestBase):
""" """
Test the ManufacturerPartCreate view. Test the ManufacturerPartCreate view.
""" """
url = reverse('manufacturer-part-create') url = reverse('manufacturer-part-create')
# First check that we can GET the form # First check that we can GET the form
@ -252,7 +252,7 @@ class ManufacturerPartViewTests(CompanyViewTestBase):
""" """
Test that the SupplierPartCreate view creates Manufacturer Part. Test that the SupplierPartCreate view creates Manufacturer Part.
""" """
url = reverse('supplier-part-create') url = reverse('supplier-part-create')
# First check that we can GET the form # First check that we can GET the form
@ -297,7 +297,7 @@ class ManufacturerPartViewTests(CompanyViewTestBase):
'confirm_delete': True 'confirm_delete': True
}, },
HTTP_X_REQUESTED_WITH='XMLHttpRequest') HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
# Check that the ManufacturerPart was deleted # Check that the ManufacturerPart was deleted

View File

@ -71,7 +71,7 @@ class CompanySimpleTest(TestCase):
acme = Company.objects.get(pk=1) acme = Company.objects.get(pk=1)
appel = Company.objects.get(pk=2) appel = Company.objects.get(pk=2)
zerg = Company.objects.get(pk=3) zerg = Company.objects.get(pk=3)
self.assertTrue(acme.has_parts) self.assertTrue(acme.has_parts)
self.assertEqual(acme.supplied_part_count, 4) self.assertEqual(acme.supplied_part_count, 4)
@ -82,7 +82,7 @@ class CompanySimpleTest(TestCase):
self.assertEqual(zerg.supplied_part_count, 2) self.assertEqual(zerg.supplied_part_count, 2)
def test_price_breaks(self): def test_price_breaks(self):
self.assertTrue(self.acme0001.has_price_breaks) self.assertTrue(self.acme0001.has_price_breaks)
self.assertTrue(self.acme0002.has_price_breaks) self.assertTrue(self.acme0002.has_price_breaks)
self.assertTrue(self.zergm312.has_price_breaks) self.assertTrue(self.zergm312.has_price_breaks)
@ -121,7 +121,7 @@ class CompanySimpleTest(TestCase):
pmin, pmax = m2x4.get_price_range(5) pmin, pmax = m2x4.get_price_range(5)
self.assertEqual(pmin, 35) self.assertEqual(pmin, 35)
self.assertEqual(pmax, 37.5) self.assertEqual(pmax, 37.5)
m3x12 = Part.objects.get(name='M3x12 SHCS') m3x12 = Part.objects.get(name='M3x12 SHCS')
self.assertEqual(m3x12.get_price_info(0.3), Decimal('2.4')) self.assertEqual(m3x12.get_price_info(0.3), Decimal('2.4'))
@ -187,14 +187,14 @@ class ManufacturerPartSimpleTest(TestCase):
# Create a manufacturer part # Create a manufacturer part
self.part = Part.objects.get(pk=1) self.part = Part.objects.get(pk=1)
manufacturer = Company.objects.get(pk=1) manufacturer = Company.objects.get(pk=1)
self.mp = ManufacturerPart.create( self.mp = ManufacturerPart.create(
part=self.part, part=self.part,
manufacturer=manufacturer, manufacturer=manufacturer,
mpn='PART_NUMBER', mpn='PART_NUMBER',
description='THIS IS A MANUFACTURER PART', description='THIS IS A MANUFACTURER PART',
) )
# Create a supplier part # Create a supplier part
supplier = Company.objects.get(pk=5) supplier = Company.objects.get(pk=5)
supplier_part = SupplierPart.objects.create( supplier_part = SupplierPart.objects.create(

View File

@ -55,7 +55,7 @@ price_break_urls = [
manufacturer_part_detail_urls = [ manufacturer_part_detail_urls = [
url(r'^edit/?', views.ManufacturerPartEdit.as_view(), name='manufacturer-part-edit'), 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(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'), url('^.*$', views.ManufacturerPartDetail.as_view(template_name='company/manufacturer_part_suppliers.html'), name='manufacturer-part-detail'),

View File

@ -96,7 +96,7 @@ class CompanyIndex(InvenTreeRoleMixin, ListView):
if self.request.path == item: if self.request.path == item:
context = lookup[item] context = lookup[item]
break break
if context is None: if context is None:
context = default context = default
@ -279,7 +279,7 @@ class CompanyCreate(AjaxCreateView):
if url == reverse('supplier-create'): if url == reverse('supplier-create'):
return _("Create new Supplier") return _("Create new Supplier")
if url == reverse('manufacturer-create'): if url == reverse('manufacturer-create'):
return _('Create new Manufacturer') return _('Create new Manufacturer')
@ -298,7 +298,7 @@ class CompanyCreate(AjaxCreateView):
initials['is_supplier'] = True initials['is_supplier'] = True
initials['is_customer'] = False initials['is_customer'] = False
initials['is_manufacturer'] = False initials['is_manufacturer'] = False
elif url == reverse('manufacturer-create'): elif url == reverse('manufacturer-create'):
initials['is_manufacturer'] = True initials['is_manufacturer'] = True
initials['is_supplier'] = True initials['is_supplier'] = True
@ -319,7 +319,7 @@ class CompanyCreate(AjaxCreateView):
class CompanyDelete(AjaxDeleteView): class CompanyDelete(AjaxDeleteView):
""" View for deleting a Company object """ """ View for deleting a Company object """
model = Company model = Company
success_url = '/company/' success_url = '/company/'
ajax_template_name = 'company/delete.html' ajax_template_name = 'company/delete.html'
@ -415,7 +415,7 @@ class ManufacturerPartCreate(AjaxCreateView):
initials['manufacturer'] = Company.objects.get(pk=manufacturer_id) initials['manufacturer'] = Company.objects.get(pk=manufacturer_id)
except (ValueError, Company.DoesNotExist): except (ValueError, Company.DoesNotExist):
pass pass
if part_id: if part_id:
try: try:
initials['part'] = Part.objects.get(pk=part_id) initials['part'] = Part.objects.get(pk=part_id)
@ -427,7 +427,7 @@ class ManufacturerPartCreate(AjaxCreateView):
class ManufacturerPartDelete(AjaxDeleteView): class ManufacturerPartDelete(AjaxDeleteView):
""" Delete view for removing a ManufacturerPart. """ Delete view for removing a ManufacturerPart.
ManufacturerParts can be deleted using a variety of 'selectors'. ManufacturerParts can be deleted using a variety of 'selectors'.
- ?part=<pk> -> Delete a single ManufacturerPart object - ?part=<pk> -> Delete a single ManufacturerPart object
@ -561,7 +561,7 @@ class SupplierPartEdit(AjaxUpdateView):
initials = super(SupplierPartEdit, self).get_initial().copy() initials = super(SupplierPartEdit, self).get_initial().copy()
supplier_part = self.get_object() supplier_part = self.get_object()
if supplier_part.manufacturer_part: if supplier_part.manufacturer_part:
initials['manufacturer'] = supplier_part.manufacturer_part.manufacturer.id initials['manufacturer'] = supplier_part.manufacturer_part.manufacturer.id
initials['MPN'] = supplier_part.manufacturer_part.MPN initials['MPN'] = supplier_part.manufacturer_part.MPN
@ -686,7 +686,7 @@ class SupplierPartCreate(AjaxCreateView):
initials['MPN'] = manufacturer_part_obj.MPN initials['MPN'] = manufacturer_part_obj.MPN
except (ValueError, ManufacturerPart.DoesNotExist, Part.DoesNotExist, Company.DoesNotExist): except (ValueError, ManufacturerPart.DoesNotExist, Part.DoesNotExist, Company.DoesNotExist):
pass pass
if part_id: if part_id:
try: try:
initials['part'] = Part.objects.get(pk=part_id) initials['part'] = Part.objects.get(pk=part_id)
@ -703,13 +703,13 @@ class SupplierPartCreate(AjaxCreateView):
if currency_code: if currency_code:
initials['single_pricing'] = ('', currency) initials['single_pricing'] = ('', currency)
return initials return initials
class SupplierPartDelete(AjaxDeleteView): class SupplierPartDelete(AjaxDeleteView):
""" Delete view for removing a SupplierPart. """ Delete view for removing a SupplierPart.
SupplierParts can be deleted using a variety of 'selectors'. SupplierParts can be deleted using a variety of 'selectors'.
- ?part=<pk> -> Delete a single SupplierPart object - ?part=<pk> -> Delete a single SupplierPart object
@ -840,7 +840,7 @@ class PriceBreakCreate(AjaxCreateView):
# Extract the currency object associated with the code # Extract the currency object associated with the code
currency = CURRENCIES.get(currency_code, None) currency = CURRENCIES.get(currency_code, None)
if currency: if currency:
initials['price'] = [1.0, currency] initials['price'] = [1.0, currency]

View File

@ -159,7 +159,7 @@ class StockItemLabelList(LabelListView, StockItemLabelMixin):
""" """
Filter the StockItem label queryset. Filter the StockItem label queryset.
""" """
queryset = super().filter_queryset(queryset) queryset = super().filter_queryset(queryset)
# List of StockItem objects to match against # List of StockItem objects to match against
@ -178,7 +178,7 @@ class StockItemLabelList(LabelListView, StockItemLabelMixin):
# Keep track of which labels match every specified stockitem # Keep track of which labels match every specified stockitem
valid_label_ids = set() valid_label_ids = set()
for label in queryset.all(): for label in queryset.all():
matches = True matches = True
@ -293,7 +293,7 @@ class StockLocationLabelList(LabelListView, StockLocationLabelMixin):
""" """
queryset = super().filter_queryset(queryset) queryset = super().filter_queryset(queryset)
# List of StockLocation objects to match against # List of StockLocation objects to match against
locations = self.get_locations() locations = self.get_locations()

View File

@ -139,7 +139,7 @@ class LabelConfig(AppConfig):
except: except:
# Database might not yet be ready # Database might not yet be ready
return return
src_dir = os.path.join( src_dir = os.path.join(
os.path.dirname(os.path.realpath(__file__)), os.path.dirname(os.path.realpath(__file__)),
'templates', 'templates',

View File

@ -44,7 +44,7 @@ def rename_label(instance, filename):
def validate_stock_item_filters(filters): def validate_stock_item_filters(filters):
filters = validateFilterString(filters, model=stock.models.StockItem) filters = validateFilterString(filters, model=stock.models.StockItem)
return filters return filters
@ -82,7 +82,7 @@ class LabelTemplate(models.Model):
# Each class of label files will be stored in a separate subdirectory # Each class of label files will be stored in a separate subdirectory
SUBDIR = "label" SUBDIR = "label"
# Object we will be printing against (will be filled out later) # Object we will be printing against (will be filled out later)
object_to_print = None object_to_print = None

View File

@ -40,7 +40,7 @@ class TestReportTests(InvenTreeAPITestCase):
return response.data return response.data
def test_list(self): def test_list(self):
response = self.do_list() response = self.do_list()
# TODO - Add some report templates to the fixtures # TODO - Add some report templates to the fixtures

View File

@ -64,7 +64,7 @@ class CancelSalesOrderForm(HelperForm):
fields = [ fields = [
'confirm', 'confirm',
] ]
class ShipSalesOrderForm(HelperForm): class ShipSalesOrderForm(HelperForm):

View File

@ -309,7 +309,7 @@ class PurchaseOrder(Order):
""" """
A PurchaseOrder can only be cancelled under the following circumstances: A PurchaseOrder can only be cancelled under the following circumstances:
""" """
return self.status in [ return self.status in [
PurchaseOrderStatus.PLACED, PurchaseOrderStatus.PLACED,
PurchaseOrderStatus.PENDING PurchaseOrderStatus.PENDING
@ -378,7 +378,7 @@ class PurchaseOrder(Order):
# Has this order been completed? # Has this order been completed?
if len(self.pending_line_items()) == 0: if len(self.pending_line_items()) == 0:
self.received_by = user self.received_by = user
self.complete_order() # This will save the model self.complete_order() # This will save the model
@ -419,7 +419,7 @@ class SalesOrder(Order):
except (ValueError, TypeError): except (ValueError, TypeError):
# Date processing error, return queryset unchanged # Date processing error, return queryset unchanged
return queryset return queryset
# Construct a queryset for "completed" orders within the range # 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) 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(): for line in self.lines.all():
if not line.is_fully_allocated(): if not line.is_fully_allocated():
return False return False
return True return True
def is_over_allocated(self): def is_over_allocated(self):
@ -590,11 +590,11 @@ class SalesOrderAttachment(InvenTreeAttachment):
class OrderLineItem(models.Model): class OrderLineItem(models.Model):
""" Abstract model for an order line item """ Abstract model for an order line item
Attributes: Attributes:
quantity: Number of items quantity: Number of items
note: Annotation for the item note: Annotation for the item
""" """
class Meta: 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')) 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')) 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')) notes = models.CharField(max_length=500, blank=True, verbose_name=_('Notes'), help_text=_('Line item notes'))
class PurchaseOrderLineItem(OrderLineItem): class PurchaseOrderLineItem(OrderLineItem):
""" Model for a purchase order line item. """ Model for a purchase order line item.
Attributes: Attributes:
order: Reference to a PurchaseOrder object order: Reference to a PurchaseOrder object
@ -637,7 +637,7 @@ class PurchaseOrderLineItem(OrderLineItem):
def get_base_part(self): def get_base_part(self):
""" Return the base-part for the line item """ """ Return the base-part for the line item """
return self.part.part return self.part.part
# TODO - Function callback for when the SupplierPart is deleted? # TODO - Function callback for when the SupplierPart is deleted?
part = models.ForeignKey( part = models.ForeignKey(

View File

@ -61,7 +61,7 @@ class POSerializer(InvenTreeModelSerializer):
return queryset return queryset
supplier_detail = CompanyBriefSerializer(source='supplier', many=False, read_only=True) supplier_detail = CompanyBriefSerializer(source='supplier', many=False, read_only=True)
line_items = serializers.IntegerField(read_only=True) line_items = serializers.IntegerField(read_only=True)
status_text = serializers.CharField(source='get_status_display', read_only=True) status_text = serializers.CharField(source='get_status_display', read_only=True)
@ -70,7 +70,7 @@ class POSerializer(InvenTreeModelSerializer):
class Meta: class Meta:
model = PurchaseOrder model = PurchaseOrder
fields = [ fields = [
'pk', 'pk',
'issue_date', 'issue_date',
@ -89,7 +89,7 @@ class POSerializer(InvenTreeModelSerializer):
'target_date', 'target_date',
'notes', 'notes',
] ]
read_only_fields = [ read_only_fields = [
'reference', 'reference',
'status' 'status'
@ -110,10 +110,10 @@ class POLineItemSerializer(InvenTreeModelSerializer):
quantity = serializers.FloatField() quantity = serializers.FloatField()
received = serializers.FloatField() received = serializers.FloatField()
part_detail = PartBriefSerializer(source='get_base_part', many=False, read_only=True) part_detail = PartBriefSerializer(source='get_base_part', many=False, read_only=True)
supplier_part_detail = SupplierPartSerializer(source='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) purchase_price_string = serializers.CharField(source='purchase_price', read_only=True)
class Meta: class Meta:
@ -144,7 +144,7 @@ class POAttachmentSerializer(InvenTreeModelSerializer):
class Meta: class Meta:
model = PurchaseOrderAttachment model = PurchaseOrderAttachment
fields = [ fields = [
'pk', 'pk',
'order', 'order',
@ -270,7 +270,7 @@ class SOLineItemSerializer(InvenTreeModelSerializer):
if allocations is not True: if allocations is not True:
self.fields.pop('allocations') self.fields.pop('allocations')
order_detail = SalesOrderSerializer(source='order', many=False, read_only=True) order_detail = SalesOrderSerializer(source='order', many=False, read_only=True)
part_detail = PartBriefSerializer(source='part', many=False, read_only=True) part_detail = PartBriefSerializer(source='part', many=False, read_only=True)
allocations = SalesOrderAllocationSerializer(many=True, read_only=True) allocations = SalesOrderAllocationSerializer(many=True, read_only=True)
@ -306,7 +306,7 @@ class SOAttachmentSerializer(InvenTreeModelSerializer):
class Meta: class Meta:
model = SalesOrderAttachment model = SalesOrderAttachment
fields = [ fields = [
'pk', 'pk',
'order', 'order',

View File

@ -94,7 +94,7 @@ class PurchaseOrderTest(OrderTest):
url = '/api/order/po/1/' url = '/api/order/po/1/'
response = self.get(url) response = self.get(url)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
data = response.data data = response.data
@ -109,7 +109,7 @@ class PurchaseOrderTest(OrderTest):
response = self.get(url) response = self.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
class SalesOrderTest(OrderTest): class SalesOrderTest(OrderTest):
""" """

View File

@ -73,7 +73,7 @@ class SalesOrderTest(TestCase):
def test_add_duplicate_line_item(self): def test_add_duplicate_line_item(self):
# Adding a duplicate line item to a SalesOrder is accepted # Adding a duplicate line item to a SalesOrder is accepted
for ii in range(1, 5): for ii in range(1, 5):
SalesOrderLineItem.objects.create(order=self.order, part=self.part, quantity=ii) 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.order.is_fully_allocated())
self.assertTrue(self.line.is_fully_allocated()) self.assertTrue(self.line.is_fully_allocated())
self.assertEqual(self.line.allocated_quantity(), 50) self.assertEqual(self.line.allocated_quantity(), 50)
def test_order_cancel(self): def test_order_cancel(self):
# Allocate line items then cancel the order # Allocate line items then cancel the order
@ -154,7 +154,7 @@ class SalesOrderTest(TestCase):
for item in outputs.all(): for item in outputs.all():
self.assertEqual(item.quantity, 25) self.assertEqual(item.quantity, 25)
self.assertEqual(sa.sales_order, None) self.assertEqual(sa.sales_order, None)
self.assertEqual(sb.sales_order, None) self.assertEqual(sb.sales_order, None)
@ -162,7 +162,7 @@ class SalesOrderTest(TestCase):
self.assertEqual(SalesOrderAllocation.objects.count(), 0) self.assertEqual(SalesOrderAllocation.objects.count(), 0)
self.assertEqual(self.order.status, status.SalesOrderStatus.SHIPPED) self.assertEqual(self.order.status, status.SalesOrderStatus.SHIPPED)
self.assertTrue(self.order.is_fully_allocated()) self.assertTrue(self.order.is_fully_allocated())
self.assertTrue(self.line.is_fully_allocated()) self.assertTrue(self.line.is_fully_allocated())
self.assertEqual(self.line.fulfilled_quantity(), 50) self.assertEqual(self.line.fulfilled_quantity(), 50)

View File

@ -17,7 +17,7 @@ import json
class OrderViewTestCase(TestCase): class OrderViewTestCase(TestCase):
fixtures = [ fixtures = [
'category', 'category',
'part', 'part',
@ -193,7 +193,7 @@ class POTests(OrderViewTestCase):
# Test without confirmation # Test without confirmation
response = self.client.post(url, {'confirm': 0}, HTTP_X_REQUESTED_WITH='XMLHttpRequest') response = self.client.post(url, {'confirm': 0}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
data = json.loads(response.content) data = json.loads(response.content)
self.assertFalse(data['form_valid']) self.assertFalse(data['form_valid'])
@ -221,7 +221,7 @@ class POTests(OrderViewTestCase):
# GET the form (pass the correct info) # GET the form (pass the correct info)
response = self.client.get(url, {'order': 1}, HTTP_X_REQUESTED_WITH='XMLHttpRequest') response = self.client.get(url, {'order': 1}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
post_data = { post_data = {
'part': 100, 'part': 100,
'quantity': 45, 'quantity': 45,
@ -303,7 +303,7 @@ class TestPOReceive(OrderViewTestCase):
self.client.get(self.url, data, HTTP_X_REQUESTED_WITH='XMLHttpRequest') self.client.get(self.url, data, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
def test_receive_lines(self): def test_receive_lines(self):
post_data = { post_data = {
} }
@ -330,7 +330,7 @@ class TestPOReceive(OrderViewTestCase):
# Receive negative number # Receive negative number
post_data['line-1'] = -100 post_data['line-1'] = -100
self.post(post_data, validate=False) self.post(post_data, validate=False)
# Receive 75 items # Receive 75 items

View File

@ -36,7 +36,7 @@ class OrderTest(TestCase):
self.assertEqual(order.get_absolute_url(), '/order/purchase-order/1/') self.assertEqual(order.get_absolute_url(), '/order/purchase-order/1/')
self.assertEqual(str(order), 'PO0001 - ACME') self.assertEqual(str(order), 'PO0001 - ACME')
line = PurchaseOrderLineItem.objects.get(pk=1) line = PurchaseOrderLineItem.objects.get(pk=1)
self.assertEqual(str(line), "100 x ACME0001 from ACME (for PO0001 - ACME)") 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 # Try to order a supplier part from the wrong supplier
sku = SupplierPart.objects.get(SKU='ZERG-WIDGET') sku = SupplierPart.objects.get(SKU='ZERG-WIDGET')
with self.assertRaises(django_exceptions.ValidationError): with self.assertRaises(django_exceptions.ValidationError):
order.add_line_item(sku, 99) order.add_line_item(sku, 99)
@ -153,7 +153,7 @@ class OrderTest(TestCase):
with self.assertRaises(django_exceptions.ValidationError): with self.assertRaises(django_exceptions.ValidationError):
order.receive_line_item(line, loc, 'not a number', user=None) order.receive_line_item(line, loc, 'not a number', user=None)
# Receive the rest of the items # Receive the rest of the items
order.receive_line_item(line, loc, 50, user=None) order.receive_line_item(line, loc, 50, user=None)

View File

@ -152,7 +152,7 @@ class SalesOrderAttachmentCreate(AjaxCreateView):
""" """
Save the user that uploaded the attachment Save the user that uploaded the attachment
""" """
attachment = form.save(commit=False) attachment = form.save(commit=False)
attachment.user = self.request.user attachment.user = self.request.user
attachment.save() attachment.save()
@ -330,7 +330,7 @@ class PurchaseOrderCreate(AjaxCreateView):
order = form.save(commit=False) order = form.save(commit=False)
order.created_by = self.request.user order.created_by = self.request.user
return super().save(form) return super().save(form)
@ -365,7 +365,7 @@ class SalesOrderCreate(AjaxCreateView):
order = form.save(commit=False) order = form.save(commit=False)
order.created_by = self.request.user order.created_by = self.request.user
return super().save(form) return super().save(form)
@ -414,7 +414,7 @@ class PurchaseOrderCancel(AjaxUpdateView):
form_class = order_forms.CancelPurchaseOrderForm form_class = order_forms.CancelPurchaseOrderForm
def validate(self, order, form, **kwargs): def validate(self, order, form, **kwargs):
confirm = str2bool(form.cleaned_data.get('confirm', False)) confirm = str2bool(form.cleaned_data.get('confirm', False))
if not confirm: if not confirm:
@ -536,11 +536,11 @@ class SalesOrderShip(AjaxUpdateView):
order = self.get_object() order = self.get_object()
self.object = order self.object = order
form = self.get_form() form = self.get_form()
confirm = str2bool(request.POST.get('confirm', False)) confirm = str2bool(request.POST.get('confirm', False))
valid = False valid = False
if not confirm: if not confirm:
@ -823,7 +823,7 @@ class OrderParts(AjaxView):
for supplier in self.suppliers: for supplier in self.suppliers:
supplier.order_items = [] supplier.order_items = []
suppliers[supplier.name] = supplier suppliers[supplier.name] = supplier
for part in self.parts: for part in self.parts:
@ -844,9 +844,9 @@ class OrderParts(AjaxView):
supplier.selected_purchase_order = orders.first().id supplier.selected_purchase_order = orders.first().id
else: else:
supplier.selected_purchase_order = None supplier.selected_purchase_order = None
suppliers[supplier.name] = supplier suppliers[supplier.name] = supplier
suppliers[supplier.name].order_items.append(part) suppliers[supplier.name].order_items.append(part)
self.suppliers = [suppliers[key] for key in suppliers.keys()] self.suppliers = [suppliers[key] for key in suppliers.keys()]
@ -864,7 +864,7 @@ class OrderParts(AjaxView):
if 'stock[]' in self.request.GET: if 'stock[]' in self.request.GET:
stock_id_list = self.request.GET.getlist('stock[]') stock_id_list = self.request.GET.getlist('stock[]')
""" Get a list of all the parts associated with the stock items. """ Get a list of all the parts associated with the stock items.
- Base part must be purchaseable. - Base part must be purchaseable.
- Return a set of corresponding Part IDs - Return a set of corresponding Part IDs
@ -907,7 +907,7 @@ class OrderParts(AjaxView):
parts = build.required_parts parts = build.required_parts
for part in parts: for part in parts:
# If ordering from a Build page, ignore parts that we have enough of # If ordering from a Build page, ignore parts that we have enough of
if part.quantity_to_order <= 0: if part.quantity_to_order <= 0:
continue continue
@ -963,19 +963,19 @@ class OrderParts(AjaxView):
# Extract part information from the form # Extract part information from the form
for item in self.request.POST: for item in self.request.POST:
if item.startswith('part-supplier-'): if item.startswith('part-supplier-'):
pk = item.replace('part-supplier-', '') pk = item.replace('part-supplier-', '')
# Check that the part actually exists # Check that the part actually exists
try: try:
part = Part.objects.get(id=pk) part = Part.objects.get(id=pk)
except (Part.DoesNotExist, ValueError): except (Part.DoesNotExist, ValueError):
continue continue
supplier_part_id = self.request.POST[item] supplier_part_id = self.request.POST[item]
quantity = self.request.POST.get('part-quantity-' + str(pk), 0) quantity = self.request.POST.get('part-quantity-' + str(pk), 0)
# Ensure a valid supplier has been passed # Ensure a valid supplier has been passed
@ -1377,7 +1377,7 @@ class SalesOrderAssignSerials(AjaxView, FormMixin):
self.form.fields['line'].widget = HiddenInput() self.form.fields['line'].widget = HiddenInput()
else: else:
self.form.add_error('line', _('Select line item')) self.form.add_error('line', _('Select line item'))
if self.part: if self.part:
self.form.fields['part'].widget = HiddenInput() self.form.fields['part'].widget = HiddenInput()
else: else:
@ -1412,7 +1412,7 @@ class SalesOrderAssignSerials(AjaxView, FormMixin):
continue continue
# Now we have a valid stock item - but can it be added to the sales order? # 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 in stock, cannot be added to the order
if not stock_item.in_stock: if not stock_item.in_stock:
self.form.add_error( self.form.add_error(
@ -1480,7 +1480,7 @@ class SalesOrderAllocationCreate(AjaxCreateView):
model = SalesOrderAllocation model = SalesOrderAllocation
form_class = order_forms.CreateSalesOrderAllocationForm form_class = order_forms.CreateSalesOrderAllocationForm
ajax_form_title = _('Allocate Stock to Order') ajax_form_title = _('Allocate Stock to Order')
def get_initial(self): def get_initial(self):
initials = super().get_initial().copy() initials = super().get_initial().copy()
@ -1495,10 +1495,10 @@ class SalesOrderAllocationCreate(AjaxCreateView):
items = StockItem.objects.filter(part=line.part) items = StockItem.objects.filter(part=line.part)
quantity = line.quantity - line.allocated_quantity() quantity = line.quantity - line.allocated_quantity()
if quantity < 0: if quantity < 0:
quantity = 0 quantity = 0
if items.count() == 1: if items.count() == 1:
item = items.first() item = items.first()
initials['item'] = item initials['item'] = item
@ -1514,7 +1514,7 @@ class SalesOrderAllocationCreate(AjaxCreateView):
return initials return initials
def get_form(self): def get_form(self):
form = super().get_form() form = super().get_form()
line_id = form['line'].value() line_id = form['line'].value()
@ -1542,10 +1542,10 @@ class SalesOrderAllocationCreate(AjaxCreateView):
# Hide the 'line' field # Hide the 'line' field
form.fields['line'].widget = HiddenInput() form.fields['line'].widget = HiddenInput()
except (ValueError, SalesOrderLineItem.DoesNotExist): except (ValueError, SalesOrderLineItem.DoesNotExist):
pass pass
return form return form
@ -1554,7 +1554,7 @@ class SalesOrderAllocationEdit(AjaxUpdateView):
model = SalesOrderAllocation model = SalesOrderAllocation
form_class = order_forms.EditSalesOrderAllocationForm form_class = order_forms.EditSalesOrderAllocationForm
ajax_form_title = _('Edit Allocation Quantity') ajax_form_title = _('Edit Allocation Quantity')
def get_form(self): def get_form(self):
form = super().get_form() form = super().get_form()

View File

@ -25,13 +25,13 @@ class PartResource(ModelResource):
# ForeignKey fields # ForeignKey fields
category = Field(attribute='category', widget=widgets.ForeignKeyWidget(PartCategory)) category = Field(attribute='category', widget=widgets.ForeignKeyWidget(PartCategory))
default_location = Field(attribute='default_location', widget=widgets.ForeignKeyWidget(StockLocation)) default_location = Field(attribute='default_location', widget=widgets.ForeignKeyWidget(StockLocation))
default_supplier = Field(attribute='default_supplier', widget=widgets.ForeignKeyWidget(SupplierPart)) default_supplier = Field(attribute='default_supplier', widget=widgets.ForeignKeyWidget(SupplierPart))
category_name = Field(attribute='category__name', readonly=True) category_name = Field(attribute='category__name', readonly=True)
variant_of = Field(attribute='variant_of', widget=widgets.ForeignKeyWidget(Part)) variant_of = Field(attribute='variant_of', widget=widgets.ForeignKeyWidget(Part))
suppliers = Field(attribute='supplier_count', readonly=True) suppliers = Field(attribute='supplier_count', readonly=True)
@ -73,7 +73,7 @@ class PartResource(ModelResource):
class PartAdmin(ImportExportModelAdmin): class PartAdmin(ImportExportModelAdmin):
resource_class = PartResource resource_class = PartResource
list_display = ('full_name', 'description', 'total_stock', 'category') list_display = ('full_name', 'description', 'total_stock', 'category')

View File

@ -41,7 +41,7 @@ class PartCategoryTree(TreeSerializer):
model = PartCategory model = PartCategory
queryset = PartCategory.objects.all() queryset = PartCategory.objects.all()
@property @property
def root_url(self): def root_url(self):
return reverse('part-index') return reverse('part-index')
@ -79,7 +79,7 @@ class CategoryList(generics.ListCreateAPIView):
pass pass
# Look for top-level categories # Look for top-level categories
elif isNull(cat_id): elif isNull(cat_id):
if not cascade: if not cascade:
queryset = queryset.filter(parent=None) queryset = queryset.filter(parent=None)
@ -166,9 +166,9 @@ class CategoryParameters(generics.ListAPIView):
parent_categories = category.get_ancestors() parent_categories = category.get_ancestors()
for parent in parent_categories: for parent in parent_categories:
category_list.append(parent.pk) category_list.append(parent.pk)
queryset = queryset.filter(category__in=category_list) queryset = queryset.filter(category__in=category_list)
return queryset return queryset
@ -264,7 +264,7 @@ class PartThumbs(generics.ListAPIView):
# Get all Parts which have an associated image # Get all Parts which have an associated image
queryset = queryset.exclude(image='') queryset = queryset.exclude(image='')
return queryset return queryset
def list(self, request, *args, **kwargs): def list(self, request, *args, **kwargs):
@ -301,7 +301,7 @@ class PartDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = Part.objects.all() queryset = Part.objects.all()
serializer_class = part_serializers.PartSerializer serializer_class = part_serializers.PartSerializer
starred_parts = None starred_parts = None
def get_queryset(self, *args, **kwargs): def get_queryset(self, *args, **kwargs):
@ -482,7 +482,7 @@ class PartList(generics.ListCreateAPIView):
def get_queryset(self, *args, **kwargs): def get_queryset(self, *args, **kwargs):
queryset = super().get_queryset(*args, **kwargs) queryset = super().get_queryset(*args, **kwargs)
queryset = part_serializers.PartSerializer.prefetch_queryset(queryset) queryset = part_serializers.PartSerializer.prefetch_queryset(queryset)
queryset = part_serializers.PartSerializer.annotate_queryset(queryset) queryset = part_serializers.PartSerializer.annotate_queryset(queryset)
@ -576,7 +576,7 @@ class PartList(generics.ListCreateAPIView):
if cat_id is None: if cat_id is None:
# No category filtering if category is not specified # No category filtering if category is not specified
pass pass
else: else:
# Category has been specified! # Category has been specified!
if isNull(cat_id): 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)) kwargs['sub_part_detail'] = str2bool(self.request.GET.get('sub_part_detail', None))
except AttributeError: except AttributeError:
pass pass
# Ensure the request context is passed through! # Ensure the request context is passed through!
kwargs['context'] = self.get_serializer_context() kwargs['context'] = self.get_serializer_context()
return self.serializer_class(*args, **kwargs) return self.serializer_class(*args, **kwargs)
def get_queryset(self, *args, **kwargs): def get_queryset(self, *args, **kwargs):
@ -867,7 +867,7 @@ class BomList(generics.ListCreateAPIView):
# Work out which lines have actually been validated # Work out which lines have actually been validated
pks = [] pks = []
for bom_item in queryset.all(): for bom_item in queryset.all():
if bom_item.is_line_valid: if bom_item.is_line_valid:
pks.append(bom_item.pk) pks.append(bom_item.pk)
@ -915,7 +915,7 @@ class BomItemValidate(generics.UpdateAPIView):
valid = request.data.get('valid', False) valid = request.data.get('valid', False)
instance = self.get_object() instance = self.get_object()
serializer = self.get_serializer(instance, data=request.data, partial=partial) serializer = self.get_serializer(instance, data=request.data, partial=partial)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
@ -949,7 +949,7 @@ part_api_urls = [
url(r'^sale-price/', include([ url(r'^sale-price/', include([
url(r'^.*$', PartSalePriceList.as_view(), name='api-part-sale-price-list'), url(r'^.*$', PartSalePriceList.as_view(), name='api-part-sale-price-list'),
])), ])),
# Base URL for PartParameter API endpoints # Base URL for PartParameter API endpoints
url(r'^parameter/', include([ url(r'^parameter/', include([
url(r'^template/$', PartParameterTemplateList.as_view(), name='api-part-param-template-list'), url(r'^template/$', PartParameterTemplateList.as_view(), name='api-part-param-template-list'),

View File

@ -43,7 +43,7 @@ class PartConfig(AppConfig):
if part.image: if part.image:
url = part.image.thumbnail.name url = part.image.thumbnail.name
loc = os.path.join(settings.MEDIA_ROOT, url) loc = os.path.join(settings.MEDIA_ROOT, url)
if not os.path.exists(loc): if not os.path.exists(loc):
logger.info("InvenTree: Generating thumbnail for Part '{p}'".format(p=part.name)) logger.info("InvenTree: Generating thumbnail for Part '{p}'".format(p=part.name))
try: try:

View File

@ -69,7 +69,7 @@ def ExportBom(part, fmt='csv', cascade=False, max_levels=None, parameter_data=Fa
for item in items: for item in items:
item.level = str(int(level)) item.level = str(int(level))
# Avoid circular BOM references # Avoid circular BOM references
if item.pk in uids: if item.pk in uids:
continue continue
@ -79,7 +79,7 @@ def ExportBom(part, fmt='csv', cascade=False, max_levels=None, parameter_data=Fa
if item.sub_part.assembly: if item.sub_part.assembly:
if max_levels is None or level < max_levels: if max_levels is None or level < max_levels:
add_items(item.sub_part.bom_items.all().order_by('id'), level + 1) add_items(item.sub_part.bom_items.all().order_by('id'), level + 1)
if cascade: if cascade:
# Cascading (multi-level) BOM # 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}) parameter_cols[name].update({b_idx: value})
except KeyError: except KeyError:
parameter_cols[name] = {b_idx: value} parameter_cols[name] = {b_idx: value}
# Add parameter columns to dataset # Add parameter columns to dataset
parameter_cols_ordered = OrderedDict(sorted(parameter_cols.items(), key=lambda x: x[0])) parameter_cols_ordered = OrderedDict(sorted(parameter_cols.items(), key=lambda x: x[0]))
add_columns_to_dataset(parameter_cols_ordered, len(bom_items)) 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 # Filter manufacturer parts
manufacturer_parts = ManufacturerPart.objects.filter(part__pk=b_part.pk) manufacturer_parts = ManufacturerPart.objects.filter(part__pk=b_part.pk)
manufacturer_parts = manufacturer_parts.prefetch_related('supplier_parts') manufacturer_parts = manufacturer_parts.prefetch_related('supplier_parts')
# Process manufacturer part # Process manufacturer part
for manufacturer_idx, manufacturer_part in enumerate(manufacturer_parts): 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 # Filter supplier parts
manufacturer_parts = ManufacturerPart.objects.filter(part__pk=b_part.pk) manufacturer_parts = ManufacturerPart.objects.filter(part__pk=b_part.pk)
for idx, manufacturer_part in enumerate(manufacturer_parts): for idx, manufacturer_part in enumerate(manufacturer_parts):
if manufacturer_part: if manufacturer_part:
@ -295,7 +295,7 @@ def ExportBom(part, fmt='csv', cascade=False, max_levels=None, parameter_data=Fa
# Filter supplier parts # Filter supplier parts
supplier_parts = SupplierPart.objects.filter(part__pk=b_part.pk) supplier_parts = SupplierPart.objects.filter(part__pk=b_part.pk)
for idx, supplier_part in enumerate(supplier_parts): for idx, supplier_part in enumerate(supplier_parts):
if supplier_part.supplier: 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) filename = '{n}_BOM.{fmt}'.format(n=part.full_name, fmt=fmt)
return DownloadFile(data, filename) return DownloadFile(data, filename)
class BomUploadManager: class BomUploadManager:
""" Class for managing an uploaded BOM file """ """ Class for managing an uploaded BOM file """
@ -342,7 +342,7 @@ class BomUploadManager:
'Part_IPN', 'Part_IPN',
'Part_ID', 'Part_ID',
] ]
# Fields which would be helpful but are not required # Fields which would be helpful but are not required
OPTIONAL_HEADERS = [ OPTIONAL_HEADERS = [
'Reference', 'Reference',
@ -360,7 +360,7 @@ class BomUploadManager:
def __init__(self, bom_file): def __init__(self, bom_file):
""" Initialize the BomUpload class with a user-uploaded file object """ """ Initialize the BomUpload class with a user-uploaded file object """
self.process(bom_file) self.process(bom_file)
def process(self, bom_file): def process(self, bom_file):
@ -387,7 +387,7 @@ class BomUploadManager:
def guess_header(self, header, threshold=80): def guess_header(self, header, threshold=80):
""" Try to match a header (from the file) to a list of known headers """ Try to match a header (from the file) to a list of known headers
Args: Args:
header - Header name to look for header - Header name to look for
threshold - Match threshold for fuzzy search threshold - Match threshold for fuzzy search
@ -421,7 +421,7 @@ class BomUploadManager:
return matches[0]['header'] return matches[0]['header']
return None return None
def columns(self): def columns(self):
""" Return a list of headers for the thingy """ """ Return a list of headers for the thingy """
headers = [] headers = []

View File

@ -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")) 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")) 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")) 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")) 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): def get_choices(self):
""" BOM export format choices """ """ BOM export format choices """
@ -324,7 +324,7 @@ class EditCategoryParameterTemplateForm(HelperForm):
add_to_all_categories = forms.BooleanField(required=False, add_to_all_categories = forms.BooleanField(required=False,
initial=False, initial=False,
help_text=_('Add parameter template to all categories')) help_text=_('Add parameter template to all categories'))
class Meta: class Meta:
model = PartCategoryParameterTemplate model = PartCategoryParameterTemplate
fields = [ fields = [

View File

@ -349,7 +349,7 @@ class Part(MPTTModel):
context['available'] = self.available_stock context['available'] = self.available_stock
context['on_order'] = self.on_order context['on_order'] = self.on_order
context['required'] = context['required_build_order_quantity'] + context['required_sales_order_quantity'] context['required'] = context['required_build_order_quantity'] + context['required_sales_order_quantity']
context['allocated'] = context['allocated_build_order_quantity'] + context['allocated_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 a) The parent part is the same as this one
b) The parent part is used in the BOM for *this* part 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 c) The parent part is used in the BOM for any child parts under this one
Failing this check raises a ValidationError! Failing this check raises a ValidationError!
""" """
@ -506,7 +506,7 @@ class Part(MPTTModel):
parts = Part.objects.filter(tree_id=self.tree_id) parts = Part.objects.filter(tree_id=self.tree_id)
stock = StockModels.StockItem.objects.filter(part__in=parts).exclude(serial=None) stock = StockModels.StockItem.objects.filter(part__in=parts).exclude(serial=None)
# There are no matchin StockItem objects (skip further tests) # There are no matchin StockItem objects (skip further tests)
if not stock.exists(): if not stock.exists():
return None return None
@ -578,7 +578,7 @@ class Part(MPTTModel):
if self.IPN: if self.IPN:
elements.append(self.IPN) elements.append(self.IPN)
elements.append(self.name) elements.append(self.name)
if self.revision: if self.revision:
@ -663,7 +663,7 @@ class Part(MPTTModel):
def clean(self): def clean(self):
""" """
Perform cleaning operations for the Part model Perform cleaning operations for the Part model
Update trackable status: Update trackable status:
If this part is trackable, and it is used in the BOM If this part is trackable, and it is used in the BOM
for a parent part which is *not* trackable, for a parent part which is *not* trackable,
@ -946,7 +946,7 @@ class Part(MPTTModel):
quantity = 0 quantity = 0
for build in builds: for build in builds:
bom_item = None bom_item = None
# List the bom lines required to make the build (including inherited ones!) # 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 build_quantity = build.quantity * bom_item.quantity
quantity += build_quantity quantity += build_quantity
return quantity return quantity
def requiring_sales_orders(self): def requiring_sales_orders(self):
@ -1008,7 +1008,7 @@ class Part(MPTTModel):
def quantity_to_order(self): def quantity_to_order(self):
""" """
Return the quantity needing to be ordered for this part. Return the quantity needing to be ordered for this part.
Here, an "order" could be one of: Here, an "order" could be one of:
- Build Order - Build Order
- Sales Order - Sales Order
@ -1019,7 +1019,7 @@ class Part(MPTTModel):
Required for orders = self.required_order_quantity() Required for orders = self.required_order_quantity()
Currently on order = self.on_order Currently on order = self.on_order
Currently building = self.quantity_being_built Currently building = self.quantity_being_built
""" """
# Total requirement # Total requirement
@ -1114,7 +1114,7 @@ class Part(MPTTModel):
if total is None: if total is None:
total = 0 total = 0
return max(total, 0) return max(total, 0)
@property @property
@ -1238,7 +1238,7 @@ class Part(MPTTModel):
@property @property
def total_stock(self): def total_stock(self):
""" Return the total stock quantity for this part. """ Return the total stock quantity for this part.
- Part may be stored in multiple locations - Part may be stored in multiple locations
- If this part is a "template" (variants exist) then these are counted too - 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' # Start with a list of all parts designated as 'sub components'
parts = Part.objects.filter(component=True) parts = Part.objects.filter(component=True)
# Exclude this part # Exclude this part
parts = parts.exclude(id=self.id) parts = parts.exclude(id=self.id)
@ -1496,7 +1496,7 @@ class Part(MPTTModel):
def get_price_info(self, quantity=1, buy=True, bom=True): def get_price_info(self, quantity=1, buy=True, bom=True):
""" Return a simplified pricing string for this part """ Return a simplified pricing string for this part
Args: Args:
quantity: Number of units to calculate price for quantity: Number of units to calculate price for
buy: Include supplier pricing (default = True) buy: Include supplier pricing (default = True)
@ -1519,7 +1519,7 @@ class Part(MPTTModel):
return "{a} - {b}".format(a=min_price, b=max_price) return "{a} - {b}".format(a=min_price, b=max_price)
def get_supplier_price_range(self, quantity=1): def get_supplier_price_range(self, quantity=1):
min_price = None min_price = None
max_price = None max_price = None
@ -1586,7 +1586,7 @@ class Part(MPTTModel):
return (min_price, max_price) return (min_price, max_price)
def get_price_range(self, quantity=1, buy=True, bom=True): def get_price_range(self, quantity=1, buy=True, bom=True):
""" Return the price range for this part. This price can be either: """ Return the price range for this part. This price can be either:
- Supplier price (if purchased from suppliers) - Supplier price (if purchased from suppliers)
@ -1645,7 +1645,7 @@ class Part(MPTTModel):
@transaction.atomic @transaction.atomic
def copy_parameters_from(self, other, **kwargs): def copy_parameters_from(self, other, **kwargs):
clear = kwargs.get('clear', True) clear = kwargs.get('clear', True)
if clear: if clear:
@ -1692,7 +1692,7 @@ class Part(MPTTModel):
# Copy the parameters data # Copy the parameters data
if kwargs.get('parameters', True): if kwargs.get('parameters', True):
self.copy_parameters_from(other) self.copy_parameters_from(other)
# Copy the fields that aren't available in the duplicate form # Copy the fields that aren't available in the duplicate form
self.salable = other.salable self.salable = other.salable
self.assembly = other.assembly self.assembly = other.assembly
@ -1722,7 +1722,7 @@ class Part(MPTTModel):
tests = tests.filter(required=required) tests = tests.filter(required=required)
return tests return tests
def getRequiredTests(self): def getRequiredTests(self):
# Return the tests which are required by this part # Return the tests which are required by this part
return self.getTestTemplates(required=True) return self.getTestTemplates(required=True)
@ -1868,7 +1868,7 @@ class PartAttachment(InvenTreeAttachment):
""" """
Model for storing file attachments against a Part object Model for storing file attachments against a Part object
""" """
def getSubdir(self): def getSubdir(self):
return os.path.join("part_files", str(self.part.id)) return os.path.join("part_files", str(self.part.id))
@ -2227,7 +2227,7 @@ class BomItem(models.Model):
def validate_hash(self, valid=True): def validate_hash(self, valid=True):
""" Mark this item as 'valid' (store the checksum hash). """ Mark this item as 'valid' (store the checksum hash).
Args: Args:
valid: If true, validate the hash, otherwise invalidate it (default = True) valid: If true, validate the hash, otherwise invalidate it (default = True)
""" """
@ -2265,7 +2265,7 @@ class BomItem(models.Model):
# Check for circular BOM references # Check for circular BOM references
if self.sub_part: if self.sub_part:
self.sub_part.checkAddToBOM(self.part) self.sub_part.checkAddToBOM(self.part)
# If the sub_part is 'trackable' then the 'quantity' field must be an integer # If the sub_part is 'trackable' then the 'quantity' field must be an integer
if self.sub_part.trackable: if self.sub_part.trackable:
if not self.quantity == int(self.quantity): if not self.quantity == int(self.quantity):
@ -2301,7 +2301,7 @@ class BomItem(models.Model):
""" """
query = self.sub_part.stock_items.all() query = self.sub_part.stock_items.all()
query = query.prefetch_related([ query = query.prefetch_related([
'sub_part__stock_items', 'sub_part__stock_items',
]) ])
@ -2358,7 +2358,7 @@ class BomItem(models.Model):
def get_required_quantity(self, build_quantity): def get_required_quantity(self, build_quantity):
""" Calculate the required part quantity, based on the supplier build_quantity. """ Calculate the required part quantity, based on the supplier build_quantity.
Includes overage estimate in the returned value. Includes overage estimate in the returned value.
Args: Args:
build_quantity: Number of parts to build build_quantity: Number of parts to build

View File

@ -134,7 +134,7 @@ class PartBriefSerializer(InvenTreeModelSerializer):
""" Serializer for Part (brief detail) """ """ Serializer for Part (brief detail) """
thumbnail = serializers.CharField(source='get_thumbnail_url', read_only=True) thumbnail = serializers.CharField(source='get_thumbnail_url', read_only=True)
stock = serializers.FloatField(source='total_stock') stock = serializers.FloatField(source='total_stock')
class Meta: class Meta:
@ -232,7 +232,7 @@ class PartSerializer(InvenTreeModelSerializer):
output_field=models.DecimalField(), output_field=models.DecimalField(),
) )
) )
# Filter to limit orders to "open" # Filter to limit orders to "open"
order_filter = Q( order_filter = Q(
order__status__in=PurchaseOrderStatus.OPEN order__status__in=PurchaseOrderStatus.OPEN
@ -259,7 +259,7 @@ class PartSerializer(InvenTreeModelSerializer):
output_field=models.DecimalField(), output_field=models.DecimalField(),
), ),
) )
return queryset return queryset
def get_starred(self, part): def get_starred(self, part):
@ -358,7 +358,7 @@ class BomItemSerializer(InvenTreeModelSerializer):
quantity = serializers.FloatField() quantity = serializers.FloatField()
part = serializers.PrimaryKeyRelatedField(queryset=Part.objects.filter(assembly=True)) part = serializers.PrimaryKeyRelatedField(queryset=Part.objects.filter(assembly=True))
part_detail = PartBriefSerializer(source='part', many=False, read_only=True) part_detail = PartBriefSerializer(source='part', many=False, read_only=True)
sub_part = serializers.PrimaryKeyRelatedField(queryset=Part.objects.filter(component=True)) sub_part = serializers.PrimaryKeyRelatedField(queryset=Part.objects.filter(component=True))

View File

@ -47,7 +47,7 @@ def str2bool(x, *args, **kwargs):
def inrange(n, *args, **kwargs): def inrange(n, *args, **kwargs):
""" Return range(n) for iterating through a numeric quantity """ """ Return range(n) for iterating through a numeric quantity """
return range(n) return range(n)
@register.simple_tag() @register.simple_tag()
def multiply(x, y, *args, **kwargs): def multiply(x, y, *args, **kwargs):
@ -59,7 +59,7 @@ def multiply(x, y, *args, **kwargs):
def add(x, y, *args, **kwargs): def add(x, y, *args, **kwargs):
""" Add two numbers together """ """ Add two numbers together """
return x + y return x + y
@register.simple_tag() @register.simple_tag()
def part_allocation_count(build, part, *args, **kwargs): def part_allocation_count(build, part, *args, **kwargs):
@ -177,7 +177,7 @@ def authorized_owners(group):
except TypeError: except TypeError:
# group.get_users returns None # group.get_users returns None
pass pass
return owners return owners

View File

@ -41,12 +41,12 @@ class PartAPITest(InvenTreeAPITestCase):
Test that we can retrieve list of part categories, Test that we can retrieve list of part categories,
with various filtering options. with various filtering options.
""" """
url = reverse('api-part-category-list') url = reverse('api-part-category-list')
# Request *all* part categories # Request *all* part categories
response = self.client.get(url, format='json') response = self.client.get(url, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data), 8) self.assertEqual(len(response.data), 8)
@ -95,7 +95,7 @@ class PartAPITest(InvenTreeAPITestCase):
url = reverse('api-part-category-list') url = reverse('api-part-category-list')
response = self.client.post(url, data, format='json') response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(response.status_code, status.HTTP_201_CREATED)
parent = response.data['pk'] parent = response.data['pk']
# Add some sub-categories to the top-level 'Animals' category # Add some sub-categories to the top-level 'Animals' category
@ -289,7 +289,7 @@ class PartAPITest(InvenTreeAPITestCase):
self.assertIn('count', data) self.assertIn('count', data)
self.assertIn('results', data) self.assertIn('results', data)
self.assertEqual(len(data['results']), n) self.assertEqual(len(data['results']), n)
@ -354,7 +354,7 @@ class PartAPIAggregationTest(InvenTreeAPITestCase):
self.assertEqual(data['in_stock'], 600) self.assertEqual(data['in_stock'], 600)
self.assertEqual(data['stock_item_count'], 4) self.assertEqual(data['stock_item_count'], 4)
# Add some more stock items!! # Add some more stock items!!
for i in range(100): for i in range(100):
StockItem.objects.create(part=self.part, quantity=5) StockItem.objects.create(part=self.part, quantity=5)
@ -463,7 +463,7 @@ class PartParameterTest(InvenTreeAPITestCase):
response = self.client.patch(url, {'data': '15'}, format='json') response = self.client.patch(url, {'data': '15'}, format='json')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
# Check that the data changed! # Check that the data changed!
response = self.client.get(url, format='json') response = self.client.get(url, format='json')

View File

@ -64,7 +64,7 @@ class BomItemTest(TestCase):
""" Test that BOM line overages are calculated correctly """ """ Test that BOM line overages are calculated correctly """
item = BomItem.objects.get(part=100, sub_part=50) item = BomItem.objects.get(part=100, sub_part=50)
q = 300 q = 300
item.quantity = q item.quantity = q
@ -77,7 +77,7 @@ class BomItemTest(TestCase):
item.overage = 'asf234?' item.overage = 'asf234?'
n = item.get_overage_quantity(q) n = item.get_overage_quantity(q)
self.assertEqual(n, 0) self.assertEqual(n, 0)
# Test absolute overage # Test absolute overage
item.overage = '3' item.overage = '3'
n = item.get_overage_quantity(q) n = item.get_overage_quantity(q)
@ -100,7 +100,7 @@ class BomItemTest(TestCase):
""" Test BOM item hash encoding """ """ Test BOM item hash encoding """
item = BomItem.objects.get(part=100, sub_part=50) item = BomItem.objects.get(part=100, sub_part=50)
h1 = item.get_item_hash() h1 = item.get_item_hash()
# Change data - the hash must change # Change data - the hash must change

View File

@ -59,7 +59,7 @@ class CategoryTest(TestCase):
def test_unique_parents(self): def test_unique_parents(self):
""" Test the 'unique_parents' functionality """ """ Test the 'unique_parents' functionality """
parents = [item.pk for item in self.transceivers.getUniqueParents()] parents = [item.pk for item in self.transceivers.getUniqueParents()]
self.assertIn(self.electronics.id, parents) self.assertIn(self.electronics.id, parents)
@ -128,9 +128,9 @@ class CategoryTest(TestCase):
with self.assertRaises(ValidationError) as err: with self.assertRaises(ValidationError) as err:
cat.full_clean() cat.full_clean()
cat.save() cat.save()
self.assertIn('Illegal character in name', str(err.exception.error_dict.get('name'))) self.assertIn('Illegal character in name', str(err.exception.error_dict.get('name')))
cat.name = 'good name' cat.name = 'good name'
cat.save() cat.save()

View File

@ -34,7 +34,7 @@ class TestForwardMigrations(MigratorTestCase):
# Initially some fields are not present # Initially some fields are not present
with self.assertRaises(AttributeError): with self.assertRaises(AttributeError):
print(p.has_variants) print(p.has_variants)
with self.assertRaises(AttributeError): with self.assertRaises(AttributeError):
print(p.is_template) print(p.is_template)

View File

@ -32,7 +32,7 @@ class TestParams(TestCase):
self.assertEqual(str(c1), 'Mechanical | Length | 2.8') self.assertEqual(str(c1), 'Mechanical | Length | 2.8')
def test_validate(self): def test_validate(self):
n = PartParameterTemplate.objects.all().count() n = PartParameterTemplate.objects.all().count()
t1 = PartParameterTemplate(name='abcde', units='dd') t1 = PartParameterTemplate(name='abcde', units='dd')

View File

@ -91,7 +91,7 @@ class PartTest(TestCase):
def test_rename_img(self): def test_rename_img(self):
img = rename_part_image(self.r1, 'hello.png') img = rename_part_image(self.r1, 'hello.png')
self.assertEqual(img, os.path.join('part_images', 'hello.png')) self.assertEqual(img, os.path.join('part_images', 'hello.png'))
def test_stock(self): def test_stock(self):
# No stock of any resistors # No stock of any resistors
res = Part.objects.filter(description__contains='resistor') 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. Some fields for the Part model can have default values specified by the user.
""" """
def setUp(self): def setUp(self):
# Create a user for auth # Create a user for auth
user = get_user_model() user = get_user_model()
@ -251,7 +251,7 @@ class PartSettingsTest(TestCase):
self.assertEqual(part.trackable, val) self.assertEqual(part.trackable, val)
self.assertEqual(part.assembly, val) self.assertEqual(part.assembly, val)
self.assertEqual(part.is_template, val) self.assertEqual(part.is_template, val)
Part.objects.filter(pk=part.pk).delete() Part.objects.filter(pk=part.pk).delete()
def test_duplicate_ipn(self): def test_duplicate_ipn(self):

View File

@ -9,7 +9,7 @@ from .models import Part, PartRelated
class PartViewTestCase(TestCase): class PartViewTestCase(TestCase):
fixtures = [ fixtures = [
'category', 'category',
'part', 'part',
@ -24,7 +24,7 @@ class PartViewTestCase(TestCase):
# Create a user # Create a user
user = get_user_model() user = get_user_model()
self.user = user.objects.create_user( self.user = user.objects.create_user(
username='username', username='username',
email='user@email.com', email='user@email.com',
@ -52,12 +52,12 @@ class PartListTest(PartViewTestCase):
def test_part_index(self): def test_part_index(self):
response = self.client.get(reverse('part-index')) response = self.client.get(reverse('part-index'))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
keys = response.context.keys() keys = response.context.keys()
self.assertIn('csrf_token', keys) self.assertIn('csrf_token', keys)
self.assertIn('parts', keys) self.assertIn('parts', keys)
self.assertIn('user', keys) self.assertIn('user', keys)
def test_export(self): def test_export(self):
""" Export part data to CSV """ """ 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') response = self.client.get(reverse('bom-download', args=(1,)), HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertIn('streaming_content', dir(response)) self.assertIn('streaming_content', dir(response))
class PartTests(PartViewTestCase): class PartTests(PartViewTestCase):
""" Tests for Part forms """ """ 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}, response = self.client.post(reverse('part-related-create'), {'part_1': 1, 'part_2': 1},
HTTP_X_REQUESTED_WITH='XMLHttpRequest') HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertContains(response, '"form_valid": false', status_code=200) self.assertContains(response, '"form_valid": false', status_code=200)
# Check final count # Check final count
n = PartRelated.objects.all().count() n = PartRelated.objects.all().count()
self.assertEqual(n, 1) self.assertEqual(n, 1)
@ -266,7 +266,7 @@ class PartQRTest(PartViewTestCase):
def test_valid_part(self): def test_valid_part(self):
response = self.client.get(reverse('part-qr', args=(1,)), HTTP_X_REQUESTED_WITH='XMLHttpRequest') response = self.client.get(reverse('part-qr', args=(1,)), HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
data = str(response.content) data = str(response.content)
self.assertIn('Part QR Code', data) self.assertIn('Part QR Code', data)

View File

@ -30,11 +30,11 @@ sale_price_break_urls = [
] ]
part_parameter_urls = [ part_parameter_urls = [
url(r'^template/new/', views.PartParameterTemplateCreate.as_view(), name='part-param-template-create'), url(r'^template/new/', views.PartParameterTemplateCreate.as_view(), name='part-param-template-create'),
url(r'^template/(?P<pk>\d+)/edit/', views.PartParameterTemplateEdit.as_view(), name='part-param-template-edit'), url(r'^template/(?P<pk>\d+)/edit/', views.PartParameterTemplateEdit.as_view(), name='part-param-template-edit'),
url(r'^template/(?P<pk>\d+)/delete/', views.PartParameterTemplateDelete.as_view(), name='part-param-template-edit'), url(r'^template/(?P<pk>\d+)/delete/', views.PartParameterTemplateDelete.as_view(), name='part-param-template-edit'),
url(r'^new/', views.PartParameterCreate.as_view(), name='part-param-create'), url(r'^new/', views.PartParameterCreate.as_view(), name='part-param-create'),
url(r'^(?P<pk>\d+)/edit/', views.PartParameterEdit.as_view(), name='part-param-edit'), url(r'^(?P<pk>\d+)/edit/', views.PartParameterEdit.as_view(), name='part-param-edit'),
url(r'^(?P<pk>\d+)/delete/', views.PartParameterDelete.as_view(), name='part-param-delete'), url(r'^(?P<pk>\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'^duplicate/', views.PartDuplicate.as_view(), name='part-duplicate'),
url(r'^make-variant/', views.MakePartVariant.as_view(), name='make-part-variant'), url(r'^make-variant/', views.MakePartVariant.as_view(), name='make-part-variant'),
url(r'^pricing/', views.PartPricing.as_view(), name='part-pricing'), url(r'^pricing/', views.PartPricing.as_view(), name='part-pricing'),
url(r'^bom-upload/?', views.BomUpload.as_view(), name='upload-bom'), url(r'^bom-upload/?', views.BomUpload.as_view(), name='upload-bom'),
url(r'^bom-duplicate/?', views.BomDuplicate.as_view(), name='duplicate-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'^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'^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'), 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'^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'^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'^notes/?', views.PartNotes.as_view(), name='part-notes'),
url(r'^qr_code/?', views.PartQRCode.as_view(), name='part-qr'), url(r'^qr_code/?', views.PartQRCode.as_view(), name='part-qr'),
# Normal thumbnail with form # 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'^subcategory/', views.CategoryDetail.as_view(template_name='part/subcategory.html'), name='category-subcategory'),
url(r'^parametric/', views.CategoryParametric.as_view(), name='category-parametric'), url(r'^parametric/', views.CategoryParametric.as_view(), name='category-parametric'),
# Anything else # Anything else
url(r'^.*$', views.CategoryDetail.as_view(), name='category-detail'), url(r'^.*$', views.CategoryDetail.as_view(), name='category-detail'),
])) ]))

View File

@ -204,12 +204,12 @@ class PartAttachmentCreate(AjaxCreateView):
class PartAttachmentEdit(AjaxUpdateView): class PartAttachmentEdit(AjaxUpdateView):
""" View for editing a PartAttachment object """ """ View for editing a PartAttachment object """
model = PartAttachment model = PartAttachment
form_class = part_forms.EditPartAttachmentForm form_class = part_forms.EditPartAttachmentForm
ajax_template_name = 'modal_form.html' ajax_template_name = 'modal_form.html'
ajax_form_title = _('Edit attachment') ajax_form_title = _('Edit attachment')
def get_data(self): def get_data(self):
return { return {
'success': _('Part attachment updated') 'success': _('Part attachment updated')
@ -245,7 +245,7 @@ class PartTestTemplateCreate(AjaxCreateView):
model = PartTestTemplate model = PartTestTemplate
form_class = part_forms.EditPartTestTemplateForm form_class = part_forms.EditPartTestTemplateForm
ajax_form_title = _("Create Test Template") ajax_form_title = _("Create Test Template")
def get_initial(self): def get_initial(self):
initials = super().get_initial() initials = super().get_initial()
@ -299,7 +299,7 @@ class PartSetCategory(AjaxUpdateView):
category = None category = None
parts = [] parts = []
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
""" Respond to a GET request to this view """ """ Respond to a GET request to this view """
@ -364,7 +364,7 @@ class PartSetCategory(AjaxUpdateView):
ctx['category'] = self.category ctx['category'] = self.category
return ctx return ctx
class MakePartVariant(AjaxCreateView): class MakePartVariant(AjaxCreateView):
""" View for creating a new variant based on an existing template Part """ View for creating a new variant based on an existing template Part
@ -501,17 +501,17 @@ class PartDuplicate(AjaxCreateView):
valid = form.is_valid() valid = form.is_valid()
name = request.POST.get('name', None) name = request.POST.get('name', None)
if name: if name:
matches = match_part_names(name) matches = match_part_names(name)
if len(matches) > 0: if len(matches) > 0:
# Display the first five closest matches # Display the first five closest matches
context['matches'] = matches[:5] context['matches'] = matches[:5]
# Enforce display of the checkbox # Enforce display of the checkbox
form.fields['confirm_creation'].widget = CheckboxInput() form.fields['confirm_creation'].widget = CheckboxInput()
# Check if the user has checked the 'confirm_creation' input # Check if the user has checked the 'confirm_creation' input
confirmed = str2bool(request.POST.get('confirm_creation', False)) confirmed = str2bool(request.POST.get('confirm_creation', False))
@ -565,7 +565,7 @@ class PartDuplicate(AjaxCreateView):
initials = super(AjaxCreateView, self).get_initial() initials = super(AjaxCreateView, self).get_initial()
initials['bom_copy'] = str2bool(InvenTreeSetting.get_setting('PART_COPY_BOM', True)) initials['bom_copy'] = str2bool(InvenTreeSetting.get_setting('PART_COPY_BOM', True))
initials['parameters_copy'] = str2bool(InvenTreeSetting.get_setting('PART_COPY_PARAMETERS', True)) initials['parameters_copy'] = str2bool(InvenTreeSetting.get_setting('PART_COPY_PARAMETERS', True))
return initials return initials
@ -575,7 +575,7 @@ class PartCreate(AjaxCreateView):
""" View for creating a new Part object. """ View for creating a new Part object.
Options for providing initial conditions: Options for providing initial conditions:
- Provide a category object as initial data - Provide a category object as initial data
""" """
model = Part model = Part
@ -636,9 +636,9 @@ class PartCreate(AjaxCreateView):
context = {} context = {}
valid = form.is_valid() valid = form.is_valid()
name = request.POST.get('name', None) name = request.POST.get('name', None)
if name: if name:
matches = match_part_names(name) matches = match_part_names(name)
@ -646,17 +646,17 @@ class PartCreate(AjaxCreateView):
# Limit to the top 5 matches (to prevent clutter) # Limit to the top 5 matches (to prevent clutter)
context['matches'] = matches[:5] context['matches'] = matches[:5]
# Enforce display of the checkbox # Enforce display of the checkbox
form.fields['confirm_creation'].widget = CheckboxInput() form.fields['confirm_creation'].widget = CheckboxInput()
# Check if the user has checked the 'confirm_creation' input # Check if the user has checked the 'confirm_creation' input
confirmed = str2bool(request.POST.get('confirm_creation', False)) confirmed = str2bool(request.POST.get('confirm_creation', False))
if not confirmed: if not confirmed:
msg = _('Possible matches exist - confirm creation of new part') msg = _('Possible matches exist - confirm creation of new part')
form.add_error('confirm_creation', msg) form.add_error('confirm_creation', msg)
form.pre_form_warning = msg form.pre_form_warning = msg
valid = False valid = False
@ -705,7 +705,7 @@ class PartCreate(AjaxCreateView):
initials['keywords'] = category.default_keywords initials['keywords'] = category.default_keywords
except (PartCategory.DoesNotExist, ValueError): except (PartCategory.DoesNotExist, ValueError):
pass pass
# Allow initial data to be passed through as arguments # Allow initial data to be passed through as arguments
for label in ['name', 'IPN', 'description', 'revision', 'keywords']: for label in ['name', 'IPN', 'description', 'revision', 'keywords']:
if label in self.request.GET: if label in self.request.GET:
@ -734,7 +734,7 @@ class PartNotes(UpdateView):
def get_success_url(self): def get_success_url(self):
""" Return the success URL for this form """ """ Return the success URL for this form """
return reverse('part-notes', kwargs={'pk': self.get_object().id}) return reverse('part-notes', kwargs={'pk': self.get_object().id})
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
@ -767,7 +767,7 @@ class PartDetail(InvenTreeRoleMixin, DetailView):
- If '?editing=True', set 'editing_enabled' context variable - If '?editing=True', set 'editing_enabled' context variable
""" """
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
part = self.get_object() part = self.get_object()
if str2bool(self.request.GET.get('edit', '')): if str2bool(self.request.GET.get('edit', '')):
@ -806,7 +806,7 @@ class PartDetailFromIPN(PartDetail):
pass pass
except queryset.model.DoesNotExist: except queryset.model.DoesNotExist:
pass pass
return None return None
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
@ -1017,7 +1017,7 @@ class BomDuplicate(AjaxUpdateView):
ajax_form_title = _('Duplicate BOM') ajax_form_title = _('Duplicate BOM')
ajax_template_name = 'part/bom_duplicate.html' ajax_template_name = 'part/bom_duplicate.html'
form_class = part_forms.BomDuplicateForm form_class = part_forms.BomDuplicateForm
def get_form(self): def get_form(self):
form = super().get_form() form = super().get_form()
@ -1218,7 +1218,7 @@ class BomUpload(InvenTreeRoleMixin, FormView):
def handleBomFileUpload(self): def handleBomFileUpload(self):
""" Process a BOM file upload form. """ Process a BOM file upload form.
This function validates that the uploaded file was valid, This function validates that the uploaded file was valid,
and contains tabulated data that can be extracted. and contains tabulated data that can be extracted.
If the file does not satisfy these requirements, 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_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_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 - 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: We also extract other information from the row, for the other non-matched fields:
- Quantity - Quantity
- Reference - Reference
- Overage - Overage
- Note - Note
""" """
# Initially use a quantity of zero # Initially use a quantity of zero
@ -1375,7 +1375,7 @@ class BomUpload(InvenTreeRoleMixin, FormView):
# Check if there is a column corresponding to "Note" field # Check if there is a column corresponding to "Note" field
if n_idx >= 0: if n_idx >= 0:
row['note'] = row['data'][n_idx] row['note'] = row['data'][n_idx]
# Supply list of part options for each row, sorted by how closely they match the part name # Supply list of part options for each row, sorted by how closely they match the part name
row['part_options'] = part_options row['part_options'] = part_options
@ -1390,7 +1390,7 @@ class BomUpload(InvenTreeRoleMixin, FormView):
try: try:
if row['part_ipn']: 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())] 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 # Check for single match
if len(part_matches) == 1: if len(part_matches) == 1:
row['part_match'] = part_matches[0] row['part_match'] = part_matches[0]
@ -1464,7 +1464,7 @@ class BomUpload(InvenTreeRoleMixin, FormView):
col_id = int(s[3]) col_id = int(s[3])
except ValueError: except ValueError:
continue continue
if row_id not in self.row_data: if row_id not in self.row_data:
self.row_data[row_id] = {} self.row_data[row_id] = {}
@ -1530,7 +1530,7 @@ class BomUpload(InvenTreeRoleMixin, FormView):
if col in self.column_selections.values(): if col in self.column_selections.values():
part_match_found = True part_match_found = True
break break
# If not, notify user # If not, notify user
if not part_match_found: if not part_match_found:
for col in BomUploadManager.PART_MATCH_HEADERS: for col in BomUploadManager.PART_MATCH_HEADERS:
@ -1546,7 +1546,7 @@ class BomUpload(InvenTreeRoleMixin, FormView):
self.getTableDataFromPost() self.getTableDataFromPost()
valid = len(self.missing_columns) == 0 and not self.duplicates valid = len(self.missing_columns) == 0 and not self.duplicates
if valid: if valid:
# Try to extract meaningful data # Try to extract meaningful data
self.preFillSelections() self.preFillSelections()
@ -1557,7 +1557,7 @@ class BomUpload(InvenTreeRoleMixin, FormView):
return self.render_to_response(self.get_context_data(form=None)) return self.render_to_response(self.get_context_data(form=None))
def handlePartSelection(self): def handlePartSelection(self):
# Extract basic table data from POST request # Extract basic table data from POST request
self.getTableDataFromPost() self.getTableDataFromPost()
@ -1595,7 +1595,7 @@ class BomUpload(InvenTreeRoleMixin, FormView):
row['errors']['quantity'] = _('Enter a valid quantity') row['errors']['quantity'] = _('Enter a valid quantity')
row['quantity'] = q row['quantity'] = q
except ValueError: except ValueError:
continue continue
@ -1648,7 +1648,7 @@ class BomUpload(InvenTreeRoleMixin, FormView):
if key.startswith(field + '_'): if key.startswith(field + '_'):
try: try:
row_id = int(key.replace(field + '_', '')) row_id = int(key.replace(field + '_', ''))
row = self.getRowByIndex(row_id) row = self.getRowByIndex(row_id)
if row: if row:
@ -1714,7 +1714,7 @@ class BomUpload(InvenTreeRoleMixin, FormView):
return self.render_to_response(ctx) return self.render_to_response(ctx)
def getRowByIndex(self, idx): def getRowByIndex(self, idx):
for row in self.bom_rows: for row in self.bom_rows:
if row['index'] == idx: if row['index'] == idx:
return row return row
@ -1732,7 +1732,7 @@ class BomUpload(InvenTreeRoleMixin, FormView):
self.form = self.get_form(self.get_form_class()) self.form = self.get_form(self.get_form_class())
# Did the user POST a file named bom_file? # Did the user POST a file named bom_file?
form_step = request.POST.get('form_step', None) form_step = request.POST.get('form_step', None)
if form_step == 'select_file': if form_step == 'select_file':
@ -1753,7 +1753,7 @@ class PartExport(AjaxView):
def get_parts(self, request): def get_parts(self, request):
""" Extract part list from the POST parameters. """ Extract part list from the POST parameters.
Parts can be supplied as: Parts can be supplied as:
- Part category - Part category
- List of part PK values - List of part PK values
""" """
@ -1956,10 +1956,10 @@ class PartPricing(AjaxView):
form_class = part_forms.PartPriceForm form_class = part_forms.PartPriceForm
role_required = ['sales_order.view', 'part.view'] role_required = ['sales_order.view', 'part.view']
def get_quantity(self): def get_quantity(self):
""" Return set quantity in decimal format """ """ Return set quantity in decimal format """
return Decimal(self.request.POST.get('quantity', 1)) return Decimal(self.request.POST.get('quantity', 1))
def get_part(self): def get_part(self):
@ -1985,7 +1985,7 @@ class PartPricing(AjaxView):
scaler = Decimal(1.0) scaler = Decimal(1.0)
part = self.get_part() part = self.get_part()
ctx = { ctx = {
'part': part, 'part': part,
'quantity': quantity, 'quantity': quantity,
@ -2039,7 +2039,7 @@ class PartPricing(AjaxView):
if min_bom_price: if min_bom_price:
ctx['min_total_bom_price'] = min_bom_price ctx['min_total_bom_price'] = min_bom_price
ctx['min_unit_bom_price'] = min_unit_bom_price ctx['min_unit_bom_price'] = min_unit_bom_price
if max_bom_price: if max_bom_price:
ctx['max_total_bom_price'] = max_bom_price ctx['max_total_bom_price'] = max_bom_price
ctx['max_unit_bom_price'] = max_unit_bom_price ctx['max_unit_bom_price'] = max_unit_bom_price
@ -2168,7 +2168,7 @@ class PartParameterDelete(AjaxDeleteView):
model = PartParameter model = PartParameter
ajax_template_name = 'part/param_delete.html' ajax_template_name = 'part/param_delete.html'
ajax_form_title = _('Delete Part Parameter') ajax_form_title = _('Delete Part Parameter')
class CategoryDetail(InvenTreeRoleMixin, DetailView): class CategoryDetail(InvenTreeRoleMixin, DetailView):
""" Detail view for PartCategory """ """ Detail view for PartCategory """
@ -2223,7 +2223,7 @@ class CategoryEdit(AjaxUpdateView):
""" """
Update view to edit a PartCategory Update view to edit a PartCategory
""" """
model = PartCategory model = PartCategory
form_class = part_forms.EditCategoryForm form_class = part_forms.EditCategoryForm
ajax_template_name = 'modal_form.html' ajax_template_name = 'modal_form.html'
@ -2244,9 +2244,9 @@ class CategoryEdit(AjaxUpdateView):
Limit the choices for 'parent' field to those which make sense Limit the choices for 'parent' field to those which make sense
""" """
form = super(AjaxUpdateView, self).get_form() form = super(AjaxUpdateView, self).get_form()
category = self.get_object() category = self.get_object()
# Remove any invalid choices for the parent category part # Remove any invalid choices for the parent category part
@ -2262,7 +2262,7 @@ class CategoryDelete(AjaxDeleteView):
""" """
Delete view to delete a PartCategory Delete view to delete a PartCategory
""" """
model = PartCategory model = PartCategory
ajax_template_name = 'part/category_delete.html' ajax_template_name = 'part/category_delete.html'
ajax_form_title = _('Delete Part Category') ajax_form_title = _('Delete Part Category')
@ -2346,7 +2346,7 @@ class CategoryParameterTemplateCreate(AjaxCreateView):
""" """
form = super(AjaxCreateView, self).get_form() form = super(AjaxCreateView, self).get_form()
form.fields['category'].widget = HiddenInput() form.fields['category'].widget = HiddenInput()
if form.is_valid(): if form.is_valid():
@ -2441,7 +2441,7 @@ class CategoryParameterTemplateEdit(AjaxUpdateView):
""" """
form = super(AjaxUpdateView, self).get_form() form = super(AjaxUpdateView, self).get_form()
form.fields['category'].widget = HiddenInput() form.fields['category'].widget = HiddenInput()
form.fields['add_to_all_categories'].widget = HiddenInput() form.fields['add_to_all_categories'].widget = HiddenInput()
form.fields['add_to_same_level_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 Create view for making a new BomItem object
""" """
model = BomItem model = BomItem
form_class = part_forms.EditBomItemForm form_class = part_forms.EditBomItemForm
ajax_template_name = 'modal_form.html' ajax_template_name = 'modal_form.html'
@ -2523,13 +2523,13 @@ class BomItemCreate(AjaxCreateView):
try: try:
part = Part.objects.get(id=part_id) part = Part.objects.get(id=part_id)
# Hide the 'part' field # Hide the 'part' field
form.fields['part'].widget = HiddenInput() form.fields['part'].widget = HiddenInput()
# Exclude the part from its own BOM # Exclude the part from its own BOM
sub_part_query = sub_part_query.exclude(id=part.id) sub_part_query = sub_part_query.exclude(id=part.id)
# Eliminate any options that are already in the BOM! # 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()]) 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 model = PartSellPriceBreak
form_class = part_forms.EditPartSalePriceBreakForm form_class = part_forms.EditPartSalePriceBreakForm
ajax_form_title = _('Add Price Break') ajax_form_title = _('Add Price Break')
def get_data(self): def get_data(self):
return { return {
'success': _('Added new price break') 'success': _('Added new price break')
@ -2645,7 +2645,7 @@ class PartSalePriceBreakCreate(AjaxCreateView):
part = Part.objects.get(id=self.request.GET.get('part')) part = Part.objects.get(id=self.request.GET.get('part'))
except (ValueError, Part.DoesNotExist): except (ValueError, Part.DoesNotExist):
part = None part = None
if part is None: if part is None:
try: try:
part = Part.objects.get(id=self.request.POST.get('part')) part = Part.objects.get(id=self.request.POST.get('part'))
@ -2690,7 +2690,7 @@ class PartSalePriceBreakEdit(AjaxUpdateView):
return form return form
class PartSalePriceBreakDelete(AjaxDeleteView): class PartSalePriceBreakDelete(AjaxDeleteView):
""" View for deleting a sale price break """ """ View for deleting a sale price break """

View File

@ -23,10 +23,10 @@ class ActionPlugin(plugin.InvenTreePlugin):
look at the PLUGIN_NAME instead. look at the PLUGIN_NAME instead.
""" """
action = cls.ACTION_NAME action = cls.ACTION_NAME
if not action: if not action:
action = cls.PLUGIN_NAME action = cls.PLUGIN_NAME
return action return action
def __init__(self, user, data=None): def __init__(self, user, data=None):

View File

@ -62,7 +62,7 @@ class StockItemReportMixin:
""" """
Return a list of requested stock items Return a list of requested stock items
""" """
items = [] items = []
params = self.request.query_params params = self.request.query_params
@ -101,7 +101,7 @@ class BuildReportMixin:
params = self.request.query_params params = self.request.query_params
for key in ['build', 'build[]', 'builds', 'builds[]']: for key in ['build', 'build[]', 'builds', 'builds[]']:
if key in params: if key in params:
builds = params.getlist(key, []) builds = params.getlist(key, [])
@ -268,7 +268,7 @@ class StockItemTestReportList(ReportListView, StockItemReportMixin):
serializer_class = TestReportSerializer serializer_class = TestReportSerializer
def filter_queryset(self, queryset): def filter_queryset(self, queryset):
queryset = super().filter_queryset(queryset) queryset = super().filter_queryset(queryset)
# List of StockItem objects to match against # List of StockItem objects to match against
@ -342,7 +342,7 @@ class StockItemTestReportPrint(generics.RetrieveAPIView, StockItemReportMixin, R
items = self.get_items() items = self.get_items()
return self.print(request, items) return self.print(request, items)
class BOMReportList(ReportListView, PartReportMixin): class BOMReportList(ReportListView, PartReportMixin):
""" """
@ -459,7 +459,7 @@ class BuildReportList(ReportListView, BuildReportMixin):
We need to compare the 'filters' string of each report, We need to compare the 'filters' string of each report,
and see if it matches against each of the specified parts and see if it matches against each of the specified parts
# TODO: This code needs some refactoring! # TODO: This code needs some refactoring!
""" """
@ -546,7 +546,7 @@ class POReportList(ReportListView, OrderReportMixin):
valid_report_ids = set() valid_report_ids = set()
for report in queryset.all(): for report in queryset.all():
matches = True matches = True
# Filter string defined for the report object # Filter string defined for the report object
@ -565,7 +565,7 @@ class POReportList(ReportListView, OrderReportMixin):
except FieldError: except FieldError:
matches = False matches = False
break break
if matches: if matches:
valid_report_ids.add(report.pk) valid_report_ids.add(report.pk)
else: else:
@ -629,7 +629,7 @@ class SOReportList(ReportListView, OrderReportMixin):
valid_report_ids = set() valid_report_ids = set()
for report in queryset.all(): for report in queryset.all():
matches = True matches = True
# Filter string defined for the report object # Filter string defined for the report object
@ -648,7 +648,7 @@ class SOReportList(ReportListView, OrderReportMixin):
except FieldError: except FieldError:
matches = False matches = False
break break
if matches: if matches:
valid_report_ids.add(report.pk) valid_report_ids.add(report.pk)
else: else:

View File

@ -52,7 +52,7 @@ class ReportFileUpload(FileSystemStorage):
For example, a snippet or asset file is referenced in a template by filename, 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* and we do not want that filename to change when we upload a new *version*
of the snippet or asset file. of the snippet or asset file.
This uploader class performs the following pseudo-code function: This uploader class performs the following pseudo-code function:
- If the model is *new*, proceed as normal - If the model is *new*, proceed as normal
@ -408,7 +408,7 @@ class PurchaseOrderReport(ReportTemplateBase):
@classmethod @classmethod
def getSubdir(cls): def getSubdir(cls):
return 'purchaseorder' return 'purchaseorder'
filters = models.CharField( filters = models.CharField(
blank=True, blank=True,
max_length=250, max_length=250,
@ -479,7 +479,7 @@ def rename_snippet(instance, filename):
if str(filename) == str(instance.snippet): if str(filename) == str(instance.snippet):
fullpath = os.path.join(settings.MEDIA_ROOT, path) fullpath = os.path.join(settings.MEDIA_ROOT, path)
fullpath = os.path.abspath(fullpath) fullpath = os.path.abspath(fullpath)
if os.path.exists(fullpath): if os.path.exists(fullpath):
logger.info(f"Deleting existing snippet file: '{filename}'") logger.info(f"Deleting existing snippet file: '{filename}'")
os.remove(fullpath) os.remove(fullpath)

View File

@ -22,7 +22,7 @@ def image_data(img, fmt='PNG'):
buffered = BytesIO() buffered = BytesIO()
img.save(buffered, format=fmt) img.save(buffered, format=fmt)
img_str = base64.b64encode(buffered.getvalue()) img_str = base64.b64encode(buffered.getvalue())
return f"data:image/{fmt.lower()};charset=utf-8;base64," + img_str.decode() return f"data:image/{fmt.lower()};charset=utf-8;base64," + img_str.decode()

View File

@ -104,7 +104,7 @@ def company_image(company):
path = os.path.abspath(path) path = os.path.abspath(path)
return f"file://{path}" return f"file://{path}"
@register.simple_tag() @register.simple_tag()
def internal_link(link, text): def internal_link(link, text):

View File

@ -29,7 +29,7 @@ def manually_translate_file(filename, save=False):
print("a) Directly enter a new tranlation in the target language") print("a) Directly enter a new tranlation in the target language")
print("b) Leave empty to skip") print("b) Leave empty to skip")
print("c) Press Ctrl+C to exit") print("c) Press Ctrl+C to exit")
print("-------------------------") print("-------------------------")
input("Press <ENTER> to start") input("Press <ENTER> to start")
print("") print("")

View File

@ -64,5 +64,5 @@ if __name__ == '__main__':
percentage = 0 percentage = 0
print(f"| {locale.ljust(4, ' ')} : {str(percentage).rjust(4, ' ')}% |") print(f"| {locale.ljust(4, ' ')} : {str(percentage).rjust(4, ' ')}% |")
print("-" * 16) print("-" * 16)

View File

@ -87,7 +87,7 @@ class StockItemResource(ModelResource):
# Date management # Date management
updated = Field(attribute='updated', widget=widgets.DateWidget()) updated = Field(attribute='updated', widget=widgets.DateWidget())
stocktake_date = Field(attribute='stocktake_date', widget=widgets.DateWidget()) stocktake_date = Field(attribute='stocktake_date', widget=widgets.DateWidget())
def after_import(self, dataset, result, using_transactions, dry_run, **kwargs): def after_import(self, dataset, result, using_transactions, dry_run, **kwargs):
@ -127,7 +127,7 @@ class StockItemAdmin(ImportExportModelAdmin):
class StockAttachmentAdmin(admin.ModelAdmin): class StockAttachmentAdmin(admin.ModelAdmin):
list_display = ('stock_item', 'attachment', 'comment') list_display = ('stock_item', 'attachment', 'comment')
class StockTrackingAdmin(ImportExportModelAdmin): class StockTrackingAdmin(ImportExportModelAdmin):
list_display = ('item', 'date', 'title') list_display = ('item', 'date', 'title')

View File

@ -117,7 +117,7 @@ class StockAdjust(APIView):
A generic class for handling stocktake actions. A generic class for handling stocktake actions.
Subclasses exist for: Subclasses exist for:
- StockCount: count stock items - StockCount: count stock items
- StockAdd: add stock items - StockAdd: add stock items
- StockRemove: remove stock items - StockRemove: remove stock items
@ -184,7 +184,7 @@ class StockCount(StockAdjust):
""" """
Endpoint for counting stock (performing a stocktake). Endpoint for counting stock (performing a stocktake).
""" """
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
self.get_items(request) self.get_items(request)
@ -225,7 +225,7 @@ class StockRemove(StockAdjust):
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
self.get_items(request) self.get_items(request)
n = 0 n = 0
for item in self.items: for item in self.items:
@ -292,7 +292,7 @@ class StockLocationList(generics.ListCreateAPIView):
params = self.request.query_params params = self.request.query_params
loc_id = params.get('parent', None) loc_id = params.get('parent', None)
cascade = str2bool(params.get('cascade', False)) cascade = str2bool(params.get('cascade', False))
# Do not filter by location # 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 we allow "cascade" at the top-level, this essentially means *all* locations
if not cascade: if not cascade:
queryset = queryset.filter(parent=None) queryset = queryset.filter(parent=None)
else: else:
try: try:
@ -321,7 +321,7 @@ class StockLocationList(generics.ListCreateAPIView):
except (ValueError, StockLocation.DoesNotExist): except (ValueError, StockLocation.DoesNotExist):
pass pass
return queryset return queryset
filter_backends = [ filter_backends = [
@ -379,14 +379,14 @@ class StockList(generics.ListCreateAPIView):
# A location was *not* specified - try to infer it # A location was *not* specified - try to infer it
if 'location' not in request.data: if 'location' not in request.data:
location = item.part.get_default_location() location = item.part.get_default_location()
if location is not None: if location is not None:
item.location = location item.location = location
item.save() item.save()
# An expiry date was *not* specified - try to infer it! # An expiry date was *not* specified - try to infer it!
if 'expiry_date' not in request.data: if 'expiry_date' not in request.data:
if item.part.default_expiry > 0: if item.part.default_expiry > 0:
item.expiry_date = datetime.now().date() + timedelta(days=item.part.default_expiry) item.expiry_date = datetime.now().date() + timedelta(days=item.part.default_expiry)
item.save() item.save()
@ -399,7 +399,7 @@ class StockList(generics.ListCreateAPIView):
""" """
Override the 'list' method, as the StockLocation objects Override the 'list' method, as the StockLocation objects
are very expensive to serialize. are very expensive to serialize.
So, we fetch and serialize the required StockLocation objects only as required. So, we fetch and serialize the required StockLocation objects only as required.
""" """
@ -601,7 +601,7 @@ class StockList(generics.ListCreateAPIView):
if stale_days > 0: if stale_days > 0:
stale_date = datetime.now().date() + timedelta(days=stale_days) 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) stale_filter = StockItem.IN_STOCK_FILTER & ~Q(expiry_date=None) & Q(expiry_date__lt=stale_date)
if stale: if stale:
@ -652,7 +652,7 @@ class StockList(generics.ListCreateAPIView):
if serial_number_gte is not None: if serial_number_gte is not None:
queryset = queryset.filter(serial__gte=serial_number_gte) queryset = queryset.filter(serial__gte=serial_number_gte)
if serial_number_lte is not None: if serial_number_lte is not None:
queryset = queryset.filter(serial__lte=serial_number_lte) queryset = queryset.filter(serial__lte=serial_number_lte)
@ -681,7 +681,7 @@ class StockList(generics.ListCreateAPIView):
else: else:
# Filter StockItem without build allocations or sales order allocations # Filter StockItem without build allocations or sales order allocations
queryset = queryset.filter(Q(sales_order_allocations__isnull=True) & Q(allocations__isnull=True)) queryset = queryset.filter(Q(sales_order_allocations__isnull=True) & Q(allocations__isnull=True))
# Do we wish to filter by "active parts" # Do we wish to filter by "active parts"
active = params.get('active', None) active = params.get('active', None)
@ -766,7 +766,7 @@ class StockList(generics.ListCreateAPIView):
queryset = queryset.filter(location__in=location.getUniqueChildren()) queryset = queryset.filter(location__in=location.getUniqueChildren())
else: else:
queryset = queryset.filter(location=loc_id) queryset = queryset.filter(location=loc_id)
except (ValueError, StockLocation.DoesNotExist): except (ValueError, StockLocation.DoesNotExist):
pass pass
@ -994,14 +994,14 @@ class StockTrackingList(generics.ListCreateAPIView):
def create(self, request, *args, **kwargs): def create(self, request, *args, **kwargs):
""" Create a new StockItemTracking object """ Create a new StockItemTracking object
Here we override the default 'create' implementation, Here we override the default 'create' implementation,
to save the user information associated with the request object. to save the user information associated with the request object.
""" """
serializer = self.get_serializer(data=request.data) serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
# Record the user who created this Part object # Record the user who created this Part object
item = serializer.save() item = serializer.save()
item.user = request.user item.user = request.user

View File

@ -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)')) serial_numbers = forms.CharField(label=_('Serial Numbers'), required=False, help_text=_('Enter unique serial numbers (or leave blank)'))
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.field_prefix = { self.field_prefix = {
'serial_numbers': 'fa-hashtag', 'serial_numbers': 'fa-hashtag',
'link': 'fa-link', 'link': 'fa-link',
@ -147,17 +147,17 @@ class CreateStockItemForm(HelperForm):
# Custom clean to prevent complex StockItem.clean() logic from running (yet) # Custom clean to prevent complex StockItem.clean() logic from running (yet)
def full_clean(self): def full_clean(self):
self._errors = ErrorDict() self._errors = ErrorDict()
if not self.is_bound: # Stop further processing. if not self.is_bound: # Stop further processing.
return return
self.cleaned_data = {} self.cleaned_data = {}
# If the form is permitted to be empty, and none of the form data has # If the form is permitted to be empty, and none of the form data has
# changed from the initial data, short circuit any validation. # changed from the initial data, short circuit any validation.
if self.empty_permitted and not self.has_changed(): if self.empty_permitted and not self.has_changed():
return return
# Don't run _post_clean() as this will run StockItem.clean() # Don't run _post_clean() as this will run StockItem.clean()
self._clean_fields() self._clean_fields()
self._clean_form() self._clean_form()
@ -167,9 +167,9 @@ class SerializeStockForm(HelperForm):
""" Form for serializing a StockItem. """ """ 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)')) 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)')) 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)')) note = forms.CharField(label=_('Notes'), required=False, help_text=_('Add transaction note (optional)'))
quantity = RoundingDecimalFormField(max_digits=10, decimal_places=5, label=_('Quantity')) quantity = RoundingDecimalFormField(max_digits=10, decimal_places=5, label=_('Quantity'))
@ -240,7 +240,7 @@ class TestReportFormatForm(HelperForm):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.fields['template'].choices = self.get_template_choices() self.fields['template'].choices = self.get_template_choices()
def get_template_choices(self): def get_template_choices(self):
""" """
Generate a list of of TestReport options for the StockItem 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')}) raise ValidationError({'quantity_to_install': _('Must not exceed available quantity')})
return data return data
class UninstallStockForm(forms.ModelForm): 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')) 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)')) 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') # 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')) 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')) 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): class EditStockItemForm(HelperForm):
""" Form for editing a StockItem object. """ Form for editing a StockItem object.
Note that not all fields can be edited here (even if they can be specified during creation. 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 location - Must be updated in a 'move' transaction
quantity - Must be updated in a 'stocktake' transaction quantity - Must be updated in a 'stocktake' transaction
part - Cannot be edited after creation part - Cannot be edited after creation

View File

@ -131,7 +131,7 @@ def before_delete_stock_location(sender, instance, using, **kwargs):
class StockItem(MPTTModel): class StockItem(MPTTModel):
""" """
A StockItem object represents a quantity of physical instances of a part. A StockItem object represents a quantity of physical instances of a part.
Attributes: Attributes:
parent: Link to another StockItem from which this StockItem was created 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) 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 add_note = False
user = kwargs.pop('user', None) user = kwargs.pop('user', None)
add_note = add_note and kwargs.pop('note', True) add_note = add_note and kwargs.pop('note', True)
super(StockItem, self).save(*args, **kwargs) super(StockItem, self).save(*args, **kwargs)
@ -226,7 +226,7 @@ class StockItem(MPTTModel):
""" """
super(StockItem, self).validate_unique(exclude) super(StockItem, self).validate_unique(exclude)
# If the serial number is set, make sure it is not a duplicate # If the serial number is set, make sure it is not a duplicate
if self.serial is not None: if self.serial is not None:
# Query to look for duplicate serial numbers # Query to look for duplicate serial numbers
@ -421,7 +421,7 @@ class StockItem(MPTTModel):
max_length=100, blank=True, null=True, max_length=100, blank=True, null=True,
help_text=_('Serial number for this item') help_text=_('Serial number for this item')
) )
link = InvenTreeURLField( link = InvenTreeURLField(
verbose_name=_('External Link'), verbose_name=_('External Link'),
max_length=125, blank=True, max_length=125, blank=True,
@ -727,7 +727,7 @@ class StockItem(MPTTModel):
items = StockItem.objects.filter(belongs_to=self) items = StockItem.objects.filter(belongs_to=self)
for item in items: for item in items:
# Prevent duplication or recursion # Prevent duplication or recursion
if item == self or item in installed: if item == self or item in installed:
continue continue
@ -906,7 +906,7 @@ class StockItem(MPTTModel):
Brief automated note detailing a movement or quantity change. Brief automated note detailing a movement or quantity change.
""" """
track = StockItemTracking.objects.create( track = StockItemTracking.objects.create(
item=self, item=self,
title=title, title=title,
@ -970,7 +970,7 @@ class StockItem(MPTTModel):
# Create a new stock item for each unique serial number # Create a new stock item for each unique serial number
for serial in serials: for serial in serials:
# Create a copy of this StockItem # Create a copy of this StockItem
new_item = StockItem.objects.get(pk=self.pk) new_item = StockItem.objects.get(pk=self.pk)
new_item.quantity = 1 new_item.quantity = 1
@ -1001,7 +1001,7 @@ class StockItem(MPTTModel):
""" Copy stock history from another StockItem """ """ Copy stock history from another StockItem """
for item in other.tracking_info.all(): for item in other.tracking_info.all():
item.item = self item.item = self
item.pk = None item.pk = None
item.save() item.save()
@ -1151,7 +1151,7 @@ class StockItem(MPTTModel):
@transaction.atomic @transaction.atomic
def updateQuantity(self, quantity): def updateQuantity(self, quantity):
""" Update stock quantity for this item. """ Update stock quantity for this item.
If the quantity has reached zero, this StockItem will be deleted. If the quantity has reached zero, this StockItem will be deleted.
Returns: Returns:
@ -1174,7 +1174,7 @@ class StockItem(MPTTModel):
self.quantity = quantity self.quantity = quantity
if quantity == 0 and self.delete_on_deplete and self.can_delete(): 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 # TODO - Do not actually "delete" stock at this point - instead give it a "DELETED" flag
self.delete() self.delete()
return False return False
@ -1584,7 +1584,7 @@ class StockItemTestResult(models.Model):
with automated testing setups. with automated testing setups.
Multiple results can be recorded against any given test, allowing tests to be run many times. Multiple results can be recorded against any given test, allowing tests to be run many times.
Attributes: Attributes:
stock_item: Link to StockItem stock_item: Link to StockItem
test: Test name (simple string matching) test: Test name (simple string matching)
@ -1613,7 +1613,7 @@ class StockItemTestResult(models.Model):
for template in templates: for template in templates:
if key == template.key: if key == template.key:
if template.requires_value: if template.requires_value:
if not self.value: if not self.value:
raise ValidationError({ raise ValidationError({

View File

@ -140,7 +140,7 @@ class StockItemSerializer(InvenTreeModelSerializer):
return queryset return queryset
status_text = serializers.CharField(source='get_status_display', read_only=True) status_text = serializers.CharField(source='get_status_display', read_only=True)
supplier_part_detail = SupplierPartSerializer(source='supplier_part', many=False, 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) 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) tracking_items = serializers.IntegerField(source='tracking_info_count', read_only=True, required=False)
quantity = serializers.FloatField() quantity = serializers.FloatField()
allocated = serializers.FloatField(source='allocation_count', required=False) allocated = serializers.FloatField(source='allocation_count', required=False)
expired = serializers.BooleanField(required=False, read_only=True) expired = serializers.BooleanField(required=False, read_only=True)

View File

@ -38,7 +38,7 @@ class StockAPITestCase(InvenTreeAPITestCase):
] ]
def setUp(self): def setUp(self):
super().setUp() super().setUp()
@ -105,7 +105,7 @@ class StockItemListTest(StockAPITestCase):
""" """
response = self.get_stock(part=25) response = self.get_stock(part=25)
self.assertEqual(len(response), 8) self.assertEqual(len(response), 8)
response = self.get_stock(part=10004) 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) self.assertContains(response, 'This field is required', status_code=status.HTTP_400_BAD_REQUEST)
# POST with an invalid part reference # POST with an invalid part reference
response = self.client.post( response = self.client.post(
@ -384,10 +384,10 @@ class StockItemTest(StockAPITestCase):
- Otherwise, check if the referenced part has a default_expiry defined - Otherwise, check if the referenced part has a default_expiry defined
- If so, use that! - If so, use that!
- Otherwise, no expiry - Otherwise, no expiry
Notes: Notes:
- Part <25> has a default_expiry of 10 days - Part <25> has a default_expiry of 10 days
""" """
# First test - create a new StockItem without an expiry date # First test - create a new StockItem without an expiry date
@ -460,7 +460,7 @@ class StocktakeTest(StockAPITestCase):
data['items'] = [{ data['items'] = [{
'pk': 10 'pk': 10
}] }]
response = self.post(url, data) response = self.post(url, data)
self.assertContains(response, 'must contain a valid pk', status_code=status.HTTP_400_BAD_REQUEST) 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) response = self.post(url, data)
self.assertContains(response, 'must contain a valid quantity', status_code=status.HTTP_400_BAD_REQUEST) self.assertContains(response, 'must contain a valid quantity', status_code=status.HTTP_400_BAD_REQUEST)
data['items'] = [{ data['items'] = [{
'pk': 1234, 'pk': 1234,
'quantity': "-1.234" 'quantity': "-1.234"
}] }]
response = self.post(url, data) response = self.post(url, data)
self.assertContains(response, 'must not be less than zero', status_code=status.HTTP_400_BAD_REQUEST) self.assertContains(response, 'must not be less than zero', status_code=status.HTTP_400_BAD_REQUEST)

View File

@ -29,7 +29,7 @@ class StockViewTestCase(TestCase):
# Create a user # Create a user
user = get_user_model() user = get_user_model()
self.user = user.objects.create_user( self.user = user.objects.create_user(
username='username', username='username',
email='user@email.com', email='user@email.com',
@ -91,7 +91,7 @@ class StockLocationTest(StockViewTestCase):
# Create with an invalid parent # Create with an invalid parent
response = self.client.get(reverse('stock-location-create'), {'location': 999}, HTTP_X_REQUESTED_WITH='XMLHttpRequest') response = self.client.get(reverse('stock-location-create'), {'location': 999}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
class StockItemTest(StockViewTestCase): class StockItemTest(StockViewTestCase):
"""" Tests for StockItem views """ """" Tests for StockItem views """
@ -211,7 +211,7 @@ class StockItemTest(StockViewTestCase):
'serial_numbers': 'dd-23-adf', 'serial_numbers': 'dd-23-adf',
'destination': 'blorg' 'destination': 'blorg'
} }
# POST # POST
response = self.client.post(url, data_valid, HTTP_X_REQUESTED_WITH='XMLHttpRequest') response = self.client.post(url, data_valid, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
@ -247,7 +247,7 @@ class StockOwnershipTest(StockViewTestCase):
# Create a new user # Create a new user
user = get_user_model() user = get_user_model()
self.new_user = user.objects.create_user( self.new_user = user.objects.create_user(
username='john', username='john',
email='john@email.com', email='john@email.com',
@ -314,7 +314,7 @@ class StockOwnershipTest(StockViewTestCase):
response = self.client.post(reverse('stock-location-edit', args=(test_location_id,)), response = self.client.post(reverse('stock-location-edit', args=(test_location_id,)),
{'name': 'Office', 'owner': new_user_group_owner.pk}, {'name': 'Office', 'owner': new_user_group_owner.pk},
HTTP_X_REQUESTED_WITH='XMLHttpRequest') HTTP_X_REQUESTED_WITH='XMLHttpRequest')
# Make sure the location's owner is unchanged # Make sure the location's owner is unchanged
location = StockLocation.objects.get(pk=test_location_id) location = StockLocation.objects.get(pk=test_location_id)
self.assertEqual(location.owner, user_group_owner) self.assertEqual(location.owner, user_group_owner)
@ -366,7 +366,7 @@ class StockOwnershipTest(StockViewTestCase):
response = self.client.post(reverse('stock-location-create'), response = self.client.post(reverse('stock-location-create'),
new_location, HTTP_X_REQUESTED_WITH='XMLHttpRequest') new_location, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertContains(response, '"form_valid": true', status_code=200) self.assertContains(response, '"form_valid": true', status_code=200)
# Retrieve created location # Retrieve created location
location_created = StockLocation.objects.get(name=new_location['name']) location_created = StockLocation.objects.get(name=new_location['name'])

View File

@ -144,7 +144,7 @@ class StockTest(TestCase):
self.drawer3.save() self.drawer3.save()
self.assertNotEqual(self.drawer3.parent, self.office) self.assertNotEqual(self.drawer3.parent, self.office)
self.assertEqual(self.drawer3.pathstring, 'Home/Drawer_3') self.assertEqual(self.drawer3.pathstring, 'Home/Drawer_3')
def test_children(self): def test_children(self):
@ -486,7 +486,7 @@ class VariantTest(StockTest):
# Attempt to create the same serial number but for a variant (should fail!) # Attempt to create the same serial number but for a variant (should fail!)
item.pk = None item.pk = None
item.part = Part.objects.get(pk=10004) item.part = Part.objects.get(pk=10004)
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
item.save() item.save()
@ -542,7 +542,7 @@ class TestResultTest(StockTest):
test='sew cushion', test='sew cushion',
result=True result=True
) )
# Still should be failing at this point, # Still should be failing at this point,
# as the most recent "apply paint" test was False # as the most recent "apply paint" test was False
self.assertFalse(item.passedAllRequiredTests()) self.assertFalse(item.passedAllRequiredTests())

View File

@ -14,7 +14,7 @@ location_urls = [
url(r'^edit/?', views.StockLocationEdit.as_view(), name='stock-location-edit'), url(r'^edit/?', views.StockLocationEdit.as_view(), name='stock-location-edit'),
url(r'^delete/?', views.StockLocationDelete.as_view(), name='stock-location-delete'), 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'^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'), url(r'sublocation/', views.StockLocationDetail.as_view(template_name='stock/sublocation.html'), name='stock-location-sublocation'),
# Anything else # Anything else

View File

@ -121,7 +121,7 @@ class StockLocationEdit(AjaxUpdateView):
context_object_name = 'location' context_object_name = 'location'
ajax_template_name = 'modal_form.html' ajax_template_name = 'modal_form.html'
ajax_form_title = _('Edit Stock Location') ajax_form_title = _('Edit Stock Location')
def get_form(self): def get_form(self):
""" Customize form data for StockLocation editing. """ Customize form data for StockLocation editing.
@ -182,7 +182,7 @@ class StockLocationEdit(AjaxUpdateView):
""" """
self.object = form.save() self.object = form.save()
# Is ownership control enabled? # Is ownership control enabled?
stock_ownership_control = InvenTreeSetting.get_setting('STOCK_OWNERSHIP_CONTROL') stock_ownership_control = InvenTreeSetting.get_setting('STOCK_OWNERSHIP_CONTROL')
@ -267,7 +267,7 @@ class StockItemAttachmentCreate(AjaxCreateView):
def save(self, form, **kwargs): def save(self, form, **kwargs):
""" Record the user that uploaded the attachment """ """ Record the user that uploaded the attachment """
attachment = form.save(commit=False) attachment = form.save(commit=False)
attachment.user = self.request.user attachment.user = self.request.user
attachment.save() attachment.save()
@ -297,7 +297,7 @@ class StockItemAttachmentCreate(AjaxCreateView):
form = super().get_form() form = super().get_form()
form.fields['stock_item'].widget = HiddenInput() form.fields['stock_item'].widget = HiddenInput()
return form return form
@ -309,7 +309,7 @@ class StockItemAttachmentEdit(AjaxUpdateView):
model = StockItemAttachment model = StockItemAttachment
form_class = StockForms.EditStockItemAttachmentForm form_class = StockForms.EditStockItemAttachmentForm
ajax_form_title = _("Edit Stock Item Attachment") ajax_form_title = _("Edit Stock Item Attachment")
def get_form(self): def get_form(self):
form = super().get_form() form = super().get_form()
@ -327,7 +327,7 @@ class StockItemAttachmentDelete(AjaxDeleteView):
ajax_form_title = _("Delete Stock Item Attachment") ajax_form_title = _("Delete Stock Item Attachment")
ajax_template_name = "attachment_delete.html" ajax_template_name = "attachment_delete.html"
context_object_name = "attachment" context_object_name = "attachment"
def get_data(self): def get_data(self):
return { return {
'danger': _("Deleted attachment"), 'danger': _("Deleted attachment"),
@ -376,7 +376,7 @@ class StockItemReturnToStock(AjaxUpdateView):
ajax_form_title = _("Return to Stock") ajax_form_title = _("Return to Stock")
context_object_name = "item" context_object_name = "item"
form_class = StockForms.ReturnStockItemForm form_class = StockForms.ReturnStockItemForm
def validate(self, item, form, **kwargs): def validate(self, item, form, **kwargs):
location = form.cleaned_data.get('location', None) location = form.cleaned_data.get('location', None)
@ -405,7 +405,7 @@ class StockItemDeleteTestData(AjaxUpdateView):
model = StockItem model = StockItem
form_class = ConfirmForm form_class = ConfirmForm
ajax_form_title = _("Delete All Test Data") ajax_form_title = _("Delete All Test Data")
role_required = ['stock.change', 'stock.delete'] role_required = ['stock.change', 'stock.delete']
def get_form(self): def get_form(self):
@ -417,7 +417,7 @@ class StockItemDeleteTestData(AjaxUpdateView):
stock_item = StockItem.objects.get(pk=self.kwargs['pk']) stock_item = StockItem.objects.get(pk=self.kwargs['pk'])
form = self.get_form() form = self.get_form()
confirm = str2bool(request.POST.get('confirm', False)) confirm = str2bool(request.POST.get('confirm', False))
if confirm is not True: if confirm is not True:
@ -488,7 +488,7 @@ class StockItemTestResultEdit(AjaxUpdateView):
form = super().get_form() form = super().get_form()
form.fields['stock_item'].widget = HiddenInput() form.fields['stock_item'].widget = HiddenInput()
return form return form
@ -545,11 +545,11 @@ class StockExport(AjaxView):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
export_format = request.GET.get('format', 'csv').lower() export_format = request.GET.get('format', 'csv').lower()
# Check if a particular location was specified # Check if a particular location was specified
loc_id = request.GET.get('location', None) loc_id = request.GET.get('location', None)
location = None location = None
if loc_id: if loc_id:
try: try:
location = StockLocation.objects.get(pk=loc_id) location = StockLocation.objects.get(pk=loc_id)
@ -646,9 +646,9 @@ class StockItemInstall(AjaxUpdateView):
In contrast to the StockItemUninstall view, In contrast to the StockItemUninstall view,
only a single stock item can be installed at once. only a single stock item can be installed at once.
The "part" to be installed must be provided in the GET query parameters. The "part" to be installed must be provided in the GET query parameters.
""" """
model = StockItem model = StockItem
@ -664,7 +664,7 @@ class StockItemInstall(AjaxUpdateView):
Requirements: Requirements:
- Items must be in stock - Items must be in stock
Filters: Filters:
- Items can be filtered by Part reference - Items can be filtered by Part reference
""" """
@ -702,7 +702,7 @@ class StockItemInstall(AjaxUpdateView):
if self.part: if self.part:
initials['part'] = self.part initials['part'] = self.part
return initials return initials
def get_form(self): def get_form(self):
@ -875,13 +875,13 @@ class StockItemUninstall(AjaxView, FormMixin):
class StockAdjust(AjaxView, FormMixin): class StockAdjust(AjaxView, FormMixin):
""" View for enacting simple stock adjustments: """ View for enacting simple stock adjustments:
- Take items from stock - Take items from stock
- Add items to stock - Add items to stock
- Count items - Count items
- Move stock - Move stock
- Delete stock items - Delete stock items
""" """
ajax_template_name = 'stock/stock_adjust.html' ajax_template_name = 'stock/stock_adjust.html'
@ -942,7 +942,7 @@ class StockAdjust(AjaxView, FormMixin):
for item in self.request.POST: for item in self.request.POST:
if item.startswith('stock-id-'): if item.startswith('stock-id-'):
pk = item.replace('stock-id-', '') pk = item.replace('stock-id-', '')
q = self.request.POST[item] q = self.request.POST[item]
@ -1022,9 +1022,9 @@ class StockAdjust(AjaxView, FormMixin):
form = self.get_form() form = self.get_form()
valid = form.is_valid() valid = form.is_valid()
for item in self.stock_items: for item in self.stock_items:
try: try:
item.new_quantity = Decimal(item.new_quantity) item.new_quantity = Decimal(item.new_quantity)
except ValueError: except ValueError:
@ -1067,7 +1067,7 @@ class StockAdjust(AjaxView, FormMixin):
# Was the entire stock taken? # Was the entire stock taken?
item = self.stock_items[0] item = self.stock_items[0]
if item.quantity == 0: if item.quantity == 0:
# Instruct the form to redirect # Instruct the form to redirect
data['url'] = reverse('stock-index') data['url'] = reverse('stock-index')
@ -1107,7 +1107,7 @@ class StockAdjust(AjaxView, FormMixin):
return _('No action performed') return _('No action performed')
def do_add(self): def do_add(self):
count = 0 count = 0
note = self.request.POST['note'] note = self.request.POST['note']
@ -1137,12 +1137,12 @@ class StockAdjust(AjaxView, FormMixin):
return _('Removed stock from {n} items').format(n=count) return _('Removed stock from {n} items').format(n=count)
def do_count(self): def do_count(self):
count = 0 count = 0
note = self.request.POST['note'] note = self.request.POST['note']
for item in self.stock_items: for item in self.stock_items:
item.stocktake(item.new_quantity, self.request.user, notes=note) item.stocktake(item.new_quantity, self.request.user, notes=note)
count += 1 count += 1
@ -1165,7 +1165,7 @@ class StockAdjust(AjaxView, FormMixin):
if set_loc: if set_loc:
item.part.default_location = destination item.part.default_location = destination
item.part.save() item.part.save()
# Do not move to the same location (unless the quantity is different) # Do not move to the same location (unless the quantity is different)
if destination == item.location and item.new_quantity == item.quantity: if destination == item.location and item.new_quantity == item.quantity:
continue continue
@ -1188,7 +1188,7 @@ class StockAdjust(AjaxView, FormMixin):
if count == 0: if count == 0:
return _('No items were moved') return _('No items were moved')
else: else:
return _('Moved {n} items to {dest}').format( return _('Moved {n} items to {dest}').format(
n=count, n=count,
@ -1201,7 +1201,7 @@ class StockAdjust(AjaxView, FormMixin):
# note = self.request.POST['note'] # note = self.request.POST['note']
for item in self.stock_items: for item in self.stock_items:
# TODO - In the future, StockItems should not be 'deleted' # TODO - In the future, StockItems should not be 'deleted'
# TODO - Instead, they should be marked as "inactive" # TODO - Instead, they should be marked as "inactive"
@ -1475,7 +1475,7 @@ class StockItemSerialize(AjaxUpdateView):
return initials return initials
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
return super().get(request, *args, **kwargs) return super().get(request, *args, **kwargs)
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
@ -1503,13 +1503,13 @@ class StockItemSerialize(AjaxUpdateView):
form.add_error('serial_numbers', e.messages) form.add_error('serial_numbers', e.messages)
valid = False valid = False
numbers = [] numbers = []
if valid: if valid:
try: try:
item.serializeStock(quantity, numbers, user, notes=notes, location=destination) item.serializeStock(quantity, numbers, user, notes=notes, location=destination)
except ValidationError as e: except ValidationError as e:
messages = e.message_dict messages = e.message_dict
for k in messages.keys(): for k in messages.keys():
if k in ['quantity', 'destination', 'serial_numbers']: if k in ['quantity', 'destination', 'serial_numbers']:
form.add_error(k, messages[k]) form.add_error(k, messages[k])
@ -1595,7 +1595,7 @@ class StockItemCreate(AjaxCreateView):
if not part.purchaseable: if not part.purchaseable:
form.fields.pop('purchase_price') form.fields.pop('purchase_price')
# Hide the 'part' field (as a valid part is selected) # Hide the 'part' field (as a valid part is selected)
# form.fields['part'].widget = HiddenInput() # form.fields['part'].widget = HiddenInput()
@ -1658,7 +1658,7 @@ class StockItemCreate(AjaxCreateView):
if type(location_owner.owner) is Group: if type(location_owner.owner) is Group:
user_as_owner = Owner.get_owner(self.request.user) user_as_owner = Owner.get_owner(self.request.user)
queryset = location_owner.get_related_owners() queryset = location_owner.get_related_owners()
if user_as_owner in queryset: if user_as_owner in queryset:
form.fields['owner'].initial = user_as_owner 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 # If location's owner is a user: automatically set owner field and disable it
form.fields['owner'].disabled = True form.fields['owner'].disabled = True
form.fields['owner'].initial = location_owner form.fields['owner'].initial = location_owner
return form return form
def get_initial(self): def get_initial(self):
@ -1746,7 +1746,7 @@ class StockItemCreate(AjaxCreateView):
data = form.cleaned_data data = form.cleaned_data
part = data.get('part', None) part = data.get('part', None)
quantity = data.get('quantity', None) quantity = data.get('quantity', None)
owner = data.get('owner', None) owner = data.get('owner', None)
@ -1831,7 +1831,7 @@ class StockItemCreate(AjaxCreateView):
) )
item.save(user=self.request.user) item.save(user=self.request.user)
# Create a single StockItem of the specified quantity # Create a single StockItem of the specified quantity
else: else:
form._post_clean() form._post_clean()
@ -1841,12 +1841,12 @@ class StockItemCreate(AjaxCreateView):
item.save(user=self.request.user) item.save(user=self.request.user)
return item return item
# Non-trackable part # Non-trackable part
else: else:
form._post_clean() form._post_clean()
item = form.save(commit=False) item = form.save(commit=False)
item.user = self.request.user item.user = self.request.user
item.save(user=self.request.user) item.save(user=self.request.user)

View File

@ -127,7 +127,7 @@ class RoleGroupAdmin(admin.ModelAdmin):
if rule_set.can_delete: if rule_set.can_delete:
permission_level = append_permission_level(permission_level, 'D') permission_level = append_permission_level(permission_level, 'D')
return permission_level return permission_level
def admin(self, obj): def admin(self, obj):

View File

@ -185,7 +185,7 @@ class RuleSet(models.Model):
can_change = models.BooleanField(verbose_name=_('Change'), default=False, help_text=_('Permissions to edit items')) 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')) can_delete = models.BooleanField(verbose_name=_('Delete'), default=False, help_text=_('Permission to delete items'))
@classmethod @classmethod
def check_table_permission(cls, user, table, permission): 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 # Add any required permissions to the group
for perm in permissions_to_add: for perm in permissions_to_add:
# Ignore if permission is already in the group # Ignore if permission is already in the group
if perm in group_permissions: if perm in group_permissions:
continue continue
@ -541,7 +541,7 @@ class Owner(models.Model):
pass pass
return owner return owner
return owner return owner
def get_related_owners(self, include_group=False): 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) Q(owner_id=self.owner.id, owner_type=ContentType.objects.get_for_model(Group).id)
else: else:
query = Q(owner_id__in=users, owner_type=ContentType.objects.get_for_model(user_model).id) query = Q(owner_id__in=users, owner_type=ContentType.objects.get_for_model(user_model).id)
related_owners = Owner.objects.filter(query) related_owners = Owner.objects.filter(query)
elif type(self.owner) is user_model: elif type(self.owner) is user_model:

View File

@ -24,7 +24,7 @@ class TestForwardMigrations(MigratorTestCase):
email='fred@fred.com', email='fred@fred.com',
password='password' password='password'
) )
User.objects.create( User.objects.create(
username='brad', username='brad',
email='brad@fred.com', email='brad@fred.com',

View File

@ -17,7 +17,7 @@ class RuleSetModelTest(TestCase):
def test_ruleset_models(self): def test_ruleset_models(self):
keys = RuleSet.RULESET_MODELS.keys() keys = RuleSet.RULESET_MODELS.keys()
# Check if there are any rulesets which do not have models defined # 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] missing = [name for name in RuleSet.RULESET_NAMES if name not in keys]
@ -88,7 +88,7 @@ class RuleSetModelTest(TestCase):
extra_models = set() extra_models = set()
defined_models = set() defined_models = set()
for model in assigned_models: for model in assigned_models:
defined_models.add(model) defined_models.add(model)
@ -198,7 +198,7 @@ class OwnerModelTest(TestCase):
self.user.delete() self.user.delete()
user_as_owner = Owner.get_owner(self.user) user_as_owner = Owner.get_owner(self.user)
self.assertEqual(user_as_owner, None) self.assertEqual(user_as_owner, None)
# Delete group and verify owner was deleted too # Delete group and verify owner was deleted too
self.group.delete() self.group.delete()
group_as_owner = Owner.get_owner(self.group) group_as_owner = Owner.get_owner(self.group)

View File

@ -295,7 +295,7 @@ def export_records(c, filename='data.json'):
for entry in data: for entry in data:
if "model" in entry: if "model" in entry:
# Clear out any permissions specified for a group # Clear out any permissions specified for a group
if entry["model"] == "auth.group": if entry["model"] == "auth.group":
entry["fields"]["permissions"] = [] entry["fields"]["permissions"] = []
@ -335,7 +335,7 @@ def import_records(c, filename='data.json'):
for entry in data: for entry in data:
if "model" in entry: if "model" in entry:
# Clear out any permissions specified for a group # Clear out any permissions specified for a group
if entry["model"] == "auth.group": if entry["model"] == "auth.group":
entry["fields"]["permissions"] = [] entry["fields"]["permissions"] = []
@ -370,7 +370,7 @@ def import_fixtures(c):
fixtures = [ fixtures = [
# Build model # Build model
'build', 'build',
# Common models # Common models
'settings', 'settings',