mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
removes all lines consisting only of spaces
this really bothers me for some reason - nothing technical
This commit is contained in:
parent
ecc9eec084
commit
f2b0717d10
@ -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
|
||||||
|
@ -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:
|
||||||
|
@ -17,5 +17,5 @@ class InvenTreeManualExchangeBackend(BaseExchangeBackend):
|
|||||||
"""
|
"""
|
||||||
Do not get any rates...
|
Do not get any rates...
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return {}
|
return {}
|
||||||
|
@ -102,5 +102,5 @@ class RoundingDecimalField(models.DecimalField):
|
|||||||
}
|
}
|
||||||
|
|
||||||
defaults.update(kwargs)
|
defaults.update(kwargs)
|
||||||
|
|
||||||
return super().formfield(**kwargs)
|
return super().formfield(**kwargs)
|
||||||
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
@ -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
|
||||||
|
@ -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.
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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")
|
||||||
|
@ -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')
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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 """
|
||||||
|
@ -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'),
|
||||||
|
@ -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:
|
||||||
|
@ -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()
|
||||||
|
@ -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'),
|
||||||
]
|
]
|
||||||
|
@ -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!
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -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']
|
||||||
|
@ -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)
|
||||||
|
@ -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(
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ from .models import InvenTreeSetting
|
|||||||
|
|
||||||
|
|
||||||
class SettingsAdmin(ImportExportModelAdmin):
|
class SettingsAdmin(ImportExportModelAdmin):
|
||||||
|
|
||||||
list_display = ('key', 'value')
|
list_display = ('key', 'value')
|
||||||
|
|
||||||
|
|
||||||
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
@ -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())
|
||||||
|
@ -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()
|
||||||
|
@ -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()
|
||||||
|
@ -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)
|
||||||
|
@ -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):
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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():
|
||||||
|
@ -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
|
||||||
|
@ -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(
|
||||||
|
@ -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'),
|
||||||
|
@ -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]
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
@ -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',
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -64,7 +64,7 @@ class CancelSalesOrderForm(HelperForm):
|
|||||||
fields = [
|
fields = [
|
||||||
'confirm',
|
'confirm',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class ShipSalesOrderForm(HelperForm):
|
class ShipSalesOrderForm(HelperForm):
|
||||||
|
|
||||||
|
@ -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(
|
||||||
|
@ -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',
|
||||||
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
@ -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')
|
||||||
|
@ -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'),
|
||||||
|
@ -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:
|
||||||
|
@ -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 = []
|
||||||
|
@ -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 = [
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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))
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
@ -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')
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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')
|
||||||
|
@ -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):
|
||||||
|
@ -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)
|
||||||
|
@ -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'),
|
||||||
]))
|
]))
|
||||||
|
@ -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 """
|
||||||
|
|
||||||
|
@ -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):
|
||||||
|
@ -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:
|
||||||
|
@ -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)
|
||||||
|
@ -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()
|
||||||
|
@ -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):
|
||||||
|
@ -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("")
|
||||||
|
@ -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)
|
||||||
|
@ -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')
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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({
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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'])
|
||||||
|
|
||||||
|
@ -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())
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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):
|
||||||
|
@ -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:
|
||||||
|
@ -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',
|
||||||
|
@ -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)
|
||||||
|
6
tasks.py
6
tasks.py
@ -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',
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user