mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
[CI] docstrings (#6172)
* Squashed commit of the following: commit52d7ff0f65
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 23:03:20 2024 +0100 fixed lookup commit0d076eaea8
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 23:03:08 2024 +0100 switched to pathlib for lookup commit473e75eda2
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 22:52:30 2024 +0100 fix wrong url response commitfd74f8d703
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 21:14:38 2024 +0100 switched to ruff for import sorting commitf83fedbbb8
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 21:03:14 2024 +0100 switched to single quotes everywhere commita92442e60e
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 20:58:23 2024 +0100 added autofixes commitcc66c93136
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 20:56:47 2024 +0100 enable autoformat commit1f343606ec
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 20:42:14 2024 +0100 Squashed commit of the following: commitf5cf7b2e78
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 20:36:57 2024 +0100 fixed reqs commit9d845bee98
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 20:32:35 2024 +0100 disable autofix/format commitaff5f27148
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 20:28:50 2024 +0100 adjust checks commit47271cf1ef
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 20:28:22 2024 +0100 reorder order of operations commite1bf178b40
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 20:01:09 2024 +0100 adapted ruff settings to better fit code base commitad7d88a6f4
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 19:59:45 2024 +0100 auto fixed docstring commita2e54a760e
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 19:46:35 2024 +0100 fix getattr useage commitcb80c73bc6
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 19:25:09 2024 +0100 fix requirements file commitb7780bbd21
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 18:42:28 2024 +0100 fix removed sections commit71f1681f55
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 18:41:21 2024 +0100 fix djlint syntax commita0bcf1bcce
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 18:35:28 2024 +0100 remove flake8 from code base commit22475b31cc
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 18:34:56 2024 +0100 remove flake8 from code base commit0413350f14
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 18:24:39 2024 +0100 moved ruff section commitd90c48a0bf
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 18:24:24 2024 +0100 move djlint config to pyproject commitc5ce55d511
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 18:20:39 2024 +0100 added isort again commit42a41d23af
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 18:19:02 2024 +0100 move config section commit8569233181
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 18:17:52 2024 +0100 fix codespell error commit2897c6704d
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 17:29:21 2024 +0100 replaced flake8 with ruff mostly for speed improvements * enable docstring checks * fix docstrings * fixed D417 Missing argument description * Squashed commit of the following: commitd3b795824b
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 22:56:17 2024 +0100 fixed source path commit0bac0c19b8
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 22:47:53 2024 +0100 fixed req commit9f61f01d9c
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 22:45:18 2024 +0100 added missing toml req commit91b71ed24a
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 20:49:50 2024 +0100 moved isort config commit12460b0419
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 20:43:22 2024 +0100 remove flake8 section from setup.cfg commitf5cf7b2e78
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 20:36:57 2024 +0100 fixed reqs commit9d845bee98
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 20:32:35 2024 +0100 disable autofix/format commitaff5f27148
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 20:28:50 2024 +0100 adjust checks commit47271cf1ef
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 20:28:22 2024 +0100 reorder order of operations commite1bf178b40
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 20:01:09 2024 +0100 adapted ruff settings to better fit code base commitad7d88a6f4
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 19:59:45 2024 +0100 auto fixed docstring commita2e54a760e
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 19:46:35 2024 +0100 fix getattr useage commitcb80c73bc6
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 19:25:09 2024 +0100 fix requirements file commitb7780bbd21
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 18:42:28 2024 +0100 fix removed sections commit71f1681f55
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 18:41:21 2024 +0100 fix djlint syntax commita0bcf1bcce
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 18:35:28 2024 +0100 remove flake8 from code base commit22475b31cc
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 18:34:56 2024 +0100 remove flake8 from code base commit0413350f14
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 18:24:39 2024 +0100 moved ruff section commitd90c48a0bf
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 18:24:24 2024 +0100 move djlint config to pyproject commitc5ce55d511
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 18:20:39 2024 +0100 added isort again commit42a41d23af
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 18:19:02 2024 +0100 move config section commit8569233181
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 18:17:52 2024 +0100 fix codespell error commit2897c6704d
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 17:29:21 2024 +0100 replaced flake8 with ruff mostly for speed improvements * fix pyproject * make docstrings more uniform * auto-format * fix order * revert url change
This commit is contained in:
parent
9db3efa085
commit
9d0264c319
@ -1,4 +1,4 @@
|
||||
"""Admin classes"""
|
||||
"""Admin classes."""
|
||||
|
||||
from django.contrib import admin
|
||||
from django.http.request import HttpRequest
|
||||
@ -10,7 +10,7 @@ from import_export.resources import ModelResource
|
||||
|
||||
|
||||
class InvenTreeResource(ModelResource):
|
||||
"""Custom subclass of the ModelResource class provided by django-import-export"
|
||||
"""Custom subclass of the ModelResource class provided by django-import-export".
|
||||
|
||||
Ensures that exported data are escaped to prevent malicious formula injection.
|
||||
Ref: https://owasp.org/www-community/attacks/CSV_Injection
|
||||
@ -32,7 +32,7 @@ class InvenTreeResource(ModelResource):
|
||||
rollback_on_validation_errors=None,
|
||||
**kwargs,
|
||||
):
|
||||
"""Override the default import_data_inner function to provide better error handling"""
|
||||
"""Override the default import_data_inner function to provide better error handling."""
|
||||
if len(dataset) > self.MAX_IMPORT_ROWS:
|
||||
raise ImportExportError(
|
||||
f'Dataset contains too many rows (max {self.MAX_IMPORT_ROWS})'
|
||||
@ -76,7 +76,7 @@ class InvenTreeResource(ModelResource):
|
||||
return row
|
||||
|
||||
def get_fields(self, **kwargs):
|
||||
"""Return fields, with some common exclusions"""
|
||||
"""Return fields, with some common exclusions."""
|
||||
fields = super().get_fields(**kwargs)
|
||||
|
||||
fields_to_exclude = ['metadata', 'lft', 'rght', 'tree_id', 'level']
|
||||
@ -84,18 +84,17 @@ class InvenTreeResource(ModelResource):
|
||||
return [f for f in fields if f.column_name not in fields_to_exclude]
|
||||
|
||||
def before_import_row(self, row, row_number=None, **kwargs):
|
||||
"""Run custom code before importing each row"""
|
||||
|
||||
"""Run custom code before importing each row."""
|
||||
for field in self.CONVERT_NULL_FIELDS:
|
||||
if field in row and row[field] is None:
|
||||
row[field] = ''
|
||||
|
||||
|
||||
class CustomRateAdmin(RateAdmin):
|
||||
"""Admin interface for the Rate class"""
|
||||
"""Admin interface for the Rate class."""
|
||||
|
||||
def has_add_permission(self, request: HttpRequest) -> bool:
|
||||
"""Disable the 'add' permission for Rate objects"""
|
||||
"""Disable the 'add' permission for Rate objects."""
|
||||
return False
|
||||
|
||||
|
||||
|
@ -98,7 +98,7 @@ class InfoView(AjaxView):
|
||||
permission_classes = [permissions.AllowAny]
|
||||
|
||||
def worker_pending_tasks(self):
|
||||
"""Return the current number of outstanding background tasks"""
|
||||
"""Return the current number of outstanding background tasks."""
|
||||
return OrmQ.objects.count()
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
@ -158,7 +158,7 @@ class NotFoundView(AjaxView):
|
||||
permission_classes = [permissions.AllowAny]
|
||||
|
||||
def not_found(self, request):
|
||||
"""Return a 404 error"""
|
||||
"""Return a 404 error."""
|
||||
return JsonResponse(
|
||||
{
|
||||
'detail': _('API endpoint not found'),
|
||||
@ -168,27 +168,27 @@ class NotFoundView(AjaxView):
|
||||
)
|
||||
|
||||
def options(self, request, *args, **kwargs):
|
||||
"""Return 404"""
|
||||
"""Return 404."""
|
||||
return self.not_found(request)
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""Return 404"""
|
||||
"""Return 404."""
|
||||
return self.not_found(request)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
"""Return 404"""
|
||||
"""Return 404."""
|
||||
return self.not_found(request)
|
||||
|
||||
def patch(self, request, *args, **kwargs):
|
||||
"""Return 404"""
|
||||
"""Return 404."""
|
||||
return self.not_found(request)
|
||||
|
||||
def put(self, request, *args, **kwargs):
|
||||
"""Return 404"""
|
||||
"""Return 404."""
|
||||
return self.not_found(request)
|
||||
|
||||
def delete(self, request, *args, **kwargs):
|
||||
"""Return 404"""
|
||||
"""Return 404."""
|
||||
return self.not_found(request)
|
||||
|
||||
|
||||
@ -204,7 +204,7 @@ class BulkDeleteMixin:
|
||||
"""
|
||||
|
||||
def filter_delete_queryset(self, queryset, request):
|
||||
"""Provide custom filtering for the queryset *before* it is deleted"""
|
||||
"""Provide custom filtering for the queryset *before* it is deleted."""
|
||||
return queryset
|
||||
|
||||
def delete(self, request, *args, **kwargs):
|
||||
@ -270,7 +270,7 @@ class BulkDeleteMixin:
|
||||
|
||||
|
||||
class ListCreateDestroyAPIView(BulkDeleteMixin, ListCreateAPI):
|
||||
"""Custom API endpoint which provides BulkDelete functionality in addition to List and Create"""
|
||||
"""Custom API endpoint which provides BulkDelete functionality in addition to List and Create."""
|
||||
|
||||
...
|
||||
|
||||
@ -328,7 +328,7 @@ class AttachmentMixin:
|
||||
|
||||
|
||||
class APISearchView(APIView):
|
||||
"""A general-purpose 'search' API endpoint
|
||||
"""A general-purpose 'search' API endpoint.
|
||||
|
||||
Returns hits against a number of different models simultaneously,
|
||||
to consolidate multiple API requests into a single query.
|
||||
@ -339,7 +339,7 @@ class APISearchView(APIView):
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
def get_result_types(self):
|
||||
"""Construct a list of search types we can return"""
|
||||
"""Construct a list of search types we can return."""
|
||||
import build.api
|
||||
import company.api
|
||||
import order.api
|
||||
@ -361,7 +361,7 @@ class APISearchView(APIView):
|
||||
}
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
"""Perform search query against available models"""
|
||||
"""Perform search query against available models."""
|
||||
data = request.data
|
||||
|
||||
results = {}
|
||||
@ -424,12 +424,12 @@ class APISearchView(APIView):
|
||||
|
||||
|
||||
class MetadataView(RetrieveUpdateAPI):
|
||||
"""Generic API endpoint for reading and editing metadata for a model"""
|
||||
"""Generic API endpoint for reading and editing metadata for a model."""
|
||||
|
||||
MODEL_REF = 'model'
|
||||
|
||||
def get_model_type(self):
|
||||
"""Return the model type associated with this API instance"""
|
||||
"""Return the model type associated with this API instance."""
|
||||
model = self.kwargs.get(self.MODEL_REF, None)
|
||||
|
||||
if model is None:
|
||||
@ -440,13 +440,13 @@ class MetadataView(RetrieveUpdateAPI):
|
||||
return model
|
||||
|
||||
def get_permission_model(self):
|
||||
"""Return the 'permission' model associated with this view"""
|
||||
"""Return the 'permission' model associated with this view."""
|
||||
return self.get_model_type()
|
||||
|
||||
def get_queryset(self):
|
||||
"""Return the queryset for this endpoint"""
|
||||
"""Return the queryset for this endpoint."""
|
||||
return self.get_model_type().objects.all()
|
||||
|
||||
def get_serializer(self, *args, **kwargs):
|
||||
"""Return MetadataSerializer instance"""
|
||||
"""Return MetadataSerializer instance."""
|
||||
return MetadataSerializer(self.get_model_type(), *args, **kwargs)
|
||||
|
@ -60,7 +60,7 @@ def to_dict(value):
|
||||
|
||||
|
||||
def is_true(x):
|
||||
"""Shortcut function to determine if a value "looks" like a boolean"""
|
||||
"""Shortcut function to determine if a value "looks" like a boolean."""
|
||||
return str(x).strip().lower() in ['1', 'y', 'yes', 't', 'true', 'on']
|
||||
|
||||
|
||||
@ -226,12 +226,12 @@ def get_setting(env_var=None, config_key=None, default_value=None, typecast=None
|
||||
|
||||
|
||||
def get_boolean_setting(env_var=None, config_key=None, default_value=False):
|
||||
"""Helper function for retrieving a boolean configuration setting"""
|
||||
"""Helper function for retrieving a boolean configuration setting."""
|
||||
return is_true(get_setting(env_var, config_key, default_value))
|
||||
|
||||
|
||||
def get_media_dir(create=True):
|
||||
"""Return the absolute path for the 'media' directory (where uploaded files are stored)"""
|
||||
"""Return the absolute path for the 'media' directory (where uploaded files are stored)."""
|
||||
md = get_setting('INVENTREE_MEDIA_ROOT', 'media_root')
|
||||
|
||||
if not md:
|
||||
@ -246,7 +246,7 @@ def get_media_dir(create=True):
|
||||
|
||||
|
||||
def get_static_dir(create=True):
|
||||
"""Return the absolute path for the 'static' directory (where static files are stored)"""
|
||||
"""Return the absolute path for the 'static' directory (where static files are stored)."""
|
||||
sd = get_setting('INVENTREE_STATIC_ROOT', 'static_root')
|
||||
|
||||
if not sd:
|
||||
@ -261,7 +261,7 @@ def get_static_dir(create=True):
|
||||
|
||||
|
||||
def get_backup_dir(create=True):
|
||||
"""Return the absolute path for the backup directory"""
|
||||
"""Return the absolute path for the backup directory."""
|
||||
bd = get_setting('INVENTREE_BACKUP_DIR', 'backup_dir')
|
||||
|
||||
if not bd:
|
||||
@ -307,7 +307,7 @@ def get_plugin_file():
|
||||
|
||||
|
||||
def get_plugin_dir():
|
||||
"""Returns the path of the custom plugins directory"""
|
||||
"""Returns the path of the custom plugins directory."""
|
||||
return get_setting('INVENTREE_PLUGIN_DIR', 'plugin_dir')
|
||||
|
||||
|
||||
@ -389,7 +389,6 @@ def get_frontend_settings(debug=True):
|
||||
Note that the new config settings use the 'FRONTEND' key,
|
||||
whereas the legacy key was 'PUI' (platform UI) which is now deprecated
|
||||
"""
|
||||
|
||||
# Legacy settings
|
||||
pui_settings = get_setting(
|
||||
'INVENTREE_PUI_SETTINGS', 'pui_settings', {}, typecast=dict
|
||||
|
@ -151,7 +151,7 @@ def convert_physical_value(value: str, unit: str = None, strip_units=True):
|
||||
|
||||
|
||||
def is_dimensionless(value):
|
||||
"""Determine if the provided value is 'dimensionless'
|
||||
"""Determine if the provided value is 'dimensionless'.
|
||||
|
||||
A dimensionless value might look like:
|
||||
|
||||
|
@ -80,7 +80,7 @@ class InvenTreeExchange(SimpleExchangeBackend):
|
||||
|
||||
@atomic
|
||||
def update_rates(self, base_currency=None, **kwargs):
|
||||
"""Call to update all exchange rates"""
|
||||
"""Call to update all exchange rates."""
|
||||
backend, _ = ExchangeBackend.objects.update_or_create(
|
||||
name=self.name, defaults={'base_currency': base_currency}
|
||||
)
|
||||
|
@ -31,8 +31,7 @@ class InvenTreeRestURLField(RestURLField):
|
||||
self.validators[-1].schemes = allowable_url_schemes()
|
||||
|
||||
def run_validation(self, data=empty):
|
||||
"""Override default validation behaviour for this field type"""
|
||||
|
||||
"""Override default validation behaviour for this field type."""
|
||||
import common.models
|
||||
|
||||
strict_urls = common.models.InvenTreeSetting.get_setting(
|
||||
@ -53,7 +52,7 @@ class InvenTreeURLField(models.URLField):
|
||||
default_validators = [AllowedURLValidator()]
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Initialization method for InvenTreeURLField"""
|
||||
"""Initialization method for InvenTreeURLField."""
|
||||
# Max length for InvenTreeURLField is set to 200
|
||||
kwargs['max_length'] = 200
|
||||
super().__init__(**kwargs)
|
||||
@ -199,13 +198,13 @@ class RoundingDecimalField(models.DecimalField):
|
||||
|
||||
|
||||
class InvenTreeNotesField(models.TextField):
|
||||
"""Custom implementation of a 'notes' field"""
|
||||
"""Custom implementation of a 'notes' field."""
|
||||
|
||||
# Maximum character limit for the various 'notes' fields
|
||||
NOTES_MAX_LENGTH = 50000
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Configure default initial values for this field"""
|
||||
"""Configure default initial values for this field."""
|
||||
kwargs['max_length'] = self.NOTES_MAX_LENGTH
|
||||
kwargs['verbose_name'] = _('Notes')
|
||||
kwargs['blank'] = True
|
||||
|
@ -17,7 +17,6 @@ class InvenTreeDateFilter(rest_filters.DateFilter):
|
||||
|
||||
def filter(self, qs, value):
|
||||
"""Override the filter method to handle timezones correctly."""
|
||||
|
||||
if settings.USE_TZ:
|
||||
if value is not None:
|
||||
tz = timezone.get_current_timezone()
|
||||
@ -28,7 +27,7 @@ class InvenTreeDateFilter(rest_filters.DateFilter):
|
||||
|
||||
|
||||
class InvenTreeSearchFilter(filters.SearchFilter):
|
||||
"""Custom search filter which allows adjusting of search terms dynamically"""
|
||||
"""Custom search filter which allows adjusting of search terms dynamically."""
|
||||
|
||||
def get_search_fields(self, view, request):
|
||||
"""Return a set of search fields for the request, adjusted based on request params.
|
||||
|
@ -1,4 +1,4 @@
|
||||
"""Custom string formatting functions and helpers"""
|
||||
"""Custom string formatting functions and helpers."""
|
||||
|
||||
import re
|
||||
import string
|
||||
@ -42,7 +42,7 @@ def parse_format_string(fmt_string: str) -> dict:
|
||||
|
||||
|
||||
def construct_format_regex(fmt_string: str) -> str:
|
||||
r"""Construct a regular expression based on a provided format string
|
||||
r"""Construct a regular expression based on a provided format string.
|
||||
|
||||
This function turns a python format string into a regular expression,
|
||||
which can be used for two purposes:
|
||||
@ -143,7 +143,7 @@ def validate_string(value: str, fmt_string: str) -> str:
|
||||
|
||||
|
||||
def extract_named_group(name: str, value: str, fmt_string: str) -> str:
|
||||
"""Extract a named value from the provided string, given the provided format string
|
||||
"""Extract a named value from the provided string, given the provided format string.
|
||||
|
||||
Args:
|
||||
name: Name of group to extract e.g. 'ref'
|
||||
@ -181,11 +181,12 @@ def extract_named_group(name: str, value: str, fmt_string: str) -> str:
|
||||
|
||||
|
||||
def format_money(money: Money, decimal_places: int = None, format: str = None) -> str:
|
||||
"""Format money object according to the currently set local
|
||||
"""Format money object according to the currently set local.
|
||||
|
||||
Args:
|
||||
decimal_places: Number of decimal places to use
|
||||
format: Format pattern according LDML / the babel format pattern syntax (https://babel.pocoo.org/en/latest/numbers.html)
|
||||
money (Money): The money object to format
|
||||
decimal_places (int): Number of decimal places to use
|
||||
format (str): Format pattern according LDML / the babel format pattern syntax (https://babel.pocoo.org/en/latest/numbers.html)
|
||||
|
||||
Returns:
|
||||
str: The formatted string
|
||||
|
@ -151,7 +151,7 @@ class SetPasswordForm(HelperForm):
|
||||
|
||||
# override allauth
|
||||
class CustomLoginForm(LoginForm):
|
||||
"""Custom login form to override default allauth behaviour"""
|
||||
"""Custom login form to override default allauth behaviour."""
|
||||
|
||||
def login(self, request, redirect_url=None):
|
||||
"""Perform login action.
|
||||
@ -317,7 +317,7 @@ class CustomAccountAdapter(
|
||||
return False
|
||||
|
||||
def get_email_confirmation_url(self, request, emailconfirmation):
|
||||
"""Construct the email confirmation url"""
|
||||
"""Construct the email confirmation url."""
|
||||
from InvenTree.helpers_model import construct_absolute_url
|
||||
|
||||
url = super().get_email_confirmation_url(request, emailconfirmation)
|
||||
@ -363,7 +363,6 @@ class CustomSocialAccountAdapter(
|
||||
self, request, provider_id, error=None, exception=None, extra_context=None
|
||||
):
|
||||
"""Callback method for authentication errors."""
|
||||
|
||||
# Log the error to the database
|
||||
log_error(request.path if request else 'sso')
|
||||
logger.error("SSO error for provider '%s' - check admin error log", provider_id)
|
||||
|
@ -116,7 +116,7 @@ def getLogoImage(as_file=False, custom=True):
|
||||
|
||||
|
||||
def getSplashScreen(custom=True):
|
||||
"""Return the InvenTree splash screen, or a custom splash if available"""
|
||||
"""Return the InvenTree splash screen, or a custom splash if available."""
|
||||
static_storage = StaticFilesStorage()
|
||||
|
||||
if custom and settings.CUSTOM_SPLASH:
|
||||
@ -161,7 +161,7 @@ def str2bool(text, test=True):
|
||||
|
||||
|
||||
def str2int(text, default=None):
|
||||
"""Convert a string to int if possible
|
||||
"""Convert a string to int if possible.
|
||||
|
||||
Args:
|
||||
text: Int like string
|
||||
@ -334,10 +334,9 @@ def MakeBarcode(cls_name, object_pk: int, object_data=None, **kwargs):
|
||||
"""Generate a string for a barcode. Adds some global InvenTree parameters.
|
||||
|
||||
Args:
|
||||
object_type: string describing the object type e.g. 'StockItem'
|
||||
object_id: ID (Primary Key) of the object in the database
|
||||
object_url: url for JSON API detail view of the object
|
||||
data: Python dict object containing extra data which will be rendered to string (must only contain stringable values)
|
||||
cls_name: string describing the object type e.g. 'StockItem'
|
||||
object_pk (int): ID (Primary Key) of the object in the database
|
||||
object_data: Python dict object containing extra data which will be rendered to string (must only contain stringable values)
|
||||
|
||||
Returns:
|
||||
json string of the supplied data plus some other data
|
||||
@ -479,13 +478,12 @@ def extract_serial_numbers(input_string, expected_quantity: int, starting_value=
|
||||
errors = []
|
||||
|
||||
def add_error(error: str):
|
||||
"""Helper function for adding an error message"""
|
||||
"""Helper function for adding an error message."""
|
||||
if error not in errors:
|
||||
errors.append(error)
|
||||
|
||||
def add_serial(serial):
|
||||
"""Helper function to check for duplicated values"""
|
||||
|
||||
"""Helper function to check for duplicated values."""
|
||||
serial = serial.strip()
|
||||
|
||||
# Ignore blank / empty serials
|
||||
@ -753,7 +751,7 @@ def strip_html_tags(value: str, raise_error=True, field_name=None):
|
||||
def remove_non_printable_characters(
|
||||
value: str, remove_newline=True, remove_ascii=True, remove_unicode=True
|
||||
):
|
||||
"""Remove non-printable / control characters from the provided string"""
|
||||
"""Remove non-printable / control characters from the provided string."""
|
||||
cleaned = value
|
||||
|
||||
if remove_ascii:
|
||||
|
@ -73,7 +73,7 @@ def construct_absolute_url(*arg, **kwargs):
|
||||
|
||||
|
||||
def get_base_url(**kwargs):
|
||||
"""Return the base URL for the InvenTree server"""
|
||||
"""Return the base URL for the InvenTree server."""
|
||||
return construct_absolute_url('', **kwargs)
|
||||
|
||||
|
||||
@ -193,7 +193,7 @@ def render_currency(
|
||||
min_decimal_places=None,
|
||||
max_decimal_places=None,
|
||||
):
|
||||
"""Render a currency / Money object to a formatted string (e.g. for reports)
|
||||
"""Render a currency / Money object to a formatted string (e.g. for reports).
|
||||
|
||||
Arguments:
|
||||
money: The Money instance to be rendered
|
||||
|
@ -18,7 +18,6 @@ class DiffMixin:
|
||||
Returns:
|
||||
object: Instance of the object saved in the database
|
||||
"""
|
||||
|
||||
if self.pk:
|
||||
try:
|
||||
return self.__class__.objects.get(pk=self.pk)
|
||||
@ -36,7 +35,6 @@ class DiffMixin:
|
||||
Returns:
|
||||
dict: Dict of field deltas
|
||||
"""
|
||||
|
||||
db_instance = self.get_db_instance()
|
||||
|
||||
if db_instance is None:
|
||||
@ -58,7 +56,6 @@ class DiffMixin:
|
||||
|
||||
def has_field_changed(self, field_name):
|
||||
"""Determine if a particular field has changed."""
|
||||
|
||||
return field_name in self.get_field_deltas()
|
||||
|
||||
|
||||
|
@ -95,6 +95,7 @@ class MetadataMixin(models.Model):
|
||||
|
||||
Args:
|
||||
key: String key for requesting metadata. e.g. if a plugin is accessing the metadata, the plugin slug should be used
|
||||
backup_value: Value that should be used if no value is found
|
||||
|
||||
Returns:
|
||||
Python dict object containing requested metadata. If no matching metadata is found, returns None
|
||||
@ -228,7 +229,7 @@ class ReferenceIndexingMixin(models.Model):
|
||||
|
||||
@classmethod
|
||||
def get_most_recent_item(cls):
|
||||
"""Return the item which is 'most recent'
|
||||
"""Return the item which is 'most recent'.
|
||||
|
||||
In practice, this means the item with the highest reference value
|
||||
"""
|
||||
@ -270,7 +271,7 @@ class ReferenceIndexingMixin(models.Model):
|
||||
|
||||
@classmethod
|
||||
def generate_reference(cls):
|
||||
"""Generate the next 'reference' field based on specified pattern"""
|
||||
"""Generate the next 'reference' field based on specified pattern."""
|
||||
fmt = cls.get_reference_pattern()
|
||||
ctx = cls.get_reference_context()
|
||||
|
||||
@ -309,7 +310,7 @@ class ReferenceIndexingMixin(models.Model):
|
||||
|
||||
@classmethod
|
||||
def validate_reference_pattern(cls, pattern):
|
||||
"""Ensure that the provided pattern is valid"""
|
||||
"""Ensure that the provided pattern is valid."""
|
||||
ctx = cls.get_reference_context()
|
||||
|
||||
try:
|
||||
@ -334,7 +335,7 @@ class ReferenceIndexingMixin(models.Model):
|
||||
|
||||
@classmethod
|
||||
def validate_reference_field(cls, value):
|
||||
"""Check that the provided 'reference' value matches the requisite pattern"""
|
||||
"""Check that the provided 'reference' value matches the requisite pattern."""
|
||||
pattern = cls.get_reference_pattern()
|
||||
|
||||
value = str(value).strip()
|
||||
@ -713,7 +714,6 @@ class InvenTreeTree(MPTTModel):
|
||||
C) delete_children = False and delete_items = True
|
||||
D) delete_children = False and delete_items = False
|
||||
"""
|
||||
|
||||
child_nodes = self.get_descendants(include_self=False)
|
||||
|
||||
# Case A: Delete all child items, and all child nodes.
|
||||
@ -756,7 +756,6 @@ class InvenTreeTree(MPTTModel):
|
||||
Arguments:
|
||||
nodes: A queryset of nodes to delete
|
||||
"""
|
||||
|
||||
nodes.update(parent=None)
|
||||
nodes.delete()
|
||||
|
||||
@ -782,11 +781,11 @@ class InvenTreeTree(MPTTModel):
|
||||
return {'parent': {'exclude_tree': self.pk}}
|
||||
|
||||
def construct_pathstring(self):
|
||||
"""Construct the pathstring for this tree node"""
|
||||
"""Construct the pathstring for this tree node."""
|
||||
return InvenTree.helpers.constructPathString([item.name for item in self.path])
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
"""Custom save method for InvenTreeTree abstract model"""
|
||||
"""Custom save method for InvenTreeTree abstract model."""
|
||||
try:
|
||||
super().save(*args, **kwargs)
|
||||
except InvalidMove:
|
||||
@ -1003,7 +1002,6 @@ class InvenTreeBarcodeMixin(models.Model):
|
||||
|
||||
def format_matched_response(self):
|
||||
"""Format a standard response for a matched barcode."""
|
||||
|
||||
data = {'pk': self.pk}
|
||||
|
||||
if hasattr(self, 'get_api_url'):
|
||||
@ -1017,7 +1015,7 @@ class InvenTreeBarcodeMixin(models.Model):
|
||||
|
||||
@property
|
||||
def barcode(self):
|
||||
"""Format a minimal barcode string (e.g. for label printing)"""
|
||||
"""Format a minimal barcode string (e.g. for label printing)."""
|
||||
return self.format_barcode(brief=True)
|
||||
|
||||
@classmethod
|
||||
@ -1055,7 +1053,7 @@ class InvenTreeBarcodeMixin(models.Model):
|
||||
return True
|
||||
|
||||
def unassign_barcode(self):
|
||||
"""Unassign custom barcode from this model"""
|
||||
"""Unassign custom barcode from this model."""
|
||||
self.barcode_data = ''
|
||||
self.barcode_hash = ''
|
||||
|
||||
|
@ -8,7 +8,7 @@ import users.models
|
||||
|
||||
|
||||
def get_model_for_view(view, raise_error=True):
|
||||
"""Attempt to introspect the 'model' type for an API view"""
|
||||
"""Attempt to introspect the 'model' type for an API view."""
|
||||
if hasattr(view, 'get_permission_model'):
|
||||
return view.get_permission_model()
|
||||
|
||||
|
@ -16,7 +16,7 @@ logger = logging.getLogger('inventree')
|
||||
|
||||
|
||||
def default_sentry_dsn():
|
||||
"""Return the default Sentry.io DSN for InvenTree"""
|
||||
"""Return the default Sentry.io DSN for InvenTree."""
|
||||
return 'https://3928ccdba1d34895abde28031fd00100@o378676.ingest.sentry.io/6494600'
|
||||
|
||||
|
||||
@ -36,7 +36,7 @@ def sentry_ignore_errors():
|
||||
|
||||
|
||||
def init_sentry(dsn, sample_rate, tags):
|
||||
"""Initialize sentry.io error reporting"""
|
||||
"""Initialize sentry.io error reporting."""
|
||||
logger.info('Initializing sentry.io integration')
|
||||
|
||||
sentry_sdk.init(
|
||||
@ -62,7 +62,7 @@ def init_sentry(dsn, sample_rate, tags):
|
||||
|
||||
|
||||
def report_exception(exc):
|
||||
"""Report an exception to sentry.io"""
|
||||
"""Report an exception to sentry.io."""
|
||||
if settings.SENTRY_ENABLED and settings.SENTRY_DSN:
|
||||
if not any(isinstance(exc, e) for e in sentry_ignore_errors()):
|
||||
logger.info('Reporting exception to sentry.io: %s', exc)
|
||||
|
@ -75,10 +75,10 @@ class InvenTreeMoneySerializer(MoneyField):
|
||||
|
||||
|
||||
class InvenTreeCurrencySerializer(serializers.ChoiceField):
|
||||
"""Custom serializers for selecting currency option"""
|
||||
"""Custom serializers for selecting currency option."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the currency serializer"""
|
||||
"""Initialize the currency serializer."""
|
||||
choices = currency_code_mappings()
|
||||
|
||||
allow_blank = kwargs.get('allow_blank', False) or kwargs.get(
|
||||
@ -289,7 +289,7 @@ class InvenTreeModelSerializer(serializers.ModelSerializer):
|
||||
return self.instance
|
||||
|
||||
def create(self, validated_data):
|
||||
"""Custom create method which supports field adjustment"""
|
||||
"""Custom create method which supports field adjustment."""
|
||||
initial_data = validated_data.copy()
|
||||
|
||||
# Remove any fields which do not exist on the model
|
||||
@ -494,7 +494,7 @@ class InvenTreeAttachmentSerializer(InvenTreeModelSerializer):
|
||||
|
||||
@staticmethod
|
||||
def attachment_fields(extra_fields=None):
|
||||
"""Default set of fields for an attachment serializer"""
|
||||
"""Default set of fields for an attachment serializer."""
|
||||
fields = [
|
||||
'pk',
|
||||
'attachment',
|
||||
@ -827,7 +827,7 @@ class RemoteImageMixin(metaclass=serializers.SerializerMetaclass):
|
||||
"""
|
||||
|
||||
def skip_create_fields(self):
|
||||
"""Ensure the 'remote_image' field is skipped when creating a new instance"""
|
||||
"""Ensure the 'remote_image' field is skipped when creating a new instance."""
|
||||
return ['remote_image']
|
||||
|
||||
remote_image = serializers.URLField(
|
||||
|
@ -22,7 +22,7 @@ logger = logging.getLogger('inventree')
|
||||
|
||||
|
||||
class GenericOAuth2ApiLoginView(OAuth2LoginView):
|
||||
"""Api view to login a user with a social account"""
|
||||
"""Api view to login a user with a social account."""
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
"""Dispatch the regular login view directly."""
|
||||
@ -30,7 +30,7 @@ class GenericOAuth2ApiLoginView(OAuth2LoginView):
|
||||
|
||||
|
||||
class GenericOAuth2ApiConnectView(GenericOAuth2ApiLoginView):
|
||||
"""Api view to connect a social account to the current user"""
|
||||
"""Api view to connect a social account to the current user."""
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
"""Dispatch the connect request directly."""
|
||||
|
@ -1,4 +1,4 @@
|
||||
"""Helper functions for Single Sign On functionality"""
|
||||
"""Helper functions for Single Sign On functionality."""
|
||||
|
||||
import logging
|
||||
|
||||
@ -9,8 +9,7 @@ logger = logging.getLogger('inventree')
|
||||
|
||||
|
||||
def get_provider_app(provider):
|
||||
"""Return the SocialApp object for the given provider"""
|
||||
|
||||
"""Return the SocialApp object for the given provider."""
|
||||
from allauth.socialaccount.models import SocialApp
|
||||
|
||||
try:
|
||||
@ -36,7 +35,6 @@ def check_provider(provider, raise_error=False):
|
||||
- Provider must either have a registered SocialApp
|
||||
- Must have at least one site enabled
|
||||
"""
|
||||
|
||||
import allauth.app_settings
|
||||
|
||||
# First, check that the provider is enabled
|
||||
@ -56,8 +54,7 @@ def check_provider(provider, raise_error=False):
|
||||
|
||||
|
||||
def provider_display_name(provider):
|
||||
"""Return the 'display name' for the given provider"""
|
||||
|
||||
"""Return the 'display name' for the given provider."""
|
||||
if app := get_provider_app(provider):
|
||||
return app.name
|
||||
|
||||
@ -66,15 +63,15 @@ def provider_display_name(provider):
|
||||
|
||||
|
||||
def login_enabled() -> bool:
|
||||
"""Return True if SSO login is enabled"""
|
||||
"""Return True if SSO login is enabled."""
|
||||
return str2bool(InvenTreeSetting.get_setting('LOGIN_ENABLE_SSO'))
|
||||
|
||||
|
||||
def registration_enabled() -> bool:
|
||||
"""Return True if SSO registration is enabled"""
|
||||
"""Return True if SSO registration is enabled."""
|
||||
return str2bool(InvenTreeSetting.get_setting('LOGIN_ENABLE_SSO_REG'))
|
||||
|
||||
|
||||
def auto_registration_enabled() -> bool:
|
||||
"""Return True if SSO auto-registration is enabled"""
|
||||
"""Return True if SSO auto-registration is enabled."""
|
||||
return str2bool(InvenTreeSetting.get_setting('LOGIN_SIGNUP_SSO_AUTO'))
|
||||
|
@ -158,7 +158,7 @@ class BuildStatusGroups:
|
||||
|
||||
|
||||
class ReturnOrderStatus(StatusCode):
|
||||
"""Defines a set of status codes for a ReturnOrder"""
|
||||
"""Defines a set of status codes for a ReturnOrder."""
|
||||
|
||||
# Order is pending, waiting for receipt of items
|
||||
PENDING = 10, _('Pending'), 'secondary'
|
||||
@ -177,7 +177,7 @@ class ReturnOrderStatusGroups:
|
||||
|
||||
|
||||
class ReturnOrderLineStatus(StatusCode):
|
||||
"""Defines a set of status codes for a ReturnOrderLineItem"""
|
||||
"""Defines a set of status codes for a ReturnOrderLineItem."""
|
||||
|
||||
PENDING = 10, _('Pending'), 'secondary'
|
||||
|
||||
|
@ -78,8 +78,8 @@ def check_daily_holdoff(task_name: str, n_days: int = 1) -> bool:
|
||||
"""Check if a periodic task should be run, based on the provided setting name.
|
||||
|
||||
Arguments:
|
||||
task_name: The name of the task being run, e.g. 'dummy_task'
|
||||
setting_name: The name of the global setting, e.g. 'INVENTREE_DUMMY_TASK_INTERVAL'
|
||||
task_name (str): The name of the task being run, e.g. 'dummy_task'
|
||||
n_days (int): The number of days between task runs (default = 1)
|
||||
|
||||
Returns:
|
||||
bool: If the task should be run *now*, or wait another day
|
||||
@ -151,7 +151,7 @@ def check_daily_holdoff(task_name: str, n_days: int = 1) -> bool:
|
||||
|
||||
|
||||
def record_task_attempt(task_name: str):
|
||||
"""Record that a multi-day task has been attempted *now*"""
|
||||
"""Record that a multi-day task has been attempted *now*."""
|
||||
from common.models import InvenTreeSetting
|
||||
|
||||
logger.info("Logging task attempt for '%s'", task_name)
|
||||
@ -162,7 +162,7 @@ def record_task_attempt(task_name: str):
|
||||
|
||||
|
||||
def record_task_success(task_name: str):
|
||||
"""Record that a multi-day task was successful *now*"""
|
||||
"""Record that a multi-day task was successful *now*."""
|
||||
from common.models import InvenTreeSetting
|
||||
|
||||
InvenTreeSetting.set_setting(
|
||||
@ -365,7 +365,7 @@ def heartbeat():
|
||||
|
||||
@scheduled_task(ScheduledTask.DAILY)
|
||||
def delete_successful_tasks():
|
||||
"""Delete successful task logs which are older than a specified period"""
|
||||
"""Delete successful task logs which are older than a specified period."""
|
||||
try:
|
||||
from django_q.models import Success
|
||||
|
||||
@ -389,7 +389,7 @@ def delete_successful_tasks():
|
||||
|
||||
@scheduled_task(ScheduledTask.DAILY)
|
||||
def delete_failed_tasks():
|
||||
"""Delete failed task logs which are older than a specified period"""
|
||||
"""Delete failed task logs which are older than a specified period."""
|
||||
try:
|
||||
from django_q.models import Failure
|
||||
|
||||
@ -435,7 +435,7 @@ def delete_old_error_logs():
|
||||
|
||||
@scheduled_task(ScheduledTask.DAILY)
|
||||
def delete_old_notifications():
|
||||
"""Delete old notification logs"""
|
||||
"""Delete old notification logs."""
|
||||
try:
|
||||
from common.models import (
|
||||
InvenTreeSetting,
|
||||
@ -552,7 +552,7 @@ def check_for_updates():
|
||||
|
||||
@scheduled_task(ScheduledTask.DAILY)
|
||||
def update_exchange_rates(force: bool = False):
|
||||
"""Update currency exchange rates
|
||||
"""Update currency exchange rates.
|
||||
|
||||
Arguments:
|
||||
force: If True, force the update to run regardless of the last update time
|
||||
@ -648,8 +648,7 @@ def check_for_migrations():
|
||||
from plugin import registry
|
||||
|
||||
def set_pending_migrations(n: int):
|
||||
"""Helper function to inform the user about pending migrations"""
|
||||
|
||||
"""Helper function to inform the user about pending migrations."""
|
||||
logger.info('There are %s pending migrations', n)
|
||||
InvenTreeSetting.set_setting('_PENDING_MIGRATIONS', n, None)
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
"""Custom template loader for InvenTree"""
|
||||
"""Custom template loader for InvenTree."""
|
||||
|
||||
import os
|
||||
|
||||
@ -8,7 +8,7 @@ from django.template.loaders.cached import Loader as CachedLoader
|
||||
|
||||
|
||||
class InvenTreeTemplateLoader(CachedLoader):
|
||||
"""Custom template loader which bypasses cache for PDF export"""
|
||||
"""Custom template loader which bypasses cache for PDF export."""
|
||||
|
||||
def get_template(self, template_name, skip=None):
|
||||
"""Return a template object for the given template name.
|
||||
|
@ -257,12 +257,12 @@ class APITests(InvenTreeAPITestCase):
|
||||
|
||||
|
||||
class BulkDeleteTests(InvenTreeAPITestCase):
|
||||
"""Unit tests for the BulkDelete endpoints"""
|
||||
"""Unit tests for the BulkDelete endpoints."""
|
||||
|
||||
superuser = True
|
||||
|
||||
def test_errors(self):
|
||||
"""Test that the correct errors are thrown"""
|
||||
"""Test that the correct errors are thrown."""
|
||||
url = reverse('api-stock-test-result-list')
|
||||
|
||||
# DELETE without any of the required fields
|
||||
@ -285,7 +285,7 @@ class BulkDeleteTests(InvenTreeAPITestCase):
|
||||
|
||||
|
||||
class SearchTests(InvenTreeAPITestCase):
|
||||
"""Unit tests for global search endpoint"""
|
||||
"""Unit tests for global search endpoint."""
|
||||
|
||||
fixtures = [
|
||||
'category',
|
||||
@ -299,7 +299,7 @@ class SearchTests(InvenTreeAPITestCase):
|
||||
]
|
||||
|
||||
def test_empty(self):
|
||||
"""Test empty request"""
|
||||
"""Test empty request."""
|
||||
data = ['', None, {}]
|
||||
|
||||
for d in data:
|
||||
@ -307,7 +307,7 @@ class SearchTests(InvenTreeAPITestCase):
|
||||
self.assertIn('Search term must be provided', str(response.data))
|
||||
|
||||
def test_results(self):
|
||||
"""Test individual result types"""
|
||||
"""Test individual result types."""
|
||||
response = self.post(
|
||||
reverse('api-search'),
|
||||
{'search': 'chair', 'limit': 3, 'part': {}, 'build': {}},
|
||||
@ -339,7 +339,7 @@ class SearchTests(InvenTreeAPITestCase):
|
||||
self.assertNotIn('build', response.data)
|
||||
|
||||
def test_permissions(self):
|
||||
"""Test that users with insufficient permissions are handled correctly"""
|
||||
"""Test that users with insufficient permissions are handled correctly."""
|
||||
# First, remove all roles
|
||||
for ruleset in self.group.rule_sets.all():
|
||||
ruleset.can_view = False
|
||||
|
@ -28,7 +28,7 @@ class ViewTests(InvenTreeTestCase):
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
def get_index_page(self):
|
||||
"""Retrieve the index page (used for subsequent unit tests)"""
|
||||
"""Retrieve the index page (used for subsequent unit tests)."""
|
||||
response = self.client.get('/index/')
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
@ -44,7 +44,7 @@ class ViewTests(InvenTreeTestCase):
|
||||
# TODO: In future, run the javascript and ensure that the panels get created!
|
||||
|
||||
def test_settings_page(self):
|
||||
"""Test that the 'settings' page loads correctly"""
|
||||
"""Test that the 'settings' page loads correctly."""
|
||||
# Settings page loads
|
||||
url = reverse('settings')
|
||||
|
||||
@ -102,7 +102,7 @@ class ViewTests(InvenTreeTestCase):
|
||||
self.assertNotIn(f'panel-{panel}', content)
|
||||
|
||||
def test_url_login(self):
|
||||
"""Test logging in via arguments"""
|
||||
"""Test logging in via arguments."""
|
||||
# Log out
|
||||
self.client.logout()
|
||||
response = self.client.get('/index/')
|
||||
|
@ -40,10 +40,10 @@ from .validators import validate_overage
|
||||
|
||||
|
||||
class ConversionTest(TestCase):
|
||||
"""Tests for conversion of physical units"""
|
||||
"""Tests for conversion of physical units."""
|
||||
|
||||
def test_prefixes(self):
|
||||
"""Test inputs where prefixes are used"""
|
||||
"""Test inputs where prefixes are used."""
|
||||
tests = {
|
||||
'3': 3,
|
||||
'3m': 3,
|
||||
@ -58,7 +58,7 @@ class ConversionTest(TestCase):
|
||||
self.assertAlmostEqual(q, expected, 3)
|
||||
|
||||
def test_base_units(self):
|
||||
"""Test conversion to specified base units"""
|
||||
"""Test conversion to specified base units."""
|
||||
tests = {
|
||||
'3': 3,
|
||||
'3 dozen': 36,
|
||||
@ -76,7 +76,7 @@ class ConversionTest(TestCase):
|
||||
self.assertAlmostEqual(float(q.magnitude), expected, places=2)
|
||||
|
||||
def test_dimensionless_units(self):
|
||||
"""Tests for 'dimensionless' unit quantities"""
|
||||
"""Tests for 'dimensionless' unit quantities."""
|
||||
# Test some dimensionless units
|
||||
tests = {
|
||||
'ea': 1,
|
||||
@ -103,7 +103,7 @@ class ConversionTest(TestCase):
|
||||
self.assertAlmostEqual(q, expected, 3)
|
||||
|
||||
def test_invalid_units(self):
|
||||
"""Test conversion with bad units"""
|
||||
"""Test conversion with bad units."""
|
||||
tests = {'3': '10', '13': '-?-', '-3': 'xyz', '-12': '-12', '1/0': '1/0'}
|
||||
|
||||
for val, unit in tests.items():
|
||||
@ -111,7 +111,7 @@ class ConversionTest(TestCase):
|
||||
InvenTree.conversion.convert_physical_value(val, unit)
|
||||
|
||||
def test_invalid_values(self):
|
||||
"""Test conversion of invalid inputs"""
|
||||
"""Test conversion of invalid inputs."""
|
||||
inputs = ['-x', '1/0', 'xyz', '12B45C']
|
||||
|
||||
for val in inputs:
|
||||
@ -124,7 +124,7 @@ class ConversionTest(TestCase):
|
||||
InvenTree.conversion.convert_physical_value(val)
|
||||
|
||||
def test_custom_units(self):
|
||||
"""Tests for custom unit conversion"""
|
||||
"""Tests for custom unit conversion."""
|
||||
# Start with an empty set of units
|
||||
CustomUnit.objects.all().delete()
|
||||
InvenTree.conversion.reload_unit_registry()
|
||||
@ -193,8 +193,7 @@ class ValidatorTest(TestCase):
|
||||
validate_overage('aaaa')
|
||||
|
||||
def test_url_validation(self):
|
||||
"""Test for AllowedURLValidator"""
|
||||
|
||||
"""Test for AllowedURLValidator."""
|
||||
from common.models import InvenTreeSetting
|
||||
from part.models import Part, PartCategory
|
||||
|
||||
@ -225,10 +224,10 @@ class ValidatorTest(TestCase):
|
||||
|
||||
|
||||
class FormatTest(TestCase):
|
||||
"""Unit tests for custom string formatting functionality"""
|
||||
"""Unit tests for custom string formatting functionality."""
|
||||
|
||||
def test_parse(self):
|
||||
"""Tests for the 'parse_format_string' function"""
|
||||
"""Tests for the 'parse_format_string' function."""
|
||||
# Extract data from a valid format string
|
||||
fmt = 'PO-{abc:02f}-{ref:04d}-{date}-???'
|
||||
|
||||
@ -244,7 +243,7 @@ class FormatTest(TestCase):
|
||||
InvenTree.format.parse_format_string(fmt)
|
||||
|
||||
def test_create_regex(self):
|
||||
"""Test function for creating a regex from a format string"""
|
||||
"""Test function for creating a regex from a format string."""
|
||||
tests = {
|
||||
'PO-123-{ref:04f}': r'^PO\-123\-(?P<ref>.+)$',
|
||||
'{PO}-???-{ref}-{date}-22': r'^(?P<PO>.+)\-...\-(?P<ref>.+)\-(?P<date>.+)\-22$',
|
||||
@ -256,7 +255,7 @@ class FormatTest(TestCase):
|
||||
self.assertEqual(InvenTree.format.construct_format_regex(fmt), reg)
|
||||
|
||||
def test_validate_format(self):
|
||||
"""Test that string validation works as expected"""
|
||||
"""Test that string validation works as expected."""
|
||||
# These tests should pass
|
||||
for value, pattern in {
|
||||
'ABC-hello-123': '???-{q}-###',
|
||||
@ -276,7 +275,7 @@ class FormatTest(TestCase):
|
||||
self.assertFalse(InvenTree.format.validate_string(value, pattern))
|
||||
|
||||
def test_extract_value(self):
|
||||
"""Test that we can extract named values based on a format string"""
|
||||
"""Test that we can extract named values based on a format string."""
|
||||
# Simple tests based on a straight-forward format string
|
||||
fmt = 'PO-###-{ref:04d}'
|
||||
|
||||
@ -319,8 +318,7 @@ class FormatTest(TestCase):
|
||||
InvenTree.format.extract_named_group('test', 'PO-ABC-xyz', 'PO-###-{test}')
|
||||
|
||||
def test_currency_formatting(self):
|
||||
"""Test that currency formatting works correctly for multiple currencies"""
|
||||
|
||||
"""Test that currency formatting works correctly for multiple currencies."""
|
||||
test_data = (
|
||||
(Money(3651.285718, 'USD'), 4, '$3,651.2857'), # noqa: E201,E202
|
||||
(Money(487587.849178, 'CAD'), 5, 'CA$487,587.84918'), # noqa: E201,E202
|
||||
@ -352,7 +350,7 @@ class TestHelpers(TestCase):
|
||||
"""Tests for InvenTree helper functions."""
|
||||
|
||||
def test_absolute_url(self):
|
||||
"""Test helper function for generating an absolute URL"""
|
||||
"""Test helper function for generating an absolute URL."""
|
||||
base = 'https://demo.inventree.org:12345'
|
||||
|
||||
InvenTreeSetting.set_setting('INVENTREE_BASE_URL', base, change_user=None)
|
||||
@ -431,7 +429,7 @@ class TestHelpers(TestCase):
|
||||
self.assertEqual(helpers.decimal2string('test'), 'test')
|
||||
|
||||
def test_logo_image(self):
|
||||
"""Test for retrieving logo image"""
|
||||
"""Test for retrieving logo image."""
|
||||
# By default, there is no custom logo provided
|
||||
logo = helpers.getLogoImage()
|
||||
self.assertEqual(logo, '/static/img/inventree.png')
|
||||
@ -440,7 +438,7 @@ class TestHelpers(TestCase):
|
||||
self.assertEqual(logo, f'file://{settings.STATIC_ROOT}/img/inventree.png')
|
||||
|
||||
def test_download_image(self):
|
||||
"""Test function for downloading image from remote URL"""
|
||||
"""Test function for downloading image from remote URL."""
|
||||
# Run check with a sequence of bad URLs
|
||||
for url in ['blog', 'htp://test.com/?', 'google', '\\invalid-url']:
|
||||
with self.assertRaises(django_exceptions.ValidationError):
|
||||
@ -452,7 +450,6 @@ class TestHelpers(TestCase):
|
||||
As the httpstat.us service occasionally refuses a connection,
|
||||
we will simply try multiple times
|
||||
"""
|
||||
|
||||
tries = 0
|
||||
|
||||
with self.assertRaises(expected_error):
|
||||
@ -499,7 +496,7 @@ class TestHelpers(TestCase):
|
||||
InvenTree.helpers_model.download_image_from_url(large_img, timeout=10)
|
||||
|
||||
def test_model_mixin(self):
|
||||
"""Test the getModelsWithMixin function"""
|
||||
"""Test the getModelsWithMixin function."""
|
||||
from InvenTree.models import InvenTreeBarcodeMixin
|
||||
|
||||
models = InvenTree.helpers_model.getModelsWithMixin(InvenTreeBarcodeMixin)
|
||||
@ -1069,7 +1066,7 @@ class TestInstanceName(InvenTreeTestCase):
|
||||
|
||||
|
||||
class TestOffloadTask(InvenTreeTestCase):
|
||||
"""Tests for offloading tasks to the background worker"""
|
||||
"""Tests for offloading tasks to the background worker."""
|
||||
|
||||
fixtures = ['category', 'part', 'location', 'stock']
|
||||
|
||||
@ -1086,7 +1083,6 @@ class TestOffloadTask(InvenTreeTestCase):
|
||||
|
||||
Ref: https://github.com/inventree/InvenTree/pull/3273
|
||||
"""
|
||||
|
||||
self.assertTrue(
|
||||
offload_task(
|
||||
'dummy_tasks.stock',
|
||||
@ -1121,7 +1117,7 @@ class TestOffloadTask(InvenTreeTestCase):
|
||||
)
|
||||
|
||||
def test_daily_holdoff(self):
|
||||
"""Tests for daily task holdoff helper functions"""
|
||||
"""Tests for daily task holdoff helper functions."""
|
||||
import InvenTree.tasks
|
||||
|
||||
with self.assertLogs(logger='inventree', level='INFO') as cm:
|
||||
@ -1179,10 +1175,10 @@ class TestOffloadTask(InvenTreeTestCase):
|
||||
|
||||
|
||||
class BarcodeMixinTest(InvenTreeTestCase):
|
||||
"""Tests for the InvenTreeBarcodeMixin mixin class"""
|
||||
"""Tests for the InvenTreeBarcodeMixin mixin class."""
|
||||
|
||||
def test_barcode_model_type(self):
|
||||
"""Test that the barcode_model_type property works for each class"""
|
||||
"""Test that the barcode_model_type property works for each class."""
|
||||
from part.models import Part
|
||||
from stock.models import StockItem, StockLocation
|
||||
|
||||
@ -1191,7 +1187,7 @@ class BarcodeMixinTest(InvenTreeTestCase):
|
||||
self.assertEqual(StockLocation.barcode_model_type(), 'stocklocation')
|
||||
|
||||
def test_barcode_hash(self):
|
||||
"""Test that the barcode hashing function provides correct results"""
|
||||
"""Test that the barcode hashing function provides correct results."""
|
||||
# Test multiple values for the hashing function
|
||||
# This is to ensure that the hash function is always "backwards compatible"
|
||||
hashing_tests = {
|
||||
@ -1226,7 +1222,7 @@ class MagicLoginTest(InvenTreeTestCase):
|
||||
"""Test magic login token generation."""
|
||||
|
||||
def test_generation(self):
|
||||
"""Test that magic login tokens are generated correctly"""
|
||||
"""Test that magic login tokens are generated correctly."""
|
||||
# User does not exists
|
||||
resp = self.client.post(reverse('sesame-generate'), {'email': 1})
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
@ -1,4 +1,4 @@
|
||||
"""Translation helper functions"""
|
||||
"""Translation helper functions."""
|
||||
|
||||
import json
|
||||
|
||||
@ -9,7 +9,7 @@ _translation_stats = None
|
||||
|
||||
|
||||
def reload_translation_stats():
|
||||
"""Reload the translation stats from the compiled file"""
|
||||
"""Reload the translation stats from the compiled file."""
|
||||
global _translation_stats
|
||||
|
||||
STATS_FILE = settings.BASE_DIR.joinpath('InvenTree/locale_stats.json').absolute()
|
||||
@ -39,7 +39,7 @@ def reload_translation_stats():
|
||||
|
||||
|
||||
def get_translation_percent(lang_code):
|
||||
"""Return the translation percentage for the given language code"""
|
||||
"""Return the translation percentage for the given language code."""
|
||||
if _translation_stats is None:
|
||||
reload_translation_stats()
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
"""Helper functions for unit testing / CI"""
|
||||
"""Helper functions for unit testing / CI."""
|
||||
|
||||
import csv
|
||||
import io
|
||||
@ -109,7 +109,7 @@ class UserMixin:
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
"""Run setup for all tests in a given class"""
|
||||
"""Run setup for all tests in a given class."""
|
||||
super().setUpTestData()
|
||||
|
||||
# Create a user to log in with
|
||||
@ -139,7 +139,7 @@ class UserMixin:
|
||||
cls.assignRole(role=role, group=cls.group)
|
||||
|
||||
def setUp(self):
|
||||
"""Run setup for individual test methods"""
|
||||
"""Run setup for individual test methods."""
|
||||
if self.auto_login:
|
||||
self.client.login(username=self.username, password=self.password)
|
||||
|
||||
@ -198,10 +198,10 @@ class PluginMixin:
|
||||
|
||||
|
||||
class ExchangeRateMixin:
|
||||
"""Mixin class for generating exchange rate data"""
|
||||
"""Mixin class for generating exchange rate data."""
|
||||
|
||||
def generate_exchange_rates(self):
|
||||
"""Helper function which generates some exchange rates to work with"""
|
||||
"""Helper function which generates some exchange rates to work with."""
|
||||
rates = {'AUD': 1.5, 'CAD': 1.7, 'GBP': 0.9, 'USD': 1.0}
|
||||
|
||||
# Create a dummy backend
|
||||
@ -253,7 +253,7 @@ class InvenTreeAPITestCase(ExchangeRateMixin, UserMixin, APITestCase):
|
||||
self.assertLess(n, value, msg=msg)
|
||||
|
||||
def checkResponse(self, url, method, expected_code, response):
|
||||
"""Debug output for an unexpected response"""
|
||||
"""Debug output for an unexpected response."""
|
||||
# No expected code, return
|
||||
if expected_code is None:
|
||||
return
|
||||
|
@ -63,7 +63,6 @@ class AllowedURLValidator(validators.URLValidator):
|
||||
|
||||
def __call__(self, value):
|
||||
"""Validate the URL."""
|
||||
|
||||
import common.models
|
||||
|
||||
self.schemes = allowable_url_schemes()
|
||||
|
@ -30,8 +30,7 @@ except (NotGitRepository, FileNotFoundError):
|
||||
|
||||
|
||||
def checkMinPythonVersion():
|
||||
"""Check that the Python version is at least 3.9"""
|
||||
|
||||
"""Check that the Python version is at least 3.9."""
|
||||
version = sys.version.split(' ')[0]
|
||||
docs = 'https://docs.inventree.org/en/stable/start/intro/#python-requirements'
|
||||
|
||||
@ -199,7 +198,7 @@ def inventreeDjangoVersion():
|
||||
|
||||
|
||||
def inventreePythonVersion():
|
||||
"""Returns the version of python"""
|
||||
"""Returns the version of python."""
|
||||
return sys.version.split(' ')[0]
|
||||
|
||||
|
||||
|
@ -333,8 +333,8 @@ class AjaxUpdateView(AjaxMixin, UpdateView):
|
||||
"""Method for updating the object in the database. Default implementation is very simple, but can be overridden if required.
|
||||
|
||||
Args:
|
||||
object - The current object, to be updated
|
||||
form - The validated form
|
||||
object: The current object, to be updated
|
||||
form: The validated form
|
||||
|
||||
Returns:
|
||||
object instance for supplied form
|
||||
@ -651,7 +651,7 @@ class DatabaseStatsView(AjaxView):
|
||||
|
||||
|
||||
class AboutView(AjaxView):
|
||||
"""A view for displaying InvenTree version information"""
|
||||
"""A view for displaying InvenTree version information."""
|
||||
|
||||
ajax_template_name = 'about.html'
|
||||
ajax_form_title = _('About InvenTree')
|
||||
|
@ -84,7 +84,7 @@ class BuildAdmin(ImportExportModelAdmin):
|
||||
|
||||
|
||||
class BuildItemAdmin(admin.ModelAdmin):
|
||||
"""Class for managing the BuildItem model via the admin interface"""
|
||||
"""Class for managing the BuildItem model via the admin interface."""
|
||||
|
||||
list_display = (
|
||||
'stock_item',
|
||||
|
@ -110,12 +110,12 @@ class WebhookView(CsrfExemptMixin, APIView):
|
||||
|
||||
|
||||
class CurrencyExchangeView(APIView):
|
||||
"""API endpoint for displaying currency information"""
|
||||
"""API endpoint for displaying currency information."""
|
||||
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
def get(self, request, format=None):
|
||||
"""Return information on available currency conversions"""
|
||||
"""Return information on available currency conversions."""
|
||||
# Extract a list of all available rates
|
||||
try:
|
||||
rates = Rate.objects.all()
|
||||
@ -157,7 +157,7 @@ class CurrencyRefreshView(APIView):
|
||||
permission_classes = [permissions.IsAuthenticated, permissions.IsAdminUser]
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
"""Performing a POST request will update currency exchange rates"""
|
||||
"""Performing a POST request will update currency exchange rates."""
|
||||
from InvenTree.tasks import update_exchange_rates
|
||||
|
||||
update_exchange_rates(force=True)
|
||||
@ -185,7 +185,7 @@ class GlobalSettingsList(SettingsList):
|
||||
serializer_class = common.serializers.GlobalSettingsSerializer
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
"""Ensure all global settings are created"""
|
||||
"""Ensure all global settings are created."""
|
||||
common.models.InvenTreeSetting.build_default_values()
|
||||
return super().list(request, *args, **kwargs)
|
||||
|
||||
@ -241,7 +241,7 @@ class UserSettingsList(SettingsList):
|
||||
serializer_class = common.serializers.UserSettingsSerializer
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
"""Ensure all user settings are created"""
|
||||
"""Ensure all user settings are created."""
|
||||
common.models.InvenTreeUserSetting.build_default_values(user=request.user)
|
||||
return super().list(request, *args, **kwargs)
|
||||
|
||||
@ -361,7 +361,7 @@ class NotificationList(NotificationMessageMixin, BulkDeleteMixin, ListAPI):
|
||||
return queryset
|
||||
|
||||
def filter_delete_queryset(self, queryset, request):
|
||||
"""Ensure that the user can only delete their *own* notifications"""
|
||||
"""Ensure that the user can only delete their *own* notifications."""
|
||||
queryset = queryset.filter(user=request.user)
|
||||
return queryset
|
||||
|
||||
@ -440,7 +440,7 @@ class NotesImageList(ListCreateAPI):
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
def perform_create(self, serializer):
|
||||
"""Create (upload) a new notes image"""
|
||||
"""Create (upload) a new notes image."""
|
||||
image = serializer.save()
|
||||
image.user = self.request.user
|
||||
image.save()
|
||||
@ -460,7 +460,7 @@ class ProjectCodeList(ListCreateAPI):
|
||||
|
||||
|
||||
class ProjectCodeDetail(RetrieveUpdateDestroyAPI):
|
||||
"""Detail view for a particular project code"""
|
||||
"""Detail view for a particular project code."""
|
||||
|
||||
queryset = common.models.ProjectCode.objects.all()
|
||||
serializer_class = common.serializers.ProjectCodeSerializer
|
||||
@ -468,7 +468,7 @@ class ProjectCodeDetail(RetrieveUpdateDestroyAPI):
|
||||
|
||||
|
||||
class CustomUnitList(ListCreateAPI):
|
||||
"""List view for custom units"""
|
||||
"""List view for custom units."""
|
||||
|
||||
queryset = common.models.CustomUnit.objects.all()
|
||||
serializer_class = common.serializers.CustomUnitSerializer
|
||||
@ -477,7 +477,7 @@ class CustomUnitList(ListCreateAPI):
|
||||
|
||||
|
||||
class CustomUnitDetail(RetrieveUpdateDestroyAPI):
|
||||
"""Detail view for a particular custom unit"""
|
||||
"""Detail view for a particular custom unit."""
|
||||
|
||||
queryset = common.models.CustomUnit.objects.all()
|
||||
serializer_class = common.serializers.CustomUnitSerializer
|
||||
|
@ -19,7 +19,6 @@ class CommonConfig(AppConfig):
|
||||
|
||||
def ready(self):
|
||||
"""Initialize restart flag clearance on startup."""
|
||||
|
||||
if InvenTree.ready.isRunningMigrations():
|
||||
return
|
||||
|
||||
|
@ -77,14 +77,15 @@ class MetaMixin(models.Model):
|
||||
|
||||
|
||||
class BaseURLValidator(URLValidator):
|
||||
"""Validator for the InvenTree base URL:
|
||||
"""Validator for the InvenTree base URL.
|
||||
|
||||
Rules:
|
||||
- Allow empty value
|
||||
- Allow value without specified TLD (top level domain)
|
||||
"""
|
||||
|
||||
def __init__(self, schemes=None, **kwargs):
|
||||
"""Custom init routine"""
|
||||
"""Custom init routine."""
|
||||
super().__init__(schemes, **kwargs)
|
||||
|
||||
# Override default host_re value - allow optional tld regex
|
||||
@ -145,7 +146,7 @@ class ProjectCode(InvenTree.models.MetadataMixin, models.Model):
|
||||
|
||||
|
||||
class SettingsKeyType(TypedDict, total=False):
|
||||
"""Type definitions for a SettingsKeyType
|
||||
"""Type definitions for a SettingsKeyType.
|
||||
|
||||
Attributes:
|
||||
name: Translatable string name of the setting (required)
|
||||
@ -216,7 +217,7 @@ class BaseInvenTreeSetting(models.Model):
|
||||
|
||||
@classmethod
|
||||
def build_default_values(cls, **kwargs):
|
||||
"""Ensure that all values defined in SETTINGS are present in the database
|
||||
"""Ensure that all values defined in SETTINGS are present in the database.
|
||||
|
||||
If a particular setting is not present, create it with the default value
|
||||
"""
|
||||
@ -269,13 +270,13 @@ class BaseInvenTreeSetting(models.Model):
|
||||
|
||||
@property
|
||||
def cache_key(self):
|
||||
"""Generate a unique cache key for this settings object"""
|
||||
"""Generate a unique cache key for this settings object."""
|
||||
return self.__class__.create_cache_key(
|
||||
self.key, **self.get_filters_for_instance()
|
||||
)
|
||||
|
||||
def save_to_cache(self):
|
||||
"""Save this setting object to cache"""
|
||||
"""Save this setting object to cache."""
|
||||
ckey = self.cache_key
|
||||
|
||||
# skip saving to cache if no pk is set
|
||||
@ -308,7 +309,7 @@ class BaseInvenTreeSetting(models.Model):
|
||||
|
||||
@classmethod
|
||||
def get_filters(cls, **kwargs):
|
||||
"""Enable to filter by other kwargs defined in cls.extra_unique_fields"""
|
||||
"""Enable to filter by other kwargs defined in cls.extra_unique_fields."""
|
||||
return {
|
||||
key: value
|
||||
for key, value in kwargs.items()
|
||||
@ -316,7 +317,7 @@ class BaseInvenTreeSetting(models.Model):
|
||||
}
|
||||
|
||||
def get_filters_for_instance(self):
|
||||
"""Enable to filter by other fields defined in self.extra_unique_fields"""
|
||||
"""Enable to filter by other fields defined in self.extra_unique_fields."""
|
||||
return {
|
||||
key: getattr(self, key, None)
|
||||
for key in self.extra_unique_fields
|
||||
@ -1090,7 +1091,7 @@ def validate_email_domains(setting):
|
||||
|
||||
|
||||
def currency_exchange_plugins():
|
||||
"""Return a set of plugin choices which can be used for currency exchange"""
|
||||
"""Return a set of plugin choices which can be used for currency exchange."""
|
||||
try:
|
||||
from plugin import registry
|
||||
|
||||
@ -1102,7 +1103,7 @@ def currency_exchange_plugins():
|
||||
|
||||
|
||||
def update_exchange_rates(setting):
|
||||
"""Update exchange rates when base currency is changed"""
|
||||
"""Update exchange rates when base currency is changed."""
|
||||
if InvenTree.ready.isImportingData():
|
||||
return
|
||||
|
||||
@ -1113,7 +1114,7 @@ def update_exchange_rates(setting):
|
||||
|
||||
|
||||
def reload_plugin_registry(setting):
|
||||
"""When a core plugin setting is changed, reload the plugin registry"""
|
||||
"""When a core plugin setting is changed, reload the plugin registry."""
|
||||
from plugin import registry
|
||||
|
||||
logger.info("Reloading plugin registry due to change in setting '%s'", setting.key)
|
||||
@ -2752,7 +2753,7 @@ class NotificationEntry(MetaMixin):
|
||||
|
||||
|
||||
class NotificationMessage(models.Model):
|
||||
"""A NotificationMessage is a message sent to a particular user, notifying them of some *important information*
|
||||
"""A NotificationMessage is a message sent to a particular user, notifying them of some *important information*.
|
||||
|
||||
Notification messages can be generated by a variety of sources.
|
||||
|
||||
@ -2870,7 +2871,7 @@ class NotesImage(models.Model):
|
||||
|
||||
|
||||
class CustomUnit(models.Model):
|
||||
"""Model for storing custom physical unit definitions
|
||||
"""Model for storing custom physical unit definitions.
|
||||
|
||||
Model Attributes:
|
||||
name: Name of the unit
|
||||
@ -2882,7 +2883,7 @@ class CustomUnit(models.Model):
|
||||
"""
|
||||
|
||||
def fmt_string(self):
|
||||
"""Construct a unit definition string e.g. 'dog_year = 52 * day = dy'"""
|
||||
"""Construct a unit definition string e.g. 'dog_year = 52 * day = dy'."""
|
||||
fmt = f'{self.name} = {self.definition}'
|
||||
|
||||
if self.symbol:
|
||||
@ -2891,7 +2892,7 @@ class CustomUnit(models.Model):
|
||||
return fmt
|
||||
|
||||
def clean(self):
|
||||
"""Validate that the provided custom unit is indeed valid"""
|
||||
"""Validate that the provided custom unit is indeed valid."""
|
||||
super().clean()
|
||||
|
||||
from InvenTree.conversion import get_unit_registry
|
||||
@ -2946,7 +2947,7 @@ class CustomUnit(models.Model):
|
||||
@receiver(post_save, sender=CustomUnit, dispatch_uid='custom_unit_saved')
|
||||
@receiver(post_delete, sender=CustomUnit, dispatch_uid='custom_unit_deleted')
|
||||
def after_custom_unit_updated(sender, instance, **kwargs):
|
||||
"""Callback when a custom unit is updated or deleted"""
|
||||
"""Callback when a custom unit is updated or deleted."""
|
||||
# Force reload of the unit registry
|
||||
from InvenTree.conversion import reload_unit_registry
|
||||
|
||||
|
@ -263,7 +263,7 @@ class UIMessageNotification(SingleNotificationMethod):
|
||||
METHOD_NAME = 'ui_message'
|
||||
|
||||
def get_targets(self):
|
||||
"""Only send notifications for active users"""
|
||||
"""Only send notifications for active users."""
|
||||
return [target for target in self.targets if target.is_active]
|
||||
|
||||
def send(self, target):
|
||||
|
@ -23,14 +23,14 @@ class SettingsValueField(serializers.Field):
|
||||
return instance
|
||||
|
||||
def to_representation(self, instance):
|
||||
"""Return the value of the setting:
|
||||
"""Return the value of the setting.
|
||||
|
||||
- Protected settings are returned as '***'
|
||||
Protected settings are returned as '***'
|
||||
"""
|
||||
return '***' if instance.protected else str(instance.value)
|
||||
|
||||
def to_internal_value(self, data):
|
||||
"""Return the internal value of the setting"""
|
||||
"""Return the internal value of the setting."""
|
||||
return str(data)
|
||||
|
||||
|
||||
|
@ -11,7 +11,7 @@ logger = logging.getLogger('inventree')
|
||||
|
||||
|
||||
def currency_code_default():
|
||||
"""Returns the default currency code (or USD if not specified)"""
|
||||
"""Returns the default currency code (or USD if not specified)."""
|
||||
from common.models import InvenTreeSetting
|
||||
|
||||
cached_value = cache.get('currency_code_default', '')
|
||||
|
@ -134,14 +134,14 @@ class SettingsTest(InvenTreeTestCase):
|
||||
self.assertNotIn('SERVER_RESTART_REQUIRED', result)
|
||||
|
||||
def test_all_settings(self):
|
||||
"""Make sure that the all_settings function returns correctly"""
|
||||
"""Make sure that the all_settings function returns correctly."""
|
||||
result = InvenTreeSetting.all_settings()
|
||||
self.assertIn('INVENTREE_INSTANCE', result)
|
||||
self.assertIsInstance(result['INVENTREE_INSTANCE'], InvenTreeSetting)
|
||||
|
||||
@mock.patch('common.models.InvenTreeSetting.get_setting_definition')
|
||||
def test_check_all_settings(self, get_setting_definition):
|
||||
"""Make sure that the check_all_settings function returns correctly"""
|
||||
"""Make sure that the check_all_settings function returns correctly."""
|
||||
# define partial schema
|
||||
settings_definition = {
|
||||
'AB': { # key that's has not already been accessed
|
||||
@ -295,7 +295,7 @@ class SettingsTest(InvenTreeTestCase):
|
||||
) # pragma: no cover
|
||||
|
||||
def test_global_setting_caching(self):
|
||||
"""Test caching operations for the global settings class"""
|
||||
"""Test caching operations for the global settings class."""
|
||||
key = 'PART_NAME_FORMAT'
|
||||
|
||||
cache_key = InvenTreeSetting.create_cache_key(key)
|
||||
@ -316,7 +316,7 @@ class SettingsTest(InvenTreeTestCase):
|
||||
self.assertEqual(InvenTreeSetting.get_setting(key), val)
|
||||
|
||||
def test_user_setting_caching(self):
|
||||
"""Test caching operation for the user settings class"""
|
||||
"""Test caching operation for the user settings class."""
|
||||
cache.clear()
|
||||
|
||||
# Generate a number of new users
|
||||
@ -348,7 +348,7 @@ class GlobalSettingsApiTest(InvenTreeAPITestCase):
|
||||
"""Tests for the global settings API."""
|
||||
|
||||
def setUp(self):
|
||||
"""Ensure cache is cleared as part of test setup"""
|
||||
"""Ensure cache is cleared as part of test setup."""
|
||||
cache.clear()
|
||||
return super().setUp()
|
||||
|
||||
@ -825,7 +825,7 @@ class NotificationTest(InvenTreeAPITestCase):
|
||||
response = self.post(url, {}, expected_code=405)
|
||||
|
||||
def test_bulk_delete(self):
|
||||
"""Tests for bulk deletion of user notifications"""
|
||||
"""Tests for bulk deletion of user notifications."""
|
||||
from error_report.models import Error
|
||||
|
||||
# Create some notification messages by throwing errors
|
||||
@ -1004,17 +1004,17 @@ class ColorThemeTest(TestCase):
|
||||
|
||||
|
||||
class CurrencyAPITests(InvenTreeAPITestCase):
|
||||
"""Unit tests for the currency exchange API endpoints"""
|
||||
"""Unit tests for the currency exchange API endpoints."""
|
||||
|
||||
def test_exchange_endpoint(self):
|
||||
"""Test that the currency exchange endpoint works as expected"""
|
||||
"""Test that the currency exchange endpoint works as expected."""
|
||||
response = self.get(reverse('api-currency-exchange'), expected_code=200)
|
||||
|
||||
self.assertIn('base_currency', response.data)
|
||||
self.assertIn('exchange_rates', response.data)
|
||||
|
||||
def test_refresh_endpoint(self):
|
||||
"""Call the 'refresh currencies' endpoint"""
|
||||
"""Call the 'refresh currencies' endpoint."""
|
||||
from djmoney.contrib.exchange.models import Rate
|
||||
|
||||
# Delete any existing exchange rate data
|
||||
@ -1074,7 +1074,7 @@ class NotesImageTest(InvenTreeAPITestCase):
|
||||
self.assertEqual(NotesImage.objects.count(), n)
|
||||
|
||||
def test_valid_image(self):
|
||||
"""Test upload of a valid image file"""
|
||||
"""Test upload of a valid image file."""
|
||||
n = NotesImage.objects.count()
|
||||
|
||||
# Construct a simple image file
|
||||
@ -1100,16 +1100,16 @@ class NotesImageTest(InvenTreeAPITestCase):
|
||||
|
||||
|
||||
class ProjectCodesTest(InvenTreeAPITestCase):
|
||||
"""Units tests for the ProjectCodes model and API endpoints"""
|
||||
"""Units tests for the ProjectCodes model and API endpoints."""
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
"""Return the URL for the project code list endpoint"""
|
||||
"""Return the URL for the project code list endpoint."""
|
||||
return reverse('api-project-code-list')
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
"""Create some initial project codes"""
|
||||
"""Create some initial project codes."""
|
||||
super().setUpTestData()
|
||||
|
||||
codes = [
|
||||
@ -1122,12 +1122,12 @@ class ProjectCodesTest(InvenTreeAPITestCase):
|
||||
ProjectCode.objects.bulk_create(codes)
|
||||
|
||||
def test_list(self):
|
||||
"""Test that the list endpoint works as expected"""
|
||||
"""Test that the list endpoint works as expected."""
|
||||
response = self.get(self.url, expected_code=200)
|
||||
self.assertEqual(len(response.data), ProjectCode.objects.count())
|
||||
|
||||
def test_delete(self):
|
||||
"""Test we can delete a project code via the API"""
|
||||
"""Test we can delete a project code via the API."""
|
||||
n = ProjectCode.objects.count()
|
||||
|
||||
# Get the first project code
|
||||
@ -1143,7 +1143,7 @@ class ProjectCodesTest(InvenTreeAPITestCase):
|
||||
self.assertEqual(ProjectCode.objects.count(), n - 1)
|
||||
|
||||
def test_duplicate_code(self):
|
||||
"""Test that we cannot create two project codes with the same code"""
|
||||
"""Test that we cannot create two project codes with the same code."""
|
||||
# Create a new project code
|
||||
response = self.post(
|
||||
self.url,
|
||||
@ -1157,7 +1157,7 @@ class ProjectCodesTest(InvenTreeAPITestCase):
|
||||
)
|
||||
|
||||
def test_write_access(self):
|
||||
"""Test that non-staff users have read-only access"""
|
||||
"""Test that non-staff users have read-only access."""
|
||||
# By default user has staff access, can create a new project code
|
||||
response = self.post(
|
||||
self.url,
|
||||
@ -1196,16 +1196,16 @@ class ProjectCodesTest(InvenTreeAPITestCase):
|
||||
|
||||
|
||||
class CustomUnitAPITest(InvenTreeAPITestCase):
|
||||
"""Unit tests for the CustomUnit API"""
|
||||
"""Unit tests for the CustomUnit API."""
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
"""Return the API endpoint for the CustomUnit list"""
|
||||
"""Return the API endpoint for the CustomUnit list."""
|
||||
return reverse('api-custom-unit-list')
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
"""Construct some initial test fixture data"""
|
||||
"""Construct some initial test fixture data."""
|
||||
super().setUpTestData()
|
||||
|
||||
units = [
|
||||
@ -1222,12 +1222,12 @@ class CustomUnitAPITest(InvenTreeAPITestCase):
|
||||
CustomUnit.objects.bulk_create(units)
|
||||
|
||||
def test_list(self):
|
||||
"""Test API list functionality"""
|
||||
"""Test API list functionality."""
|
||||
response = self.get(self.url, expected_code=200)
|
||||
self.assertEqual(len(response.data), CustomUnit.objects.count())
|
||||
|
||||
def test_edit(self):
|
||||
"""Test edit permissions for CustomUnit model"""
|
||||
"""Test edit permissions for CustomUnit model."""
|
||||
unit = CustomUnit.objects.first()
|
||||
|
||||
# Try to edit without permission
|
||||
@ -1254,7 +1254,7 @@ class CustomUnitAPITest(InvenTreeAPITestCase):
|
||||
self.assertEqual(unit.name, 'new_unit_name')
|
||||
|
||||
def test_validation(self):
|
||||
"""Test that validation works as expected"""
|
||||
"""Test that validation works as expected."""
|
||||
unit = CustomUnit.objects.first()
|
||||
|
||||
self.user.is_staff = True
|
||||
|
@ -1,4 +1,4 @@
|
||||
"""Admin class for the 'company' app"""
|
||||
"""Admin class for the 'company' app."""
|
||||
|
||||
from django.contrib import admin
|
||||
|
||||
@ -25,7 +25,7 @@ class CompanyResource(InvenTreeResource):
|
||||
"""Class for managing Company data import/export."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass defines extra options"""
|
||||
"""Metaclass defines extra options."""
|
||||
|
||||
model = Company
|
||||
skip_unchanged = True
|
||||
@ -34,7 +34,7 @@ class CompanyResource(InvenTreeResource):
|
||||
|
||||
|
||||
class CompanyAdmin(ImportExportModelAdmin):
|
||||
"""Admin class for the Company model"""
|
||||
"""Admin class for the Company model."""
|
||||
|
||||
resource_class = CompanyResource
|
||||
|
||||
@ -47,7 +47,7 @@ class SupplierPartResource(InvenTreeResource):
|
||||
"""Class for managing SupplierPart data import/export."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass defines extra admin options"""
|
||||
"""Metaclass defines extra admin options."""
|
||||
|
||||
model = SupplierPart
|
||||
skip_unchanged = True
|
||||
@ -64,13 +64,13 @@ class SupplierPartResource(InvenTreeResource):
|
||||
|
||||
|
||||
class SupplierPriceBreakInline(admin.TabularInline):
|
||||
"""Inline for supplier-part pricing"""
|
||||
"""Inline for supplier-part pricing."""
|
||||
|
||||
model = SupplierPriceBreak
|
||||
|
||||
|
||||
class SupplierPartAdmin(ImportExportModelAdmin):
|
||||
"""Admin class for the SupplierPart model"""
|
||||
"""Admin class for the SupplierPart model."""
|
||||
|
||||
resource_class = SupplierPartResource
|
||||
|
||||
@ -87,7 +87,7 @@ class ManufacturerPartResource(InvenTreeResource):
|
||||
"""Class for managing ManufacturerPart data import/export."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass defines extra admin options"""
|
||||
"""Metaclass defines extra admin options."""
|
||||
|
||||
model = ManufacturerPart
|
||||
skip_unchanged = True
|
||||
@ -129,7 +129,7 @@ class ManufacturerPartParameterResource(InvenTreeResource):
|
||||
"""Class for managing ManufacturerPartParameter data import/export."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass defines extra admin options"""
|
||||
"""Metaclass defines extra admin options."""
|
||||
|
||||
model = ManufacturerPartParameter
|
||||
skip_unchanged = True
|
||||
@ -153,7 +153,7 @@ class SupplierPriceBreakResource(InvenTreeResource):
|
||||
"""Class for managing SupplierPriceBreak data import/export."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass defines extra admin options"""
|
||||
"""Metaclass defines extra admin options."""
|
||||
|
||||
model = SupplierPriceBreak
|
||||
skip_unchanged = True
|
||||
@ -174,7 +174,7 @@ class SupplierPriceBreakResource(InvenTreeResource):
|
||||
|
||||
|
||||
class SupplierPriceBreakAdmin(ImportExportModelAdmin):
|
||||
"""Admin class for the SupplierPriceBreak model"""
|
||||
"""Admin class for the SupplierPriceBreak model."""
|
||||
|
||||
resource_class = SupplierPriceBreakResource
|
||||
|
||||
@ -184,10 +184,10 @@ class SupplierPriceBreakAdmin(ImportExportModelAdmin):
|
||||
|
||||
|
||||
class AddressResource(InvenTreeResource):
|
||||
"""Class for managing Address data import/export"""
|
||||
"""Class for managing Address data import/export."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass defining extra options"""
|
||||
"""Metaclass defining extra options."""
|
||||
|
||||
model = Address
|
||||
skip_unchanged = True
|
||||
@ -198,7 +198,7 @@ class AddressResource(InvenTreeResource):
|
||||
|
||||
|
||||
class AddressAdmin(ImportExportModelAdmin):
|
||||
"""Admin class for the Address model"""
|
||||
"""Admin class for the Address model."""
|
||||
|
||||
resource_class = AddressResource
|
||||
|
||||
@ -208,10 +208,10 @@ class AddressAdmin(ImportExportModelAdmin):
|
||||
|
||||
|
||||
class ContactResource(InvenTreeResource):
|
||||
"""Class for managing Contact data import/export"""
|
||||
"""Class for managing Contact data import/export."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass defining extra options"""
|
||||
"""Metaclass defining extra options."""
|
||||
|
||||
model = Contact
|
||||
skip_unchanged = True
|
||||
@ -222,7 +222,7 @@ class ContactResource(InvenTreeResource):
|
||||
|
||||
|
||||
class ContactAdmin(ImportExportModelAdmin):
|
||||
"""Admin class for the Contact model"""
|
||||
"""Admin class for the Contact model."""
|
||||
|
||||
resource_class = ContactResource
|
||||
|
||||
|
@ -52,7 +52,7 @@ class CompanyList(ListCreateAPI):
|
||||
queryset = Company.objects.all()
|
||||
|
||||
def get_queryset(self):
|
||||
"""Return annotated queryset for the company list endpoint"""
|
||||
"""Return annotated queryset for the company list endpoint."""
|
||||
queryset = super().get_queryset()
|
||||
queryset = CompanySerializer.annotate_queryset(queryset)
|
||||
|
||||
@ -76,7 +76,7 @@ class CompanyDetail(RetrieveUpdateDestroyAPI):
|
||||
serializer_class = CompanySerializer
|
||||
|
||||
def get_queryset(self):
|
||||
"""Return annotated queryset for the company detail endpoint"""
|
||||
"""Return annotated queryset for the company detail endpoint."""
|
||||
queryset = super().get_queryset()
|
||||
queryset = CompanySerializer.annotate_queryset(queryset)
|
||||
|
||||
@ -84,7 +84,7 @@ class CompanyDetail(RetrieveUpdateDestroyAPI):
|
||||
|
||||
|
||||
class CompanyAttachmentList(AttachmentMixin, ListCreateDestroyAPIView):
|
||||
"""API endpoint for the CompanyAttachment model"""
|
||||
"""API endpoint for the CompanyAttachment model."""
|
||||
|
||||
queryset = CompanyAttachment.objects.all()
|
||||
serializer_class = CompanyAttachmentSerializer
|
||||
@ -100,7 +100,7 @@ class CompanyAttachmentDetail(AttachmentMixin, RetrieveUpdateDestroyAPI):
|
||||
|
||||
|
||||
class ContactList(ListCreateDestroyAPIView):
|
||||
"""API endpoint for list view of Company model"""
|
||||
"""API endpoint for list view of Company model."""
|
||||
|
||||
queryset = Contact.objects.all()
|
||||
serializer_class = ContactSerializer
|
||||
@ -117,14 +117,14 @@ class ContactList(ListCreateDestroyAPIView):
|
||||
|
||||
|
||||
class ContactDetail(RetrieveUpdateDestroyAPI):
|
||||
"""Detail endpoint for Company model"""
|
||||
"""Detail endpoint for Company model."""
|
||||
|
||||
queryset = Contact.objects.all()
|
||||
serializer_class = ContactSerializer
|
||||
|
||||
|
||||
class AddressList(ListCreateDestroyAPIView):
|
||||
"""API endpoint for list view of Address model"""
|
||||
"""API endpoint for list view of Address model."""
|
||||
|
||||
queryset = Address.objects.all()
|
||||
serializer_class = AddressSerializer
|
||||
@ -139,7 +139,7 @@ class AddressList(ListCreateDestroyAPIView):
|
||||
|
||||
|
||||
class AddressDetail(RetrieveUpdateDestroyAPI):
|
||||
"""API endpoint for a single Address object"""
|
||||
"""API endpoint for a single Address object."""
|
||||
|
||||
queryset = Address.objects.all()
|
||||
serializer_class = AddressSerializer
|
||||
@ -173,7 +173,7 @@ class ManufacturerPartList(ListCreateDestroyAPIView):
|
||||
filterset_class = ManufacturerPartFilter
|
||||
|
||||
def get_serializer(self, *args, **kwargs):
|
||||
"""Return serializer instance for this endpoint"""
|
||||
"""Return serializer instance for this endpoint."""
|
||||
# Do we wish to include extra detail?
|
||||
try:
|
||||
params = self.request.query_params
|
||||
@ -233,10 +233,10 @@ class ManufacturerPartAttachmentDetail(AttachmentMixin, RetrieveUpdateDestroyAPI
|
||||
|
||||
|
||||
class ManufacturerPartParameterFilter(rest_filters.FilterSet):
|
||||
"""Custom filterset for the ManufacturerPartParameterList API endpoint"""
|
||||
"""Custom filterset for the ManufacturerPartParameterList API endpoint."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options"""
|
||||
"""Metaclass options."""
|
||||
|
||||
model = ManufacturerPartParameter
|
||||
fields = ['name', 'value', 'units', 'manufacturer_part']
|
||||
@ -258,7 +258,7 @@ class ManufacturerPartParameterList(ListCreateDestroyAPIView):
|
||||
filterset_class = ManufacturerPartParameterFilter
|
||||
|
||||
def get_serializer(self, *args, **kwargs):
|
||||
"""Return serializer instance for this endpoint"""
|
||||
"""Return serializer instance for this endpoint."""
|
||||
# Do we wish to include any extra detail?
|
||||
try:
|
||||
params = self.request.query_params
|
||||
@ -288,10 +288,10 @@ class ManufacturerPartParameterDetail(RetrieveUpdateDestroyAPI):
|
||||
|
||||
|
||||
class SupplierPartFilter(rest_filters.FilterSet):
|
||||
"""API filters for the SupplierPartList endpoint"""
|
||||
"""API filters for the SupplierPartList endpoint."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass option"""
|
||||
"""Metaclass option."""
|
||||
|
||||
model = SupplierPart
|
||||
fields = [
|
||||
@ -325,7 +325,7 @@ class SupplierPartList(ListCreateDestroyAPIView):
|
||||
filterset_class = SupplierPartFilter
|
||||
|
||||
def get_queryset(self, *args, **kwargs):
|
||||
"""Return annotated queryest object for the SupplierPart list"""
|
||||
"""Return annotated queryest object for the SupplierPart list."""
|
||||
queryset = super().get_queryset(*args, **kwargs)
|
||||
queryset = SupplierPartSerializer.annotate_queryset(queryset)
|
||||
|
||||
@ -354,7 +354,7 @@ class SupplierPartList(ListCreateDestroyAPIView):
|
||||
return queryset
|
||||
|
||||
def get_serializer(self, *args, **kwargs):
|
||||
"""Return serializer instance for this endpoint"""
|
||||
"""Return serializer instance for this endpoint."""
|
||||
# Do we wish to include extra detail?
|
||||
try:
|
||||
params = self.request.query_params
|
||||
@ -425,10 +425,10 @@ class SupplierPartDetail(RetrieveUpdateDestroyAPI):
|
||||
|
||||
|
||||
class SupplierPriceBreakFilter(rest_filters.FilterSet):
|
||||
"""Custom API filters for the SupplierPriceBreak list endpoint"""
|
||||
"""Custom API filters for the SupplierPriceBreak list endpoint."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options"""
|
||||
"""Metaclass options."""
|
||||
|
||||
model = SupplierPriceBreak
|
||||
fields = ['part', 'quantity']
|
||||
@ -456,7 +456,7 @@ class SupplierPriceBreakList(ListCreateAPI):
|
||||
filterset_class = SupplierPriceBreakFilter
|
||||
|
||||
def get_serializer(self, *args, **kwargs):
|
||||
"""Return serializer instance for this endpoint"""
|
||||
"""Return serializer instance for this endpoint."""
|
||||
try:
|
||||
params = self.request.query_params
|
||||
|
||||
|
@ -1,10 +1,10 @@
|
||||
"""Config for the 'company' app"""
|
||||
"""Config for the 'company' app."""
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class CompanyConfig(AppConfig):
|
||||
"""Config class for the 'company' app"""
|
||||
"""Config class for the 'company' app."""
|
||||
|
||||
name = 'company'
|
||||
|
||||
|
@ -90,7 +90,7 @@ class Company(InvenTreeNotesMixin, MetadataMixin, models.Model):
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass defines extra model options"""
|
||||
"""Metaclass defines extra model options."""
|
||||
|
||||
ordering = ['name']
|
||||
constraints = [
|
||||
@ -100,7 +100,7 @@ class Company(InvenTreeNotesMixin, MetadataMixin, models.Model):
|
||||
|
||||
@staticmethod
|
||||
def get_api_url():
|
||||
"""Return the API URL associated with the Company model"""
|
||||
"""Return the API URL associated with the Company model."""
|
||||
return reverse('api-company-list')
|
||||
|
||||
name = models.CharField(
|
||||
@ -186,7 +186,7 @@ class Company(InvenTreeNotesMixin, MetadataMixin, models.Model):
|
||||
|
||||
@property
|
||||
def address(self):
|
||||
"""Return the string representation for the primary address
|
||||
"""Return the string representation for the primary address.
|
||||
|
||||
This property exists for backwards compatibility
|
||||
"""
|
||||
@ -196,7 +196,7 @@ class Company(InvenTreeNotesMixin, MetadataMixin, models.Model):
|
||||
|
||||
@property
|
||||
def primary_address(self):
|
||||
"""Returns address object of primary address. Parsed by serializer"""
|
||||
"""Returns address object of primary address. Parsed by serializer."""
|
||||
return Address.objects.filter(company=self.id).filter(primary=True).first()
|
||||
|
||||
@property
|
||||
@ -251,15 +251,15 @@ class Company(InvenTreeNotesMixin, MetadataMixin, models.Model):
|
||||
|
||||
|
||||
class CompanyAttachment(InvenTreeAttachment):
|
||||
"""Model for storing file or URL attachments against a Company object"""
|
||||
"""Model for storing file or URL attachments against a Company object."""
|
||||
|
||||
@staticmethod
|
||||
def get_api_url():
|
||||
"""Return the API URL associated with this model"""
|
||||
"""Return the API URL associated with this model."""
|
||||
return reverse('api-company-attachment-list')
|
||||
|
||||
def getSubdir(self):
|
||||
"""Return the subdirectory where these attachments are uploaded"""
|
||||
"""Return the subdirectory where these attachments are uploaded."""
|
||||
return os.path.join('company_files', str(self.company.pk))
|
||||
|
||||
company = models.ForeignKey(
|
||||
@ -283,7 +283,7 @@ class Contact(MetadataMixin, models.Model):
|
||||
|
||||
@staticmethod
|
||||
def get_api_url():
|
||||
"""Return the API URL associated with the Contcat model"""
|
||||
"""Return the API URL associated with the Contcat model."""
|
||||
return reverse('api-contact-list')
|
||||
|
||||
company = models.ForeignKey(
|
||||
@ -300,7 +300,7 @@ class Contact(MetadataMixin, models.Model):
|
||||
|
||||
|
||||
class Address(models.Model):
|
||||
"""An address represents a physical location where the company is located. It is possible for a company to have multiple locations
|
||||
"""An address represents a physical location where the company is located. It is possible for a company to have multiple locations.
|
||||
|
||||
Attributes:
|
||||
company: Company link for this address
|
||||
@ -316,16 +316,16 @@ class Address(models.Model):
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass defines extra model options"""
|
||||
"""Metaclass defines extra model options."""
|
||||
|
||||
verbose_name_plural = 'Addresses'
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Custom init function"""
|
||||
"""Custom init function."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def __str__(self):
|
||||
"""Defines string representation of address to supple a one-line to API calls"""
|
||||
"""Defines string representation of address to supple a one-line to API calls."""
|
||||
available_lines = [
|
||||
self.line1,
|
||||
self.line2,
|
||||
@ -343,8 +343,9 @@ class Address(models.Model):
|
||||
return ', '.join(populated_lines)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
"""Run checks when saving an address:
|
||||
"""Run checks when saving an address.
|
||||
|
||||
Rules:
|
||||
- If this address is marked as "primary", ensure that all other addresses for this company are marked as non-primary
|
||||
"""
|
||||
others = list(
|
||||
@ -366,7 +367,7 @@ class Address(models.Model):
|
||||
|
||||
@staticmethod
|
||||
def get_api_url():
|
||||
"""Return the API URL associated with the Contcat model"""
|
||||
"""Return the API URL associated with the Contcat model."""
|
||||
return reverse('api-address-list')
|
||||
|
||||
company = models.ForeignKey(
|
||||
@ -465,13 +466,13 @@ class ManufacturerPart(MetadataMixin, InvenTreeBarcodeMixin, models.Model):
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass defines extra model options"""
|
||||
"""Metaclass defines extra model options."""
|
||||
|
||||
unique_together = ('part', 'manufacturer', 'MPN')
|
||||
|
||||
@staticmethod
|
||||
def get_api_url():
|
||||
"""Return the API URL associated with the ManufacturerPart instance"""
|
||||
"""Return the API URL associated with the ManufacturerPart instance."""
|
||||
return reverse('api-manufacturer-part-list')
|
||||
|
||||
part = models.ForeignKey(
|
||||
@ -542,7 +543,7 @@ class ManufacturerPart(MetadataMixin, InvenTreeBarcodeMixin, models.Model):
|
||||
return manufacturer_part
|
||||
|
||||
def __str__(self):
|
||||
"""Format a string representation of a ManufacturerPart"""
|
||||
"""Format a string representation of a ManufacturerPart."""
|
||||
s = ''
|
||||
|
||||
if self.manufacturer:
|
||||
@ -559,11 +560,11 @@ class ManufacturerPartAttachment(InvenTreeAttachment):
|
||||
|
||||
@staticmethod
|
||||
def get_api_url():
|
||||
"""Return the API URL associated with the ManufacturerPartAttachment model"""
|
||||
"""Return the API URL associated with the ManufacturerPartAttachment model."""
|
||||
return reverse('api-manufacturer-part-attachment-list')
|
||||
|
||||
def getSubdir(self):
|
||||
"""Return the subdirectory where attachment files for the ManufacturerPart model are located"""
|
||||
"""Return the subdirectory where attachment files for the ManufacturerPart model are located."""
|
||||
return os.path.join('manufacturer_part_files', str(self.manufacturer_part.id))
|
||||
|
||||
manufacturer_part = models.ForeignKey(
|
||||
@ -583,13 +584,13 @@ class ManufacturerPartParameter(models.Model):
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass defines extra model options"""
|
||||
"""Metaclass defines extra model options."""
|
||||
|
||||
unique_together = ('manufacturer_part', 'name')
|
||||
|
||||
@staticmethod
|
||||
def get_api_url():
|
||||
"""Return the API URL associated with the ManufacturerPartParameter model"""
|
||||
"""Return the API URL associated with the ManufacturerPartParameter model."""
|
||||
return reverse('api-manufacturer-part-parameter-list')
|
||||
|
||||
manufacturer_part = models.ForeignKey(
|
||||
@ -630,7 +631,7 @@ class SupplierPartManager(models.Manager):
|
||||
"""
|
||||
|
||||
def get_queryset(self):
|
||||
"""Prefetch related fields when querying against the SupplierPart model"""
|
||||
"""Prefetch related fields when querying against the SupplierPart model."""
|
||||
# Always prefetch related models
|
||||
return (
|
||||
super()
|
||||
@ -660,7 +661,7 @@ class SupplierPart(MetadataMixin, InvenTreeBarcodeMixin, common.models.MetaMixin
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass defines extra model options"""
|
||||
"""Metaclass defines extra model options."""
|
||||
|
||||
unique_together = ('part', 'supplier', 'SKU')
|
||||
|
||||
@ -673,20 +674,21 @@ class SupplierPart(MetadataMixin, InvenTreeBarcodeMixin, common.models.MetaMixin
|
||||
|
||||
@staticmethod
|
||||
def get_api_url():
|
||||
"""Return the API URL associated with the SupplierPart model"""
|
||||
"""Return the API URL associated with the SupplierPart model."""
|
||||
return reverse('api-supplier-part-list')
|
||||
|
||||
def get_absolute_url(self):
|
||||
"""Return the web URL of the detail view for this SupplierPart"""
|
||||
"""Return the web URL of the detail view for this SupplierPart."""
|
||||
return reverse('supplier-part-detail', kwargs={'pk': self.id})
|
||||
|
||||
def api_instance_filters(self):
|
||||
"""Return custom API filters for this particular instance"""
|
||||
"""Return custom API filters for this particular instance."""
|
||||
return {'manufacturer_part': {'part': self.part.pk}}
|
||||
|
||||
def clean(self):
|
||||
"""Custom clean action for the SupplierPart model:
|
||||
"""Custom clean action for the SupplierPart model.
|
||||
|
||||
Rules:
|
||||
- Ensure that manufacturer_part.part and part are the same!
|
||||
"""
|
||||
super().clean()
|
||||
@ -888,7 +890,7 @@ class SupplierPart(MetadataMixin, InvenTreeBarcodeMixin, common.models.MetaMixin
|
||||
)
|
||||
|
||||
def update_available_quantity(self, quantity):
|
||||
"""Update the available quantity for this SupplierPart"""
|
||||
"""Update the available quantity for this SupplierPart."""
|
||||
self.available = quantity
|
||||
self.availability_updated = datetime.now()
|
||||
self.save()
|
||||
@ -911,7 +913,7 @@ class SupplierPart(MetadataMixin, InvenTreeBarcodeMixin, common.models.MetaMixin
|
||||
|
||||
@property
|
||||
def has_price_breaks(self):
|
||||
"""Return True if this SupplierPart has associated price breaks"""
|
||||
"""Return True if this SupplierPart has associated price breaks."""
|
||||
return self.price_breaks.count() > 0
|
||||
|
||||
@property
|
||||
@ -921,7 +923,7 @@ class SupplierPart(MetadataMixin, InvenTreeBarcodeMixin, common.models.MetaMixin
|
||||
|
||||
@property
|
||||
def unit_pricing(self):
|
||||
"""Return the single-quantity pricing for this SupplierPart"""
|
||||
"""Return the single-quantity pricing for this SupplierPart."""
|
||||
return self.get_price(1)
|
||||
|
||||
def add_price_break(self, quantity, price) -> None:
|
||||
@ -971,11 +973,11 @@ class SupplierPart(MetadataMixin, InvenTreeBarcodeMixin, common.models.MetaMixin
|
||||
|
||||
@property
|
||||
def pretty_name(self):
|
||||
"""Format a 'pretty' name for this SupplierPart"""
|
||||
"""Format a 'pretty' name for this SupplierPart."""
|
||||
return str(self)
|
||||
|
||||
def __str__(self):
|
||||
"""Format a string representation of a SupplierPart"""
|
||||
"""Format a string representation of a SupplierPart."""
|
||||
s = ''
|
||||
|
||||
if self.part.IPN:
|
||||
@ -1005,7 +1007,7 @@ class SupplierPriceBreak(common.models.PriceBreak):
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass defines extra model options"""
|
||||
"""Metaclass defines extra model options."""
|
||||
|
||||
unique_together = ('part', 'quantity')
|
||||
|
||||
@ -1013,12 +1015,12 @@ class SupplierPriceBreak(common.models.PriceBreak):
|
||||
db_table = 'part_supplierpricebreak'
|
||||
|
||||
def __str__(self):
|
||||
"""Format a string representation of a SupplierPriceBreak instance"""
|
||||
"""Format a string representation of a SupplierPriceBreak instance."""
|
||||
return f'{self.part.SKU} - {self.price} @ {self.quantity}'
|
||||
|
||||
@staticmethod
|
||||
def get_api_url():
|
||||
"""Return the API URL associated with the SupplierPriceBreak model"""
|
||||
"""Return the API URL associated with the SupplierPriceBreak model."""
|
||||
return reverse('api-part-supplier-price-list')
|
||||
|
||||
part = models.ForeignKey(
|
||||
@ -1033,7 +1035,7 @@ class SupplierPriceBreak(common.models.PriceBreak):
|
||||
post_save, sender=SupplierPriceBreak, dispatch_uid='post_save_supplier_price_break'
|
||||
)
|
||||
def after_save_supplier_price(sender, instance, created, **kwargs):
|
||||
"""Callback function when a SupplierPriceBreak is created or updated"""
|
||||
"""Callback function when a SupplierPriceBreak is created or updated."""
|
||||
if InvenTree.ready.canAppAccessDatabase() and not InvenTree.ready.isImportingData():
|
||||
if instance.part and instance.part.part:
|
||||
instance.part.part.schedule_pricing_update(create=True)
|
||||
@ -1045,7 +1047,7 @@ def after_save_supplier_price(sender, instance, created, **kwargs):
|
||||
dispatch_uid='post_delete_supplier_price_break',
|
||||
)
|
||||
def after_delete_supplier_price(sender, instance, **kwargs):
|
||||
"""Callback function when a SupplierPriceBreak is deleted"""
|
||||
"""Callback function when a SupplierPriceBreak is deleted."""
|
||||
if InvenTree.ready.canAppAccessDatabase() and not InvenTree.ready.isImportingData():
|
||||
if instance.part and instance.part.part:
|
||||
instance.part.part.schedule_pricing_update(create=False)
|
||||
|
@ -36,7 +36,7 @@ from .models import (
|
||||
|
||||
|
||||
class CompanyBriefSerializer(InvenTreeModelSerializer):
|
||||
"""Serializer for Company object (limited detail)"""
|
||||
"""Serializer for Company object (limited detail)."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options."""
|
||||
@ -50,10 +50,10 @@ class CompanyBriefSerializer(InvenTreeModelSerializer):
|
||||
|
||||
|
||||
class AddressSerializer(InvenTreeModelSerializer):
|
||||
"""Serializer for the Address Model"""
|
||||
"""Serializer for the Address Model."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options"""
|
||||
"""Metaclass options."""
|
||||
|
||||
model = Address
|
||||
fields = [
|
||||
@ -74,10 +74,10 @@ class AddressSerializer(InvenTreeModelSerializer):
|
||||
|
||||
|
||||
class AddressBriefSerializer(InvenTreeModelSerializer):
|
||||
"""Serializer for Address Model (limited)"""
|
||||
"""Serializer for Address Model (limited)."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options"""
|
||||
"""Metaclass options."""
|
||||
|
||||
model = Address
|
||||
fields = [
|
||||
@ -94,7 +94,7 @@ class AddressBriefSerializer(InvenTreeModelSerializer):
|
||||
|
||||
|
||||
class CompanySerializer(RemoteImageMixin, InvenTreeModelSerializer):
|
||||
"""Serializer for Company object (full detail)"""
|
||||
"""Serializer for Company object (full detail)."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options."""
|
||||
@ -127,7 +127,7 @@ class CompanySerializer(RemoteImageMixin, InvenTreeModelSerializer):
|
||||
|
||||
@staticmethod
|
||||
def annotate_queryset(queryset):
|
||||
"""Annotate the supplied queryset with aggregated information"""
|
||||
"""Annotate the supplied queryset with aggregated information."""
|
||||
# Add count of parts manufactured
|
||||
queryset = queryset.annotate(
|
||||
parts_manufactured=SubqueryCount('manufactured_parts')
|
||||
@ -154,7 +154,7 @@ class CompanySerializer(RemoteImageMixin, InvenTreeModelSerializer):
|
||||
)
|
||||
|
||||
def save(self):
|
||||
"""Save the Company instance"""
|
||||
"""Save the Company instance."""
|
||||
super().save()
|
||||
|
||||
company = self.instance
|
||||
@ -176,10 +176,10 @@ class CompanySerializer(RemoteImageMixin, InvenTreeModelSerializer):
|
||||
|
||||
|
||||
class CompanyAttachmentSerializer(InvenTreeAttachmentSerializer):
|
||||
"""Serializer for the CompanyAttachment class"""
|
||||
"""Serializer for the CompanyAttachment class."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass defines serializer options"""
|
||||
"""Metaclass defines serializer options."""
|
||||
|
||||
model = CompanyAttachment
|
||||
|
||||
@ -187,10 +187,10 @@ class CompanyAttachmentSerializer(InvenTreeAttachmentSerializer):
|
||||
|
||||
|
||||
class ContactSerializer(InvenTreeModelSerializer):
|
||||
"""Serializer class for the Contact model"""
|
||||
"""Serializer class for the Contact model."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options"""
|
||||
"""Metaclass options."""
|
||||
|
||||
model = Contact
|
||||
fields = ['pk', 'company', 'name', 'phone', 'email', 'role']
|
||||
@ -220,7 +220,7 @@ class ManufacturerPartSerializer(InvenTreeTagModelSerializer):
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize this serializer with extra detail fields as required"""
|
||||
"""Initialize this serializer with extra detail fields as required."""
|
||||
part_detail = kwargs.pop('part_detail', True)
|
||||
manufacturer_detail = kwargs.pop('manufacturer_detail', True)
|
||||
prettify = kwargs.pop('pretty', False)
|
||||
@ -278,7 +278,7 @@ class ManufacturerPartParameterSerializer(InvenTreeModelSerializer):
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize this serializer with extra detail fields as required"""
|
||||
"""Initialize this serializer with extra detail fields as required."""
|
||||
man_detail = kwargs.pop('manufacturer_part_detail', False)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
@ -331,7 +331,7 @@ class SupplierPartSerializer(InvenTreeTagModelSerializer):
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize this serializer with extra detail fields as required"""
|
||||
"""Initialize this serializer with extra detail fields as required."""
|
||||
# Check if 'available' quantity was supplied
|
||||
self.has_available_quantity = 'available' in kwargs.get('data', {})
|
||||
|
||||
@ -395,7 +395,7 @@ class SupplierPartSerializer(InvenTreeTagModelSerializer):
|
||||
|
||||
@staticmethod
|
||||
def annotate_queryset(queryset):
|
||||
"""Annotate the SupplierPart queryset with extra fields:
|
||||
"""Annotate the SupplierPart queryset with extra fields.
|
||||
|
||||
Fields:
|
||||
in_stock: Current stock quantity for each SupplierPart
|
||||
@ -405,7 +405,7 @@ class SupplierPartSerializer(InvenTreeTagModelSerializer):
|
||||
return queryset
|
||||
|
||||
def update(self, supplier_part, data):
|
||||
"""Custom update functionality for the serializer"""
|
||||
"""Custom update functionality for the serializer."""
|
||||
available = data.pop('available', None)
|
||||
|
||||
response = super().update(supplier_part, data)
|
||||
@ -457,7 +457,7 @@ class SupplierPriceBreakSerializer(InvenTreeModelSerializer):
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize this serializer with extra fields as required"""
|
||||
"""Initialize this serializer with extra fields as required."""
|
||||
supplier_detail = kwargs.pop('supplier_detail', False)
|
||||
part_detail = kwargs.pop('part_detail', False)
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
"""Unit testing for the company app API functions"""
|
||||
"""Unit testing for the company app API functions."""
|
||||
|
||||
from django.urls import reverse
|
||||
|
||||
@ -16,7 +16,7 @@ class CompanyTest(InvenTreeAPITestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
"""Perform initialization for the unit test class"""
|
||||
"""Perform initialization for the unit test class."""
|
||||
super().setUpTestData()
|
||||
|
||||
# Create some company objects to work with
|
||||
@ -34,7 +34,7 @@ class CompanyTest(InvenTreeAPITestCase):
|
||||
)
|
||||
|
||||
def test_company_list(self):
|
||||
"""Test the list API endpoint for the Company model"""
|
||||
"""Test the list API endpoint for the Company model."""
|
||||
url = reverse('api-company-list')
|
||||
|
||||
# There should be three companies
|
||||
@ -133,13 +133,13 @@ class CompanyTest(InvenTreeAPITestCase):
|
||||
|
||||
|
||||
class ContactTest(InvenTreeAPITestCase):
|
||||
"""Tests for the Contact models"""
|
||||
"""Tests for the Contact models."""
|
||||
|
||||
roles = []
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
"""Perform init for this test class"""
|
||||
"""Perform init for this test class."""
|
||||
super().setUpTestData()
|
||||
|
||||
# Create some companies
|
||||
@ -163,7 +163,7 @@ class ContactTest(InvenTreeAPITestCase):
|
||||
cls.url = reverse('api-contact-list')
|
||||
|
||||
def test_list(self):
|
||||
"""Test company list API endpoint"""
|
||||
"""Test company list API endpoint."""
|
||||
# List all results
|
||||
response = self.get(self.url, {}, expected_code=200)
|
||||
|
||||
@ -180,7 +180,7 @@ class ContactTest(InvenTreeAPITestCase):
|
||||
self.assertEqual(len(response.data), 3)
|
||||
|
||||
def test_create(self):
|
||||
"""Test that we can create a new Contact object via the API"""
|
||||
"""Test that we can create a new Contact object via the API."""
|
||||
n = Contact.objects.count()
|
||||
|
||||
company = Company.objects.first()
|
||||
@ -199,7 +199,7 @@ class ContactTest(InvenTreeAPITestCase):
|
||||
self.assertEqual(Contact.objects.count(), n + 1)
|
||||
|
||||
def test_edit(self):
|
||||
"""Test that we can edit a Contact via the API"""
|
||||
"""Test that we can edit a Contact via the API."""
|
||||
# Get the first contact
|
||||
contact = Contact.objects.first()
|
||||
# Use this contact in the tests
|
||||
@ -222,7 +222,7 @@ class ContactTest(InvenTreeAPITestCase):
|
||||
self.assertEqual(contact.role, 'x')
|
||||
|
||||
def test_delete(self):
|
||||
"""Tests that we can delete a Contact via the API"""
|
||||
"""Tests that we can delete a Contact via the API."""
|
||||
# Get the last contact
|
||||
contact = Contact.objects.first()
|
||||
url = reverse('api-contact-detail', kwargs={'pk': contact.pk})
|
||||
@ -239,13 +239,13 @@ class ContactTest(InvenTreeAPITestCase):
|
||||
|
||||
|
||||
class AddressTest(InvenTreeAPITestCase):
|
||||
"""Test cases for Address API endpoints"""
|
||||
"""Test cases for Address API endpoints."""
|
||||
|
||||
roles = []
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
"""Perform initialization for this test class"""
|
||||
"""Perform initialization for this test class."""
|
||||
super().setUpTestData()
|
||||
cls.num_companies = 3
|
||||
cls.num_addr = 3
|
||||
@ -271,13 +271,13 @@ class AddressTest(InvenTreeAPITestCase):
|
||||
Address.objects.bulk_create(addresses)
|
||||
|
||||
def test_list(self):
|
||||
"""Test listing all addresses without filtering"""
|
||||
"""Test listing all addresses without filtering."""
|
||||
response = self.get(self.url, expected_code=200)
|
||||
|
||||
self.assertEqual(len(response.data), self.num_companies * self.num_addr)
|
||||
|
||||
def test_filter_list(self):
|
||||
"""Test listing addresses filtered on company"""
|
||||
"""Test listing addresses filtered on company."""
|
||||
company = Company.objects.first()
|
||||
|
||||
response = self.get(self.url, {'company': company.pk}, expected_code=200)
|
||||
@ -285,7 +285,7 @@ class AddressTest(InvenTreeAPITestCase):
|
||||
self.assertEqual(len(response.data), self.num_addr)
|
||||
|
||||
def test_create(self):
|
||||
"""Test creating a new address"""
|
||||
"""Test creating a new address."""
|
||||
company = Company.objects.first()
|
||||
|
||||
self.post(self.url, {'company': company.pk, 'title': 'HQ'}, expected_code=403)
|
||||
@ -295,7 +295,7 @@ class AddressTest(InvenTreeAPITestCase):
|
||||
self.post(self.url, {'company': company.pk, 'title': 'HQ'}, expected_code=201)
|
||||
|
||||
def test_get(self):
|
||||
"""Test that objects are properly returned from a get"""
|
||||
"""Test that objects are properly returned from a get."""
|
||||
addr = Address.objects.first()
|
||||
|
||||
url = reverse('api-address-detail', kwargs={'pk': addr.pk})
|
||||
@ -315,7 +315,7 @@ class AddressTest(InvenTreeAPITestCase):
|
||||
self.assertIn(key, response.data)
|
||||
|
||||
def test_edit(self):
|
||||
"""Test editing an object"""
|
||||
"""Test editing an object."""
|
||||
addr = Address.objects.first()
|
||||
|
||||
url = reverse('api-address-detail', kwargs={'pk': addr.pk})
|
||||
@ -331,7 +331,7 @@ class AddressTest(InvenTreeAPITestCase):
|
||||
self.assertEqual(data['title'], 'World')
|
||||
|
||||
def test_delete(self):
|
||||
"""Test deleting an object"""
|
||||
"""Test deleting an object."""
|
||||
addr = Address.objects.first()
|
||||
|
||||
url = reverse('api-address-detail', kwargs={'pk': addr.pk})
|
||||
@ -360,7 +360,7 @@ class ManufacturerTest(InvenTreeAPITestCase):
|
||||
roles = ['part.add', 'part.change']
|
||||
|
||||
def test_manufacturer_part_list(self):
|
||||
"""Test the ManufacturerPart API list functionality"""
|
||||
"""Test the ManufacturerPart API list functionality."""
|
||||
url = reverse('api-manufacturer-part-list')
|
||||
|
||||
# There should be three manufacturer parts
|
||||
@ -399,14 +399,14 @@ class ManufacturerTest(InvenTreeAPITestCase):
|
||||
self.assertEqual(response.data['MPN'], 'MPN-TEST-123')
|
||||
|
||||
def test_manufacturer_part_search(self):
|
||||
"""Test search functionality in manufacturer list"""
|
||||
"""Test search functionality in manufacturer list."""
|
||||
url = reverse('api-manufacturer-part-list')
|
||||
data = {'search': 'MPN'}
|
||||
response = self.get(url, data)
|
||||
self.assertEqual(len(response.data), 3)
|
||||
|
||||
def test_supplier_part_create(self):
|
||||
"""Test a SupplierPart can be created via the API"""
|
||||
"""Test a SupplierPart can be created via the API."""
|
||||
url = reverse('api-supplier-part-list')
|
||||
|
||||
# Create a manufacturer part
|
||||
@ -449,7 +449,7 @@ class ManufacturerTest(InvenTreeAPITestCase):
|
||||
|
||||
|
||||
class SupplierPartTest(InvenTreeAPITestCase):
|
||||
"""Unit tests for the SupplierPart API endpoints"""
|
||||
"""Unit tests for the SupplierPart API endpoints."""
|
||||
|
||||
fixtures = [
|
||||
'category',
|
||||
@ -463,7 +463,7 @@ class SupplierPartTest(InvenTreeAPITestCase):
|
||||
roles = ['part.add', 'part.change', 'part.add', 'purchase_order.change']
|
||||
|
||||
def test_supplier_part_list(self):
|
||||
"""Test the SupplierPart API list functionality"""
|
||||
"""Test the SupplierPart API list functionality."""
|
||||
url = reverse('api-supplier-part-list')
|
||||
|
||||
# Return *all* SupplierParts
|
||||
@ -484,7 +484,7 @@ class SupplierPartTest(InvenTreeAPITestCase):
|
||||
self.assertEqual(len(response.data), n)
|
||||
|
||||
def test_available(self):
|
||||
"""Tests for updating the 'available' field"""
|
||||
"""Tests for updating the 'available' field."""
|
||||
url = reverse('api-supplier-part-list')
|
||||
|
||||
# Should fail when sending an invalid 'available' field
|
||||
@ -545,7 +545,7 @@ class CompanyMetadataAPITest(InvenTreeAPITestCase):
|
||||
roles = ['company.change', 'purchase_order.change', 'part.change']
|
||||
|
||||
def metatester(self, apikey, model):
|
||||
"""Generic tester"""
|
||||
"""Generic tester."""
|
||||
modeldata = model.objects.first()
|
||||
|
||||
# Useless test unless a model object is found
|
||||
@ -571,7 +571,7 @@ class CompanyMetadataAPITest(InvenTreeAPITestCase):
|
||||
)
|
||||
|
||||
def test_metadata(self):
|
||||
"""Test all endpoints"""
|
||||
"""Test all endpoints."""
|
||||
for apikey, model in {
|
||||
'api-manufacturer-part-metadata': ManufacturerPart,
|
||||
'api-supplier-part-metadata': SupplierPart,
|
||||
|
@ -6,7 +6,7 @@ from InvenTree import unit_test
|
||||
|
||||
|
||||
class TestForwardMigrations(MigratorTestCase):
|
||||
"""Unit testing class for testing 'company' app migrations"""
|
||||
"""Unit testing class for testing 'company' app migrations."""
|
||||
|
||||
migrate_from = ('company', unit_test.getOldestMigrationFile('company'))
|
||||
migrate_to = ('company', unit_test.getNewestMigrationFile('company'))
|
||||
@ -20,7 +20,7 @@ class TestForwardMigrations(MigratorTestCase):
|
||||
)
|
||||
|
||||
def test_migrations(self):
|
||||
"""Test the database state after applying all migrations"""
|
||||
"""Test the database state after applying all migrations."""
|
||||
Company = self.new_state.apps.get_model('company', 'company')
|
||||
|
||||
self.assertEqual(Company.objects.count(), 1)
|
||||
@ -33,8 +33,9 @@ class TestManufacturerField(MigratorTestCase):
|
||||
migrate_to = ('company', '0019_auto_20200413_0642')
|
||||
|
||||
def prepare(self):
|
||||
"""Prepare the database by adding some test data 'before' the change:
|
||||
"""Prepare the database by adding some test data 'before' the change.
|
||||
|
||||
Changes:
|
||||
- Part object
|
||||
- Company object (supplier)
|
||||
- SupplierPart object
|
||||
@ -99,8 +100,9 @@ class TestManufacturerPart(MigratorTestCase):
|
||||
migrate_to = ('company', '0037_supplierpart_update_3')
|
||||
|
||||
def prepare(self):
|
||||
"""Prepare the database by adding some test data 'before' the change:
|
||||
"""Prepare the database by adding some test data 'before' the change.
|
||||
|
||||
Changes:
|
||||
- Part object
|
||||
- Company object (supplier)
|
||||
- SupplierPart object
|
||||
@ -217,8 +219,9 @@ class TestCurrencyMigration(MigratorTestCase):
|
||||
migrate_to = ('company', '0026_auto_20201110_1011')
|
||||
|
||||
def prepare(self):
|
||||
"""Prepare some data:
|
||||
"""Prepare some data.
|
||||
|
||||
Changes:
|
||||
- A part to buy
|
||||
- A supplier to buy from
|
||||
- A supplier part
|
||||
@ -269,7 +272,7 @@ class TestCurrencyMigration(MigratorTestCase):
|
||||
self.assertIsNone(pb.price)
|
||||
|
||||
def test_currency_migration(self):
|
||||
"""Test database state after applying migrations"""
|
||||
"""Test database state after applying migrations."""
|
||||
PB = self.new_state.apps.get_model('company', 'supplierpricebreak')
|
||||
|
||||
for pb in PB.objects.all():
|
||||
@ -278,7 +281,7 @@ class TestCurrencyMigration(MigratorTestCase):
|
||||
|
||||
|
||||
class TestAddressMigration(MigratorTestCase):
|
||||
"""Test moving address data into Address model"""
|
||||
"""Test moving address data into Address model."""
|
||||
|
||||
migrate_from = ('company', '0063_auto_20230502_1956')
|
||||
migrate_to = ('company', '0064_move_address_field_to_address_model')
|
||||
@ -289,14 +292,14 @@ class TestAddressMigration(MigratorTestCase):
|
||||
l2 = 'splitting functionality'
|
||||
|
||||
def prepare(self):
|
||||
"""Set up some companies with addresses"""
|
||||
"""Set up some companies with addresses."""
|
||||
Company = self.old_state.apps.get_model('company', 'company')
|
||||
|
||||
Company.objects.create(name='Company 1', address=self.short_l1)
|
||||
Company.objects.create(name='Company 2', address=self.long_l1 + self.l2)
|
||||
|
||||
def test_address_migration(self):
|
||||
"""Test database state after applying the migration"""
|
||||
"""Test database state after applying the migration."""
|
||||
Address = self.new_state.apps.get_model('company', 'address')
|
||||
Company = self.new_state.apps.get_model('company', 'company')
|
||||
|
||||
@ -323,7 +326,7 @@ class TestSupplierPartQuantity(MigratorTestCase):
|
||||
migrate_to = ('company', unit_test.getNewestMigrationFile('company'))
|
||||
|
||||
def prepare(self):
|
||||
"""Prepare a number of SupplierPart objects"""
|
||||
"""Prepare a number of SupplierPart objects."""
|
||||
Part = self.old_state.apps.get_model('part', 'part')
|
||||
Company = self.old_state.apps.get_model('company', 'company')
|
||||
SupplierPart = self.old_state.apps.get_model('company', 'supplierpart')
|
||||
|
@ -1,4 +1,4 @@
|
||||
"""Unit tests specific to the SupplierPart model"""
|
||||
"""Unit tests specific to the SupplierPart model."""
|
||||
|
||||
from decimal import Decimal
|
||||
|
||||
@ -10,10 +10,10 @@ from part.models import Part
|
||||
|
||||
|
||||
class SupplierPartPackUnitsTests(InvenTreeTestCase):
|
||||
"""Unit tests for the SupplierPart pack_quantity field"""
|
||||
"""Unit tests for the SupplierPart pack_quantity field."""
|
||||
|
||||
def test_pack_quantity_dimensionless(self):
|
||||
"""Test valid values for the 'pack_quantity' field"""
|
||||
"""Test valid values for the 'pack_quantity' field."""
|
||||
# Create a part without units (dimensionless)
|
||||
part = Part.objects.create(
|
||||
name='Test Part', description='Test part description', component=True
|
||||
@ -48,7 +48,7 @@ class SupplierPartPackUnitsTests(InvenTreeTestCase):
|
||||
sp.full_clean()
|
||||
|
||||
def test_pack_quantity(self):
|
||||
"""Test pack_quantity for a part with a specified dimension"""
|
||||
"""Test pack_quantity for a part with a specified dimension."""
|
||||
# Create a part with units 'm'
|
||||
part = Part.objects.create(
|
||||
name='Test Part',
|
||||
|
@ -1,4 +1,4 @@
|
||||
"""Unit tests for Company views (see views.py)"""
|
||||
"""Unit tests for Company views (see views.py)."""
|
||||
|
||||
from django.urls import reverse
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
"""Unit tests for the models in the 'company' app"""
|
||||
"""Unit tests for the models in the 'company' app."""
|
||||
|
||||
import os
|
||||
from decimal import Decimal
|
||||
@ -19,7 +19,7 @@ from .models import (
|
||||
|
||||
|
||||
class CompanySimpleTest(TestCase):
|
||||
"""Unit tests for the Company model"""
|
||||
"""Unit tests for the Company model."""
|
||||
|
||||
fixtures = [
|
||||
'company',
|
||||
@ -34,7 +34,7 @@ class CompanySimpleTest(TestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
"""Perform initialization for the tests in this class"""
|
||||
"""Perform initialization for the tests in this class."""
|
||||
super().setUpTestData()
|
||||
|
||||
Company.objects.create(
|
||||
@ -51,18 +51,18 @@ class CompanySimpleTest(TestCase):
|
||||
cls.zergm312 = SupplierPart.objects.get(SKU='ZERGM312')
|
||||
|
||||
def test_company_model(self):
|
||||
"""Tests for the company model data"""
|
||||
"""Tests for the company model data."""
|
||||
c = Company.objects.get(name='ABC Co.')
|
||||
self.assertEqual(c.name, 'ABC Co.')
|
||||
self.assertEqual(str(c), 'ABC Co. - Seller of ABC products')
|
||||
|
||||
def test_company_url(self):
|
||||
"""Test the detail URL for a company"""
|
||||
"""Test the detail URL for a company."""
|
||||
c = Company.objects.get(pk=1)
|
||||
self.assertEqual(c.get_absolute_url(), '/company/1/')
|
||||
|
||||
def test_image_renamer(self):
|
||||
"""Test the company image upload functionality"""
|
||||
"""Test the company image upload functionality."""
|
||||
c = Company.objects.get(pk=1)
|
||||
rn = rename_company_image(c, 'test.png')
|
||||
self.assertEqual(rn, 'company_images' + os.path.sep + 'company_1_img.png')
|
||||
@ -71,7 +71,7 @@ class CompanySimpleTest(TestCase):
|
||||
self.assertEqual(rn, 'company_images' + os.path.sep + 'company_1_img')
|
||||
|
||||
def test_price_breaks(self):
|
||||
"""Unit tests for price breaks"""
|
||||
"""Unit tests for price breaks."""
|
||||
self.assertTrue(self.acme0001.has_price_breaks)
|
||||
self.assertTrue(self.acme0002.has_price_breaks)
|
||||
self.assertTrue(self.zergm312.has_price_breaks)
|
||||
@ -100,7 +100,7 @@ class CompanySimpleTest(TestCase):
|
||||
self.assertEqual(p(55), 68.75)
|
||||
|
||||
def test_part_pricing(self):
|
||||
"""Unit tests for supplier part pricing"""
|
||||
"""Unit tests for supplier part pricing."""
|
||||
m2x4 = Part.objects.get(name='M2x4 LPHS')
|
||||
|
||||
self.assertEqual(m2x4.get_price_info(5.5), '38.5 - 41.25')
|
||||
@ -153,10 +153,10 @@ class CompanySimpleTest(TestCase):
|
||||
|
||||
|
||||
class ContactSimpleTest(TestCase):
|
||||
"""Unit tests for the Contact model"""
|
||||
"""Unit tests for the Contact model."""
|
||||
|
||||
def setUp(self):
|
||||
"""Initialization for the tests in this class"""
|
||||
"""Initialization for the tests in this class."""
|
||||
# Create a simple company
|
||||
self.c = Company.objects.create(
|
||||
name='Test Corp.', description='We make stuff good'
|
||||
@ -168,39 +168,39 @@ class ContactSimpleTest(TestCase):
|
||||
Contact.objects.create(name='Sally Smith', company=self.c)
|
||||
|
||||
def test_exists(self):
|
||||
"""Test that contacts exist"""
|
||||
"""Test that contacts exist."""
|
||||
self.assertEqual(Contact.objects.count(), 3)
|
||||
|
||||
def test_delete(self):
|
||||
"""Test deletion of a Contact instance"""
|
||||
"""Test deletion of a Contact instance."""
|
||||
# Remove the parent company
|
||||
Company.objects.get(pk=self.c.pk).delete()
|
||||
self.assertEqual(Contact.objects.count(), 0)
|
||||
|
||||
|
||||
class AddressTest(TestCase):
|
||||
"""Unit tests for the Address model"""
|
||||
"""Unit tests for the Address model."""
|
||||
|
||||
def setUp(self):
|
||||
"""Initialization for the tests in this class"""
|
||||
"""Initialization for the tests in this class."""
|
||||
# Create a simple company
|
||||
self.c = Company.objects.create(
|
||||
name='Test Corp.', description='We make stuff good'
|
||||
)
|
||||
|
||||
def test_create(self):
|
||||
"""Test that object creation with only company supplied is successful"""
|
||||
"""Test that object creation with only company supplied is successful."""
|
||||
Address.objects.create(company=self.c)
|
||||
self.assertEqual(Address.objects.count(), 1)
|
||||
|
||||
def test_delete(self):
|
||||
"""Test Address deletion"""
|
||||
"""Test Address deletion."""
|
||||
addr = Address.objects.create(company=self.c)
|
||||
addr.delete()
|
||||
self.assertEqual(Address.objects.count(), 0)
|
||||
|
||||
def test_primary_constraint(self):
|
||||
"""Test that there can only be one company-'primary=true' pair"""
|
||||
"""Test that there can only be one company-'primary=true' pair."""
|
||||
Address.objects.create(company=self.c, primary=True)
|
||||
Address.objects.create(company=self.c, primary=False)
|
||||
|
||||
@ -216,12 +216,12 @@ class AddressTest(TestCase):
|
||||
self.assertTrue(Address.objects.last().primary)
|
||||
|
||||
def test_first_address_is_primary(self):
|
||||
"""Test that first address related to company is always set to primary"""
|
||||
"""Test that first address related to company is always set to primary."""
|
||||
addr = Address.objects.create(company=self.c)
|
||||
self.assertTrue(addr.primary)
|
||||
|
||||
def test_model_str(self):
|
||||
"""Test value of __str__"""
|
||||
"""Test value of __str__."""
|
||||
t = 'Test address'
|
||||
l1 = 'Busy street 56'
|
||||
l2 = 'Red building'
|
||||
@ -249,12 +249,12 @@ class AddressTest(TestCase):
|
||||
|
||||
|
||||
class ManufacturerPartSimpleTest(TestCase):
|
||||
"""Unit tests for the ManufacturerPart model"""
|
||||
"""Unit tests for the ManufacturerPart model."""
|
||||
|
||||
fixtures = ['category', 'company', 'location', 'part', 'manufacturer_part']
|
||||
|
||||
def setUp(self):
|
||||
"""Initialization for the unit tests in this class"""
|
||||
"""Initialization for the unit tests in this class."""
|
||||
# Create a manufacturer part
|
||||
self.part = Part.objects.get(pk=1)
|
||||
manufacturer = Company.objects.get(pk=1)
|
||||
@ -275,7 +275,7 @@ class ManufacturerPartSimpleTest(TestCase):
|
||||
supplier_part.save()
|
||||
|
||||
def test_exists(self):
|
||||
"""That that a ManufacturerPart has been created"""
|
||||
"""That that a ManufacturerPart has been created."""
|
||||
self.assertEqual(ManufacturerPart.objects.count(), 4)
|
||||
|
||||
# Check that manufacturer part was created from supplier part creation
|
||||
@ -283,7 +283,7 @@ class ManufacturerPartSimpleTest(TestCase):
|
||||
self.assertEqual(manufacturer_parts.count(), 1)
|
||||
|
||||
def test_delete(self):
|
||||
"""Test deletion of a ManufacturerPart"""
|
||||
"""Test deletion of a ManufacturerPart."""
|
||||
Part.objects.get(pk=self.part.id).delete()
|
||||
# Check that ManufacturerPart was deleted
|
||||
self.assertEqual(ManufacturerPart.objects.count(), 3)
|
||||
|
@ -20,7 +20,7 @@ class CompanyIndex(InvenTreeRoleMixin, ListView):
|
||||
permission_required = 'company.view_company'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
"""Add extra context data to the company index page"""
|
||||
"""Add extra context data to the company index page."""
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
|
||||
# Provide custom context data to the template,
|
||||
|
@ -24,7 +24,7 @@ class StatusView(APIView):
|
||||
MODEL_REF = 'statusmodel'
|
||||
|
||||
def get_status_model(self, *args, **kwargs):
|
||||
"""Return the StatusCode model based on extra parameters passed to the view"""
|
||||
"""Return the StatusCode model based on extra parameters passed to the view."""
|
||||
status_model = self.kwargs.get(self.MODEL_REF, None)
|
||||
|
||||
if status_model is None:
|
||||
@ -35,7 +35,7 @@ class StatusView(APIView):
|
||||
return status_model
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""Perform a GET request to learn information about status codes"""
|
||||
"""Perform a GET request to learn information about status codes."""
|
||||
status_class = self.get_status_model()
|
||||
|
||||
if not inspect.isclass(status_class):
|
||||
@ -55,7 +55,7 @@ class AllStatusViews(StatusView):
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""Perform a GET request to learn information about status codes"""
|
||||
"""Perform a GET request to learn information about status codes."""
|
||||
data = {}
|
||||
|
||||
for status_class in StatusCode.__subclasses__():
|
||||
|
@ -77,7 +77,7 @@ class StatusCode(BaseEnum):
|
||||
|
||||
@classmethod
|
||||
def values(cls, key=None):
|
||||
"""Return a dict representation containing all required information"""
|
||||
"""Return a dict representation containing all required information."""
|
||||
elements = [itm for itm in cls if cls._is_element(itm.name)]
|
||||
if key is None:
|
||||
return elements
|
||||
@ -148,7 +148,7 @@ class StatusCode(BaseEnum):
|
||||
|
||||
@classmethod
|
||||
def dict(cls, key=None):
|
||||
"""Return a dict representation containing all required information"""
|
||||
"""Return a dict representation containing all required information."""
|
||||
return {
|
||||
x.name: {'color': x.color, 'key': x.value, 'label': x.label, 'name': x.name}
|
||||
for x in cls.values(key)
|
||||
|
@ -23,7 +23,6 @@ def dflt(*args, **kwargs):
|
||||
|
||||
def _clean_storage(refs):
|
||||
"""Clean the storage."""
|
||||
|
||||
for ref in refs:
|
||||
del ref
|
||||
storage.collect()
|
||||
@ -44,7 +43,6 @@ class TransitionTests(InvenTreeTestCase):
|
||||
|
||||
def test_storage(self):
|
||||
"""Ensure that the storage collection mechanism works."""
|
||||
|
||||
global raise_storage
|
||||
global raise_function
|
||||
|
||||
@ -54,7 +52,6 @@ class TransitionTests(InvenTreeTestCase):
|
||||
class RaisingImplementation(TransitionMethod):
|
||||
def transition(self, *args, **kwargs):
|
||||
"""Custom transition method."""
|
||||
|
||||
global raise_storage
|
||||
|
||||
if raise_storage:
|
||||
@ -75,7 +72,6 @@ class TransitionTests(InvenTreeTestCase):
|
||||
|
||||
def test_function(self):
|
||||
"""Ensure that a TransitionMethod's function is called."""
|
||||
|
||||
global raise_storage
|
||||
global raise_function
|
||||
|
||||
|
@ -22,7 +22,7 @@ class GeneralStatus(StatusCode):
|
||||
jkl = None # This should be ignored
|
||||
|
||||
def GHI(self): # This should be ignored
|
||||
"""A invalid function"""
|
||||
"""A invalid function."""
|
||||
pass
|
||||
|
||||
|
||||
@ -36,7 +36,7 @@ class GeneralStateTest(InvenTreeTestCase):
|
||||
self.assertEqual(GeneralStatus.COMPLETE, 30)
|
||||
|
||||
def test_code_functions(self):
|
||||
"""Test that the status code class functions work correctly"""
|
||||
"""Test that the status code class functions work correctly."""
|
||||
# render
|
||||
self.assertEqual(
|
||||
GeneralStatus.render(10),
|
||||
|
@ -76,7 +76,6 @@ class StateTransitionMixin:
|
||||
instance: Object instance
|
||||
default_action: Default action to be taken if none of the transitions returns a boolean true value
|
||||
"""
|
||||
|
||||
# Check if there is a custom override function for this transition
|
||||
for override in storage.list:
|
||||
rslt = override.transition(
|
||||
|
@ -1,4 +1,4 @@
|
||||
"""Gunicorn configuration script for InvenTree web server"""
|
||||
"""Gunicorn configuration script for InvenTree web server."""
|
||||
|
||||
import multiprocessing
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
"""Admin functionality for the 'label' app"""
|
||||
"""Admin functionality for the 'label' app."""
|
||||
|
||||
from django.contrib import admin
|
||||
|
||||
@ -6,7 +6,7 @@ import label.models
|
||||
|
||||
|
||||
class LabelAdmin(admin.ModelAdmin):
|
||||
"""Admin class for the various label models"""
|
||||
"""Admin class for the various label models."""
|
||||
|
||||
list_display = ('name', 'description', 'label', 'filters', 'enabled')
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
"""API functionality for the 'label' app"""
|
||||
"""API functionality for the 'label' app."""
|
||||
|
||||
from django.core.exceptions import FieldError, ValidationError
|
||||
from django.http import JsonResponse
|
||||
@ -42,7 +42,7 @@ class LabelFilterMixin:
|
||||
ITEM_KEY = 'item'
|
||||
|
||||
def get_items(self):
|
||||
"""Return a list of database objects from query parameter"""
|
||||
"""Return a list of database objects from query parameter."""
|
||||
ids = []
|
||||
|
||||
# Construct a list of possible query parameter value options
|
||||
@ -140,7 +140,7 @@ class LabelPrintMixin(LabelFilterMixin):
|
||||
|
||||
@method_decorator(never_cache)
|
||||
def dispatch(self, *args, **kwargs):
|
||||
"""Prevent caching when printing report templates"""
|
||||
"""Prevent caching when printing report templates."""
|
||||
return super().dispatch(*args, **kwargs)
|
||||
|
||||
def get_serializer(self, *args, **kwargs):
|
||||
@ -160,7 +160,7 @@ class LabelPrintMixin(LabelFilterMixin):
|
||||
return serializer
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""Perform a GET request against this endpoint to print labels"""
|
||||
"""Perform a GET request against this endpoint to print labels."""
|
||||
common.models.InvenTreeUserSetting.set_setting(
|
||||
'DEFAULT_' + self.ITEM_KEY.upper() + '_LABEL_TEMPLATE',
|
||||
self.get_object().pk,
|
||||
@ -170,7 +170,7 @@ class LabelPrintMixin(LabelFilterMixin):
|
||||
return self.print(request, self.get_items())
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
"""Perform a GET request against this endpoint to print labels"""
|
||||
"""Perform a GET request against this endpoint to print labels."""
|
||||
return self.get(request, *args, **kwargs)
|
||||
|
||||
def get_plugin(self, request):
|
||||
@ -245,7 +245,7 @@ class LabelPrintMixin(LabelFilterMixin):
|
||||
|
||||
|
||||
class StockItemLabelMixin:
|
||||
"""Mixin for StockItemLabel endpoints"""
|
||||
"""Mixin for StockItemLabel endpoints."""
|
||||
|
||||
queryset = label.models.StockItemLabel.objects.all()
|
||||
serializer_class = label.serializers.StockItemLabelSerializer
|
||||
@ -280,7 +280,7 @@ class StockItemLabelPrint(StockItemLabelMixin, LabelPrintMixin, RetrieveAPI):
|
||||
|
||||
|
||||
class StockLocationLabelMixin:
|
||||
"""Mixin for StockLocationLabel endpoints"""
|
||||
"""Mixin for StockLocationLabel endpoints."""
|
||||
|
||||
queryset = label.models.StockLocationLabel.objects.all()
|
||||
serializer_class = label.serializers.StockLocationLabelSerializer
|
||||
@ -315,7 +315,7 @@ class StockLocationLabelPrint(StockLocationLabelMixin, LabelPrintMixin, Retrieve
|
||||
|
||||
|
||||
class PartLabelMixin:
|
||||
"""Mixin for PartLabel endpoints"""
|
||||
"""Mixin for PartLabel endpoints."""
|
||||
|
||||
queryset = label.models.PartLabel.objects.all()
|
||||
serializer_class = label.serializers.PartLabelSerializer
|
||||
@ -343,7 +343,7 @@ class PartLabelPrint(PartLabelMixin, LabelPrintMixin, RetrieveAPI):
|
||||
|
||||
|
||||
class BuildLineLabelMixin:
|
||||
"""Mixin class for BuildLineLabel endpoints"""
|
||||
"""Mixin class for BuildLineLabel endpoints."""
|
||||
|
||||
queryset = label.models.BuildLineLabel.objects.all()
|
||||
serializer_class = label.serializers.BuildLineLabelSerializer
|
||||
@ -353,19 +353,19 @@ class BuildLineLabelMixin:
|
||||
|
||||
|
||||
class BuildLineLabelList(BuildLineLabelMixin, LabelListView):
|
||||
"""API endpoint for viewing a list of BuildLineLabel objects"""
|
||||
"""API endpoint for viewing a list of BuildLineLabel objects."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class BuildLineLabelDetail(BuildLineLabelMixin, RetrieveUpdateDestroyAPI):
|
||||
"""API endpoint for a single BuildLineLabel object"""
|
||||
"""API endpoint for a single BuildLineLabel object."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class BuildLineLabelPrint(BuildLineLabelMixin, LabelPrintMixin, RetrieveAPI):
|
||||
"""API endpoint for printing a BuildLineLabel object"""
|
||||
"""API endpoint for printing a BuildLineLabel object."""
|
||||
|
||||
pass
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
"""label app specification"""
|
||||
"""label app specification."""
|
||||
|
||||
import hashlib
|
||||
import logging
|
||||
@ -29,7 +29,7 @@ def hashFile(filename):
|
||||
|
||||
|
||||
class LabelConfig(AppConfig):
|
||||
"""App configuration class for the 'label' app"""
|
||||
"""App configuration class for the 'label' app."""
|
||||
|
||||
name = 'label'
|
||||
|
||||
|
@ -48,28 +48,28 @@ def rename_label_output(instance, filename):
|
||||
|
||||
|
||||
def validate_stock_item_filters(filters):
|
||||
"""Validate query filters for the StockItemLabel model"""
|
||||
"""Validate query filters for the StockItemLabel model."""
|
||||
filters = validateFilterString(filters, model=stock.models.StockItem)
|
||||
|
||||
return filters
|
||||
|
||||
|
||||
def validate_stock_location_filters(filters):
|
||||
"""Validate query filters for the StockLocationLabel model"""
|
||||
"""Validate query filters for the StockLocationLabel model."""
|
||||
filters = validateFilterString(filters, model=stock.models.StockLocation)
|
||||
|
||||
return filters
|
||||
|
||||
|
||||
def validate_part_filters(filters):
|
||||
"""Validate query filters for the PartLabel model"""
|
||||
"""Validate query filters for the PartLabel model."""
|
||||
filters = validateFilterString(filters, model=part.models.Part)
|
||||
|
||||
return filters
|
||||
|
||||
|
||||
def validate_build_line_filters(filters):
|
||||
"""Validate query filters for the BuildLine model"""
|
||||
"""Validate query filters for the BuildLine model."""
|
||||
filters = validateFilterString(filters, model=build.models.BuildLine)
|
||||
|
||||
return filters
|
||||
@ -82,7 +82,7 @@ class WeasyprintLabelMixin(WeasyTemplateResponseMixin):
|
||||
pdf_attachment = True
|
||||
|
||||
def __init__(self, request, template, **kwargs):
|
||||
"""Initialize a label mixin with certain properties"""
|
||||
"""Initialize a label mixin with certain properties."""
|
||||
self.request = request
|
||||
self.template_name = template
|
||||
self.pdf_filename = kwargs.get('filename', 'label.pdf')
|
||||
@ -104,11 +104,11 @@ class LabelTemplate(MetadataMixin, models.Model):
|
||||
|
||||
@property
|
||||
def template(self):
|
||||
"""Return the file path of the template associated with this label instance"""
|
||||
"""Return the file path of the template associated with this label instance."""
|
||||
return self.label.path
|
||||
|
||||
def __str__(self):
|
||||
"""Format a string representation of a label instance"""
|
||||
"""Format a string representation of a label instance."""
|
||||
return f'{self.name} - {self.description}'
|
||||
|
||||
name = models.CharField(
|
||||
@ -196,7 +196,6 @@ class LabelTemplate(MetadataMixin, models.Model):
|
||||
|
||||
This is inserted at the top of the style block for a given label
|
||||
"""
|
||||
|
||||
width = kwargs.get('width', self.width)
|
||||
height = kwargs.get('height', self.height)
|
||||
margin = kwargs.get('margin', 0)
|
||||
@ -215,7 +214,6 @@ class LabelTemplate(MetadataMixin, models.Model):
|
||||
request: The HTTP request object
|
||||
kwargs: Additional keyword arguments
|
||||
"""
|
||||
|
||||
context = self.get_context_data(request)
|
||||
|
||||
# By default, each label is supplied with '@page' data
|
||||
@ -242,8 +240,7 @@ class LabelTemplate(MetadataMixin, models.Model):
|
||||
return context
|
||||
|
||||
def render_as_string(self, request, target_object=None, **kwargs):
|
||||
"""Render the label to a HTML string"""
|
||||
|
||||
"""Render the label to a HTML string."""
|
||||
if target_object:
|
||||
self.object_to_print = target_object
|
||||
|
||||
@ -256,7 +253,6 @@ class LabelTemplate(MetadataMixin, models.Model):
|
||||
|
||||
Uses django-weasyprint plugin to render HTML template
|
||||
"""
|
||||
|
||||
if target_object:
|
||||
self.object_to_print = target_object
|
||||
|
||||
@ -275,7 +271,7 @@ class LabelTemplate(MetadataMixin, models.Model):
|
||||
|
||||
|
||||
class LabelOutput(models.Model):
|
||||
"""Class representing a label output file
|
||||
"""Class representing a label output file.
|
||||
|
||||
'Printing' a label may generate a file object (such as PDF)
|
||||
which is made available for download.
|
||||
@ -301,7 +297,7 @@ class StockItemLabel(LabelTemplate):
|
||||
|
||||
@staticmethod
|
||||
def get_api_url():
|
||||
"""Return the API URL associated with the StockItemLabel model"""
|
||||
"""Return the API URL associated with the StockItemLabel model."""
|
||||
return reverse('api-stockitem-label-list') # pragma: no cover
|
||||
|
||||
SUBDIR = 'stockitem'
|
||||
@ -340,7 +336,7 @@ class StockLocationLabel(LabelTemplate):
|
||||
|
||||
@staticmethod
|
||||
def get_api_url():
|
||||
"""Return the API URL associated with the StockLocationLabel model"""
|
||||
"""Return the API URL associated with the StockLocationLabel model."""
|
||||
return reverse('api-stocklocation-label-list') # pragma: no cover
|
||||
|
||||
SUBDIR = 'stocklocation'
|
||||
@ -365,7 +361,7 @@ class PartLabel(LabelTemplate):
|
||||
|
||||
@staticmethod
|
||||
def get_api_url():
|
||||
"""Return the API url associated with the PartLabel model"""
|
||||
"""Return the API url associated with the PartLabel model."""
|
||||
return reverse('api-part-label-list') # pragma: no cover
|
||||
|
||||
SUBDIR = 'part'
|
||||
@ -396,11 +392,11 @@ class PartLabel(LabelTemplate):
|
||||
|
||||
|
||||
class BuildLineLabel(LabelTemplate):
|
||||
"""Template for printing labels against BuildLine objects"""
|
||||
"""Template for printing labels against BuildLine objects."""
|
||||
|
||||
@staticmethod
|
||||
def get_api_url():
|
||||
"""Return the API URL associated with the BuildLineLabel model"""
|
||||
"""Return the API URL associated with the BuildLineLabel model."""
|
||||
return reverse('api-buildline-label-list')
|
||||
|
||||
SUBDIR = 'buildline'
|
||||
|
@ -1,4 +1,4 @@
|
||||
"""API serializers for the label app"""
|
||||
"""API serializers for the label app."""
|
||||
|
||||
import label.models
|
||||
from InvenTree.serializers import (
|
||||
@ -8,13 +8,13 @@ from InvenTree.serializers import (
|
||||
|
||||
|
||||
class LabelSerializerBase(InvenTreeModelSerializer):
|
||||
"""Base class for label serializer"""
|
||||
"""Base class for label serializer."""
|
||||
|
||||
label = InvenTreeAttachmentSerializerField(required=True)
|
||||
|
||||
@staticmethod
|
||||
def label_fields():
|
||||
"""Generic serializer fields for a label template"""
|
||||
"""Generic serializer fields for a label template."""
|
||||
return ['pk', 'name', 'description', 'label', 'filters', 'enabled']
|
||||
|
||||
|
||||
@ -49,7 +49,7 @@ class PartLabelSerializer(LabelSerializerBase):
|
||||
|
||||
|
||||
class BuildLineLabelSerializer(LabelSerializerBase):
|
||||
"""Serializes a BuildLineLabel object"""
|
||||
"""Serializes a BuildLineLabel object."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options."""
|
||||
|
@ -1,4 +1,4 @@
|
||||
"""Background tasks for the label app"""
|
||||
"""Background tasks for the label app."""
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
@ -10,6 +10,6 @@ from label.models import LabelOutput
|
||||
|
||||
@scheduled_task(ScheduledTask.DAILY)
|
||||
def cleanup_old_label_outputs():
|
||||
"""Remove old label outputs from the database"""
|
||||
"""Remove old label outputs from the database."""
|
||||
# Remove any label outputs which are older than 30 days
|
||||
LabelOutput.objects.filter(created__lte=timezone.now() - timedelta(days=5)).delete()
|
||||
|
@ -1,4 +1,4 @@
|
||||
"""Unit tests for label API"""
|
||||
"""Unit tests for label API."""
|
||||
|
||||
from django.urls import reverse
|
||||
|
||||
@ -15,7 +15,7 @@ class TestReportTests(InvenTreeAPITestCase):
|
||||
list_url = reverse('api-stockitem-testreport-list')
|
||||
|
||||
def do_list(self, filters=None):
|
||||
"""Helper function to request list of labels with provided filters"""
|
||||
"""Helper function to request list of labels with provided filters."""
|
||||
# Set default - see B006
|
||||
if filters is None:
|
||||
filters = {}
|
||||
@ -27,7 +27,7 @@ class TestReportTests(InvenTreeAPITestCase):
|
||||
return response.data
|
||||
|
||||
def test_list(self):
|
||||
"""Test the API list endpoint"""
|
||||
"""Test the API list endpoint."""
|
||||
response = self.do_list()
|
||||
|
||||
# TODO - Add some report templates to the fixtures
|
||||
|
@ -1,4 +1,4 @@
|
||||
"""Tests for labels"""
|
||||
"""Tests for labels."""
|
||||
|
||||
import io
|
||||
import json
|
||||
@ -22,13 +22,13 @@ from .models import PartLabel, StockItemLabel, StockLocationLabel
|
||||
|
||||
|
||||
class LabelTest(InvenTreeAPITestCase):
|
||||
"""Unit test class for label models"""
|
||||
"""Unit test class for label models."""
|
||||
|
||||
fixtures = ['category', 'part', 'location', 'stock']
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
"""Ensure that some label instances exist as part of init routine"""
|
||||
"""Ensure that some label instances exist as part of init routine."""
|
||||
super().setUpTestData()
|
||||
apps.get_app_config('label').create_labels()
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python
|
||||
"""InvenTree / django management commands"""
|
||||
"""InvenTree / django management commands."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
@ -1,4 +1,4 @@
|
||||
"""Admin functionality for the 'order' app"""
|
||||
"""Admin functionality for the 'order' app."""
|
||||
|
||||
from django.contrib import admin
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
@ -12,36 +12,36 @@ from order import models
|
||||
|
||||
|
||||
class ProjectCodeResourceMixin:
|
||||
"""Mixin for exporting project code data"""
|
||||
"""Mixin for exporting project code data."""
|
||||
|
||||
project_code = Field(attribute='project_code', column_name=_('Project Code'))
|
||||
|
||||
def dehydrate_project_code(self, order):
|
||||
"""Return the project code value, not the pk"""
|
||||
"""Return the project code value, not the pk."""
|
||||
if order.project_code:
|
||||
return order.project_code.code
|
||||
return ''
|
||||
|
||||
|
||||
class TotalPriceResourceMixin:
|
||||
"""Mixin for exporting total price data"""
|
||||
"""Mixin for exporting total price data."""
|
||||
|
||||
total_price = Field(attribute='total_price', column_name=_('Total Price'))
|
||||
|
||||
def dehydrate_total_price(self, order):
|
||||
"""Return the total price amount, not the object itself"""
|
||||
"""Return the total price amount, not the object itself."""
|
||||
if order.total_price:
|
||||
return order.total_price.amount
|
||||
return ''
|
||||
|
||||
|
||||
class PriceResourceMixin:
|
||||
"""Mixin for 'price' field"""
|
||||
"""Mixin for 'price' field."""
|
||||
|
||||
price = Field(attribute='price', column_name=_('Price'))
|
||||
|
||||
def dehydrate_price(self, line):
|
||||
"""Return the price amount, not the object itself"""
|
||||
"""Return the price amount, not the object itself."""
|
||||
if line.price:
|
||||
return line.price.amount
|
||||
return ''
|
||||
@ -49,7 +49,7 @@ class PriceResourceMixin:
|
||||
|
||||
# region general classes
|
||||
class GeneralExtraLineAdmin:
|
||||
"""Admin class template for the 'ExtraLineItem' models"""
|
||||
"""Admin class template for the 'ExtraLineItem' models."""
|
||||
|
||||
list_display = ('order', 'quantity', 'reference')
|
||||
|
||||
@ -59,7 +59,7 @@ class GeneralExtraLineAdmin:
|
||||
|
||||
|
||||
class GeneralExtraLineMeta:
|
||||
"""Metaclass template for the 'ExtraLineItem' models"""
|
||||
"""Metaclass template for the 'ExtraLineItem' models."""
|
||||
|
||||
skip_unchanged = True
|
||||
report_skipped = False
|
||||
@ -70,7 +70,7 @@ class GeneralExtraLineMeta:
|
||||
|
||||
|
||||
class PurchaseOrderLineItemInlineAdmin(admin.StackedInline):
|
||||
"""Inline admin class for the PurchaseOrderLineItem model"""
|
||||
"""Inline admin class for the PurchaseOrderLineItem model."""
|
||||
|
||||
model = models.PurchaseOrderLineItem
|
||||
extra = 0
|
||||
@ -82,7 +82,7 @@ class PurchaseOrderResource(
|
||||
"""Class for managing import / export of PurchaseOrder data."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass"""
|
||||
"""Metaclass options."""
|
||||
|
||||
model = models.PurchaseOrder
|
||||
skip_unchanged = True
|
||||
@ -101,7 +101,7 @@ class PurchaseOrderResource(
|
||||
|
||||
|
||||
class PurchaseOrderAdmin(ImportExportModelAdmin):
|
||||
"""Admin class for the PurchaseOrder model"""
|
||||
"""Admin class for the PurchaseOrder model."""
|
||||
|
||||
resource_class = PurchaseOrderResource
|
||||
|
||||
@ -122,7 +122,7 @@ class SalesOrderResource(
|
||||
"""Class for managing import / export of SalesOrder data."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options"""
|
||||
"""Metaclass options."""
|
||||
|
||||
model = models.SalesOrder
|
||||
skip_unchanged = True
|
||||
@ -141,7 +141,7 @@ class SalesOrderResource(
|
||||
|
||||
|
||||
class SalesOrderAdmin(ImportExportModelAdmin):
|
||||
"""Admin class for the SalesOrder model"""
|
||||
"""Admin class for the SalesOrder model."""
|
||||
|
||||
resource_class = SalesOrderResource
|
||||
|
||||
@ -158,7 +158,7 @@ class PurchaseOrderLineItemResource(PriceResourceMixin, InvenTreeResource):
|
||||
"""Class for managing import / export of PurchaseOrderLineItem data."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass"""
|
||||
"""Metaclass."""
|
||||
|
||||
model = models.PurchaseOrderLineItem
|
||||
skip_unchanged = True
|
||||
@ -174,7 +174,7 @@ class PurchaseOrderLineItemResource(PriceResourceMixin, InvenTreeResource):
|
||||
SKU = Field(attribute='part__SKU', readonly=True)
|
||||
|
||||
def dehydrate_purchase_price(self, line):
|
||||
"""Return a string value of the 'purchase_price' field, rather than the 'Money' object"""
|
||||
"""Return a string value of the 'purchase_price' field, rather than the 'Money' object."""
|
||||
if line.purchase_price:
|
||||
return line.purchase_price.amount
|
||||
return ''
|
||||
@ -193,7 +193,7 @@ class SalesOrderLineItemResource(PriceResourceMixin, InvenTreeResource):
|
||||
"""Class for managing import / export of SalesOrderLineItem data."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options"""
|
||||
"""Metaclass options."""
|
||||
|
||||
model = models.SalesOrderLineItem
|
||||
skip_unchanged = True
|
||||
@ -228,7 +228,7 @@ class SalesOrderExtraLineResource(PriceResourceMixin, InvenTreeResource):
|
||||
|
||||
|
||||
class PurchaseOrderLineItemAdmin(ImportExportModelAdmin):
|
||||
"""Admin class for the PurchaseOrderLine model"""
|
||||
"""Admin class for the PurchaseOrderLine model."""
|
||||
|
||||
resource_class = PurchaseOrderLineItemResource
|
||||
|
||||
@ -240,13 +240,13 @@ class PurchaseOrderLineItemAdmin(ImportExportModelAdmin):
|
||||
|
||||
|
||||
class PurchaseOrderExtraLineAdmin(GeneralExtraLineAdmin, ImportExportModelAdmin):
|
||||
"""Admin class for the PurchaseOrderExtraLine model"""
|
||||
"""Admin class for the PurchaseOrderExtraLine model."""
|
||||
|
||||
resource_class = PurchaseOrderExtraLineResource
|
||||
|
||||
|
||||
class SalesOrderLineItemAdmin(ImportExportModelAdmin):
|
||||
"""Admin class for the SalesOrderLine model"""
|
||||
"""Admin class for the SalesOrderLine model."""
|
||||
|
||||
resource_class = SalesOrderLineItemResource
|
||||
|
||||
@ -263,13 +263,13 @@ class SalesOrderLineItemAdmin(ImportExportModelAdmin):
|
||||
|
||||
|
||||
class SalesOrderExtraLineAdmin(GeneralExtraLineAdmin, ImportExportModelAdmin):
|
||||
"""Admin class for the SalesOrderExtraLine model"""
|
||||
"""Admin class for the SalesOrderExtraLine model."""
|
||||
|
||||
resource_class = SalesOrderExtraLineResource
|
||||
|
||||
|
||||
class SalesOrderShipmentAdmin(ImportExportModelAdmin):
|
||||
"""Admin class for the SalesOrderShipment model"""
|
||||
"""Admin class for the SalesOrderShipment model."""
|
||||
|
||||
list_display = ['order', 'shipment_date', 'reference']
|
||||
|
||||
@ -279,7 +279,7 @@ class SalesOrderShipmentAdmin(ImportExportModelAdmin):
|
||||
|
||||
|
||||
class SalesOrderAllocationAdmin(ImportExportModelAdmin):
|
||||
"""Admin class for the SalesOrderAllocation model"""
|
||||
"""Admin class for the SalesOrderAllocation model."""
|
||||
|
||||
list_display = ('line', 'item', 'quantity')
|
||||
|
||||
@ -289,10 +289,10 @@ class SalesOrderAllocationAdmin(ImportExportModelAdmin):
|
||||
class ReturnOrderResource(
|
||||
ProjectCodeResourceMixin, TotalPriceResourceMixin, InvenTreeResource
|
||||
):
|
||||
"""Class for managing import / export of ReturnOrder data"""
|
||||
"""Class for managing import / export of ReturnOrder data."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options"""
|
||||
"""Metaclass options."""
|
||||
|
||||
model = models.ReturnOrder
|
||||
skip_unchanged = True
|
||||
@ -301,7 +301,7 @@ class ReturnOrderResource(
|
||||
|
||||
|
||||
class ReturnOrderAdmin(ImportExportModelAdmin):
|
||||
"""Admin class for the ReturnOrder model"""
|
||||
"""Admin class for the ReturnOrder model."""
|
||||
|
||||
resource_class = ReturnOrderResource
|
||||
|
||||
@ -315,10 +315,10 @@ class ReturnOrderAdmin(ImportExportModelAdmin):
|
||||
|
||||
|
||||
class ReturnOrderLineItemResource(PriceResourceMixin, InvenTreeResource):
|
||||
"""Class for managing import / export of ReturnOrderLineItem data"""
|
||||
"""Class for managing import / export of ReturnOrderLineItem data."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options"""
|
||||
"""Metaclass options."""
|
||||
|
||||
model = models.ReturnOrderLineItem
|
||||
skip_unchanged = True
|
||||
@ -327,7 +327,7 @@ class ReturnOrderLineItemResource(PriceResourceMixin, InvenTreeResource):
|
||||
|
||||
|
||||
class ReturnOrderLineItemAdmin(ImportExportModelAdmin):
|
||||
"""Admin class for ReturnOrderLine model"""
|
||||
"""Admin class for ReturnOrderLine model."""
|
||||
|
||||
resource_class = ReturnOrderLineItemResource
|
||||
|
||||
@ -335,16 +335,16 @@ class ReturnOrderLineItemAdmin(ImportExportModelAdmin):
|
||||
|
||||
|
||||
class ReturnOrderExtraLineClass(PriceResourceMixin, InvenTreeResource):
|
||||
"""Class for managing import/export of ReturnOrderExtraLine data"""
|
||||
"""Class for managing import/export of ReturnOrderExtraLine data."""
|
||||
|
||||
class Meta(GeneralExtraLineMeta):
|
||||
"""Metaclass options"""
|
||||
"""Metaclass options."""
|
||||
|
||||
model = models.ReturnOrderExtraLine
|
||||
|
||||
|
||||
class ReturnOrdeerExtraLineAdmin(GeneralExtraLineAdmin, ImportExportModelAdmin):
|
||||
"""Admin class for the ReturnOrderExtraLine model"""
|
||||
"""Admin class for the ReturnOrderExtraLine model."""
|
||||
|
||||
resource_class = ReturnOrderExtraLineClass
|
||||
|
||||
|
@ -53,7 +53,7 @@ class GeneralExtraLineList(APIDownloadMixin):
|
||||
"""General template for ExtraLine API classes."""
|
||||
|
||||
def get_serializer(self, *args, **kwargs):
|
||||
"""Return the serializer instance for this endpoint"""
|
||||
"""Return the serializer instance for this endpoint."""
|
||||
try:
|
||||
params = self.request.query_params3
|
||||
|
||||
@ -66,7 +66,7 @@ class GeneralExtraLineList(APIDownloadMixin):
|
||||
return self.serializer_class(*args, **kwargs)
|
||||
|
||||
def get_queryset(self, *args, **kwargs):
|
||||
"""Return the annotated queryset for this endpoint"""
|
||||
"""Return the annotated queryset for this endpoint."""
|
||||
queryset = super().get_queryset(*args, **kwargs)
|
||||
|
||||
queryset = queryset.prefetch_related('order')
|
||||
@ -89,7 +89,7 @@ class OrderFilter(rest_filters.FilterSet):
|
||||
status = rest_filters.NumberFilter(label='Order Status', method='filter_status')
|
||||
|
||||
def filter_status(self, queryset, name, value):
|
||||
"""Filter by integer status code"""
|
||||
"""Filter by integer status code."""
|
||||
return queryset.filter(status=value)
|
||||
|
||||
# Exact match for reference
|
||||
@ -126,7 +126,7 @@ class OrderFilter(rest_filters.FilterSet):
|
||||
)
|
||||
|
||||
def filter_outstanding(self, queryset, name, value):
|
||||
"""Generic filter for determining if an order is 'outstanding'"""
|
||||
"""Generic filter for determining if an order is 'outstanding'."""
|
||||
if str2bool(value):
|
||||
return queryset.filter(status__in=self.Meta.model.get_status_class().OPEN)
|
||||
return queryset.exclude(status__in=self.Meta.model.get_status_class().OPEN)
|
||||
@ -140,14 +140,14 @@ class OrderFilter(rest_filters.FilterSet):
|
||||
)
|
||||
|
||||
def filter_has_project_code(self, queryset, name, value):
|
||||
"""Filter by whether or not the order has a project code"""
|
||||
"""Filter by whether or not the order has a project code."""
|
||||
if str2bool(value):
|
||||
return queryset.exclude(project_code=None)
|
||||
return queryset.filter(project_code=None)
|
||||
|
||||
|
||||
class LineItemFilter(rest_filters.FilterSet):
|
||||
"""Base class for custom API filters for order line item list(s)"""
|
||||
"""Base class for custom API filters for order line item list(s)."""
|
||||
|
||||
# Filter by order status
|
||||
order_status = rest_filters.NumberFilter(
|
||||
@ -159,7 +159,7 @@ class LineItemFilter(rest_filters.FilterSet):
|
||||
)
|
||||
|
||||
def filter_has_pricing(self, queryset, name, value):
|
||||
"""Filter by whether or not the line item has pricing information"""
|
||||
"""Filter by whether or not the line item has pricing information."""
|
||||
filters = {self.Meta.price_field: None}
|
||||
|
||||
if str2bool(value):
|
||||
@ -178,13 +178,13 @@ class PurchaseOrderFilter(OrderFilter):
|
||||
|
||||
|
||||
class PurchaseOrderMixin:
|
||||
"""Mixin class for PurchaseOrder endpoints"""
|
||||
"""Mixin class for PurchaseOrder endpoints."""
|
||||
|
||||
queryset = models.PurchaseOrder.objects.all()
|
||||
serializer_class = serializers.PurchaseOrderSerializer
|
||||
|
||||
def get_serializer(self, *args, **kwargs):
|
||||
"""Return the serializer instance for this endpoint"""
|
||||
"""Return the serializer instance for this endpoint."""
|
||||
try:
|
||||
kwargs['supplier_detail'] = str2bool(
|
||||
self.request.query_params.get('supplier_detail', False)
|
||||
@ -198,7 +198,7 @@ class PurchaseOrderMixin:
|
||||
return self.serializer_class(*args, **kwargs)
|
||||
|
||||
def get_queryset(self, *args, **kwargs):
|
||||
"""Return the annotated queryset for this endpoint"""
|
||||
"""Return the annotated queryset for this endpoint."""
|
||||
queryset = super().get_queryset(*args, **kwargs)
|
||||
|
||||
queryset = queryset.prefetch_related('supplier', 'lines')
|
||||
@ -266,7 +266,7 @@ class PurchaseOrderList(PurchaseOrderMixin, APIDownloadMixin, ListCreateAPI):
|
||||
)
|
||||
|
||||
def download_queryset(self, queryset, export_format):
|
||||
"""Download the filtered queryset as a file"""
|
||||
"""Download the filtered queryset as a file."""
|
||||
dataset = PurchaseOrderResource().export(queryset=queryset)
|
||||
|
||||
filedata = dataset.export(export_format)
|
||||
@ -276,7 +276,7 @@ class PurchaseOrderList(PurchaseOrderMixin, APIDownloadMixin, ListCreateAPI):
|
||||
return DownloadFile(filedata, filename)
|
||||
|
||||
def filter_queryset(self, queryset):
|
||||
"""Custom queryset filtering"""
|
||||
"""Custom queryset filtering."""
|
||||
# Perform basic filtering
|
||||
queryset = super().filter_queryset(queryset)
|
||||
|
||||
@ -427,7 +427,7 @@ class PurchaseOrderLineItemFilter(LineItemFilter):
|
||||
pending = rest_filters.BooleanFilter(label='pending', method='filter_pending')
|
||||
|
||||
def filter_pending(self, queryset, name, value):
|
||||
"""Filter by "pending" status (order status = pending)"""
|
||||
"""Filter by "pending" status (order status = pending)."""
|
||||
if str2bool(value):
|
||||
return queryset.filter(order__status__in=PurchaseOrderStatusGroups.OPEN)
|
||||
return queryset.exclude(order__status__in=PurchaseOrderStatusGroups.OPEN)
|
||||
@ -435,7 +435,7 @@ class PurchaseOrderLineItemFilter(LineItemFilter):
|
||||
received = rest_filters.BooleanFilter(label='received', method='filter_received')
|
||||
|
||||
def filter_received(self, queryset, name, value):
|
||||
"""Filter by lines which are "received" (or "not" received)
|
||||
"""Filter by lines which are "received" (or "not" received).
|
||||
|
||||
A line is considered "received" when received >= quantity
|
||||
"""
|
||||
@ -450,13 +450,13 @@ class PurchaseOrderLineItemFilter(LineItemFilter):
|
||||
|
||||
|
||||
class PurchaseOrderLineItemMixin:
|
||||
"""Mixin class for PurchaseOrderLineItem endpoints"""
|
||||
"""Mixin class for PurchaseOrderLineItem endpoints."""
|
||||
|
||||
queryset = models.PurchaseOrderLineItem.objects.all()
|
||||
serializer_class = serializers.PurchaseOrderLineItemSerializer
|
||||
|
||||
def get_queryset(self, *args, **kwargs):
|
||||
"""Return annotated queryset for this endpoint"""
|
||||
"""Return annotated queryset for this endpoint."""
|
||||
queryset = super().get_queryset(*args, **kwargs)
|
||||
|
||||
queryset = serializers.PurchaseOrderLineItemSerializer.annotate_queryset(
|
||||
@ -466,7 +466,7 @@ class PurchaseOrderLineItemMixin:
|
||||
return queryset
|
||||
|
||||
def get_serializer(self, *args, **kwargs):
|
||||
"""Return serializer instance for this endpoint"""
|
||||
"""Return serializer instance for this endpoint."""
|
||||
try:
|
||||
kwargs['part_detail'] = str2bool(
|
||||
self.request.query_params.get('part_detail', False)
|
||||
@ -513,7 +513,7 @@ class PurchaseOrderLineItemList(
|
||||
return queryset
|
||||
|
||||
def download_queryset(self, queryset, export_format):
|
||||
"""Download the requested queryset as a file"""
|
||||
"""Download the requested queryset as a file."""
|
||||
dataset = PurchaseOrderLineItemResource().export(queryset=queryset)
|
||||
|
||||
filedata = dataset.export(export_format)
|
||||
@ -564,7 +564,7 @@ class PurchaseOrderExtraLineList(GeneralExtraLineList, ListCreateAPI):
|
||||
serializer_class = serializers.PurchaseOrderExtraLineSerializer
|
||||
|
||||
def download_queryset(self, queryset, export_format):
|
||||
"""Download this queryset as a file"""
|
||||
"""Download this queryset as a file."""
|
||||
dataset = PurchaseOrderExtraLineResource().export(queryset=queryset)
|
||||
filedata = dataset.export(export_format)
|
||||
filename = f'InvenTree_ExtraPurchaseOrderLines.{export_format}'
|
||||
@ -580,7 +580,7 @@ class PurchaseOrderExtraLineDetail(RetrieveUpdateDestroyAPI):
|
||||
|
||||
|
||||
class SalesOrderAttachmentList(AttachmentMixin, ListCreateDestroyAPIView):
|
||||
"""API endpoint for listing (and creating) a SalesOrderAttachment (file upload)"""
|
||||
"""API endpoint for listing (and creating) a SalesOrderAttachment (file upload)."""
|
||||
|
||||
queryset = models.SalesOrderAttachment.objects.all()
|
||||
serializer_class = serializers.SalesOrderAttachmentSerializer
|
||||
@ -606,13 +606,13 @@ class SalesOrderFilter(OrderFilter):
|
||||
|
||||
|
||||
class SalesOrderMixin:
|
||||
"""Mixin class for SalesOrder endpoints"""
|
||||
"""Mixin class for SalesOrder endpoints."""
|
||||
|
||||
queryset = models.SalesOrder.objects.all()
|
||||
serializer_class = serializers.SalesOrderSerializer
|
||||
|
||||
def get_serializer(self, *args, **kwargs):
|
||||
"""Return serializer instance for this endpoint"""
|
||||
"""Return serializer instance for this endpoint."""
|
||||
try:
|
||||
kwargs['customer_detail'] = str2bool(
|
||||
self.request.query_params.get('customer_detail', False)
|
||||
@ -626,7 +626,7 @@ class SalesOrderMixin:
|
||||
return self.serializer_class(*args, **kwargs)
|
||||
|
||||
def get_queryset(self, *args, **kwargs):
|
||||
"""Return annotated queryset for this endpoint"""
|
||||
"""Return annotated queryset for this endpoint."""
|
||||
queryset = super().get_queryset(*args, **kwargs)
|
||||
|
||||
queryset = queryset.prefetch_related('customer', 'lines')
|
||||
@ -660,7 +660,7 @@ class SalesOrderList(SalesOrderMixin, APIDownloadMixin, ListCreateAPI):
|
||||
)
|
||||
|
||||
def download_queryset(self, queryset, export_format):
|
||||
"""Download this queryset as a file"""
|
||||
"""Download this queryset as a file."""
|
||||
dataset = SalesOrderResource().export(queryset=queryset)
|
||||
|
||||
filedata = dataset.export(export_format)
|
||||
@ -759,13 +759,13 @@ class SalesOrderLineItemFilter(LineItemFilter):
|
||||
|
||||
|
||||
class SalesOrderLineItemMixin:
|
||||
"""Mixin class for SalesOrderLineItem endpoints"""
|
||||
"""Mixin class for SalesOrderLineItem endpoints."""
|
||||
|
||||
queryset = models.SalesOrderLineItem.objects.all()
|
||||
serializer_class = serializers.SalesOrderLineItemSerializer
|
||||
|
||||
def get_serializer(self, *args, **kwargs):
|
||||
"""Return serializer for this endpoint with extra data as requested"""
|
||||
"""Return serializer for this endpoint with extra data as requested."""
|
||||
try:
|
||||
params = self.request.query_params
|
||||
|
||||
@ -782,7 +782,7 @@ class SalesOrderLineItemMixin:
|
||||
return self.serializer_class(*args, **kwargs)
|
||||
|
||||
def get_queryset(self, *args, **kwargs):
|
||||
"""Return annotated queryset for this endpoint"""
|
||||
"""Return annotated queryset for this endpoint."""
|
||||
queryset = super().get_queryset(*args, **kwargs)
|
||||
|
||||
queryset = queryset.prefetch_related(
|
||||
@ -805,7 +805,7 @@ class SalesOrderLineItemList(SalesOrderLineItemMixin, APIDownloadMixin, ListCrea
|
||||
filterset_class = SalesOrderLineItemFilter
|
||||
|
||||
def download_queryset(self, queryset, export_format):
|
||||
"""Download the requested queryset as a file"""
|
||||
"""Download the requested queryset as a file."""
|
||||
dataset = SalesOrderLineItemResource().export(queryset=queryset)
|
||||
filedata = dataset.export(export_format)
|
||||
|
||||
@ -833,7 +833,7 @@ class SalesOrderExtraLineList(GeneralExtraLineList, ListCreateAPI):
|
||||
serializer_class = serializers.SalesOrderExtraLineSerializer
|
||||
|
||||
def download_queryset(self, queryset, export_format):
|
||||
"""Download this queryset as a file"""
|
||||
"""Download this queryset as a file."""
|
||||
dataset = SalesOrderExtraLineResource().export(queryset=queryset)
|
||||
filedata = dataset.export(export_format)
|
||||
filename = f'InvenTree_ExtraSalesOrderLines.{export_format}'
|
||||
@ -854,7 +854,7 @@ class SalesOrderContextMixin:
|
||||
queryset = models.SalesOrder.objects.all()
|
||||
|
||||
def get_serializer_context(self):
|
||||
"""Add the 'order' reference to the serializer context for any classes which inherit this mixin"""
|
||||
"""Add the 'order' reference to the serializer context for any classes which inherit this mixin."""
|
||||
ctx = super().get_serializer_context()
|
||||
|
||||
ctx['request'] = self.request
|
||||
@ -868,13 +868,13 @@ class SalesOrderContextMixin:
|
||||
|
||||
|
||||
class SalesOrderCancel(SalesOrderContextMixin, CreateAPI):
|
||||
"""API endpoint to cancel a SalesOrder"""
|
||||
"""API endpoint to cancel a SalesOrder."""
|
||||
|
||||
serializer_class = serializers.SalesOrderCancelSerializer
|
||||
|
||||
|
||||
class SalesOrderIssue(SalesOrderContextMixin, CreateAPI):
|
||||
"""API endpoint to issue a SalesOrder"""
|
||||
"""API endpoint to issue a SalesOrder."""
|
||||
|
||||
serializer_class = serializers.SalesOrderIssueSerializer
|
||||
|
||||
@ -935,7 +935,7 @@ class SalesOrderAllocationList(ListAPI):
|
||||
return self.serializer_class(*args, **kwargs)
|
||||
|
||||
def filter_queryset(self, queryset):
|
||||
"""Custom queryset filtering"""
|
||||
"""Custom queryset filtering."""
|
||||
queryset = super().filter_queryset(queryset)
|
||||
|
||||
# Filter by order
|
||||
@ -995,7 +995,7 @@ class SalesOrderShipmentFilter(rest_filters.FilterSet):
|
||||
shipped = rest_filters.BooleanFilter(label='shipped', method='filter_shipped')
|
||||
|
||||
def filter_shipped(self, queryset, name, value):
|
||||
"""Filter SalesOrder list by 'shipped' status (boolean)"""
|
||||
"""Filter SalesOrder list by 'shipped' status (boolean)."""
|
||||
if str2bool(value):
|
||||
return queryset.exclude(shipment_date=None)
|
||||
return queryset.filter(shipment_date=None)
|
||||
@ -1003,7 +1003,7 @@ class SalesOrderShipmentFilter(rest_filters.FilterSet):
|
||||
delivered = rest_filters.BooleanFilter(label='delivered', method='filter_delivered')
|
||||
|
||||
def filter_delivered(self, queryset, name, value):
|
||||
"""Filter SalesOrder list by 'delivered' status (boolean)"""
|
||||
"""Filter SalesOrder list by 'delivered' status (boolean)."""
|
||||
if str2bool(value):
|
||||
return queryset.exclude(delivery_date=None)
|
||||
return queryset.filter(delivery_date=None)
|
||||
@ -1048,7 +1048,7 @@ class SalesOrderShipmentComplete(CreateAPI):
|
||||
|
||||
|
||||
class PurchaseOrderAttachmentList(AttachmentMixin, ListCreateDestroyAPIView):
|
||||
"""API endpoint for listing (and creating) a PurchaseOrderAttachment (file upload)"""
|
||||
"""API endpoint for listing (and creating) a PurchaseOrderAttachment (file upload)."""
|
||||
|
||||
queryset = models.PurchaseOrderAttachment.objects.all()
|
||||
serializer_class = serializers.PurchaseOrderAttachmentSerializer
|
||||
@ -1064,23 +1064,23 @@ class PurchaseOrderAttachmentDetail(AttachmentMixin, RetrieveUpdateDestroyAPI):
|
||||
|
||||
|
||||
class ReturnOrderFilter(OrderFilter):
|
||||
"""Custom API filters for the ReturnOrderList endpoint"""
|
||||
"""Custom API filters for the ReturnOrderList endpoint."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options"""
|
||||
"""Metaclass options."""
|
||||
|
||||
model = models.ReturnOrder
|
||||
fields = ['customer']
|
||||
|
||||
|
||||
class ReturnOrderMixin:
|
||||
"""Mixin class for ReturnOrder endpoints"""
|
||||
"""Mixin class for ReturnOrder endpoints."""
|
||||
|
||||
queryset = models.ReturnOrder.objects.all()
|
||||
serializer_class = serializers.ReturnOrderSerializer
|
||||
|
||||
def get_serializer(self, *args, **kwargs):
|
||||
"""Return serializer instance for this endpoint"""
|
||||
"""Return serializer instance for this endpoint."""
|
||||
try:
|
||||
kwargs['customer_detail'] = str2bool(
|
||||
self.request.query_params.get('customer_detail', False)
|
||||
@ -1094,7 +1094,7 @@ class ReturnOrderMixin:
|
||||
return self.serializer_class(*args, **kwargs)
|
||||
|
||||
def get_queryset(self, *args, **kwargs):
|
||||
"""Return annotated queryset for this endpoint"""
|
||||
"""Return annotated queryset for this endpoint."""
|
||||
queryset = super().get_queryset(*args, **kwargs)
|
||||
|
||||
queryset = queryset.prefetch_related('customer')
|
||||
@ -1105,7 +1105,7 @@ class ReturnOrderMixin:
|
||||
|
||||
|
||||
class ReturnOrderList(ReturnOrderMixin, APIDownloadMixin, ListCreateAPI):
|
||||
"""API endpoint for accessing a list of ReturnOrder objects"""
|
||||
"""API endpoint for accessing a list of ReturnOrder objects."""
|
||||
|
||||
filterset_class = ReturnOrderFilter
|
||||
|
||||
@ -1124,7 +1124,7 @@ class ReturnOrderList(ReturnOrderMixin, APIDownloadMixin, ListCreateAPI):
|
||||
)
|
||||
|
||||
def download_queryset(self, queryset, export_format):
|
||||
"""Download this queryset as a file"""
|
||||
"""Download this queryset as a file."""
|
||||
dataset = ReturnOrderResource().export(queryset=queryset)
|
||||
filedata = dataset.export(export_format)
|
||||
filename = f'InvenTree_ReturnOrders.{export_format}'
|
||||
@ -1161,13 +1161,13 @@ class ReturnOrderList(ReturnOrderMixin, APIDownloadMixin, ListCreateAPI):
|
||||
|
||||
|
||||
class ReturnOrderDetail(ReturnOrderMixin, RetrieveUpdateDestroyAPI):
|
||||
"""API endpoint for detail view of a single ReturnOrder object"""
|
||||
"""API endpoint for detail view of a single ReturnOrder object."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class ReturnOrderContextMixin:
|
||||
"""Simple mixin class to add a ReturnOrder to the serializer context"""
|
||||
"""Simple mixin class to add a ReturnOrder to the serializer context."""
|
||||
|
||||
queryset = models.ReturnOrder.objects.all()
|
||||
|
||||
@ -1189,35 +1189,35 @@ class ReturnOrderContextMixin:
|
||||
|
||||
|
||||
class ReturnOrderCancel(ReturnOrderContextMixin, CreateAPI):
|
||||
"""API endpoint to cancel a ReturnOrder"""
|
||||
"""API endpoint to cancel a ReturnOrder."""
|
||||
|
||||
serializer_class = serializers.ReturnOrderCancelSerializer
|
||||
|
||||
|
||||
class ReturnOrderComplete(ReturnOrderContextMixin, CreateAPI):
|
||||
"""API endpoint to complete a ReturnOrder"""
|
||||
"""API endpoint to complete a ReturnOrder."""
|
||||
|
||||
serializer_class = serializers.ReturnOrderCompleteSerializer
|
||||
|
||||
|
||||
class ReturnOrderIssue(ReturnOrderContextMixin, CreateAPI):
|
||||
"""API endpoint to issue (place) a ReturnOrder"""
|
||||
"""API endpoint to issue (place) a ReturnOrder."""
|
||||
|
||||
serializer_class = serializers.ReturnOrderIssueSerializer
|
||||
|
||||
|
||||
class ReturnOrderReceive(ReturnOrderContextMixin, CreateAPI):
|
||||
"""API endpoint to receive items against a ReturnOrder"""
|
||||
"""API endpoint to receive items against a ReturnOrder."""
|
||||
|
||||
queryset = models.ReturnOrder.objects.none()
|
||||
serializer_class = serializers.ReturnOrderReceiveSerializer
|
||||
|
||||
|
||||
class ReturnOrderLineItemFilter(LineItemFilter):
|
||||
"""Custom filters for the ReturnOrderLineItemList endpoint"""
|
||||
"""Custom filters for the ReturnOrderLineItemList endpoint."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options"""
|
||||
"""Metaclass options."""
|
||||
|
||||
price_field = 'price'
|
||||
model = models.ReturnOrderLineItem
|
||||
@ -1228,20 +1228,20 @@ class ReturnOrderLineItemFilter(LineItemFilter):
|
||||
received = rest_filters.BooleanFilter(label='received', method='filter_received')
|
||||
|
||||
def filter_received(self, queryset, name, value):
|
||||
"""Filter by 'received' field"""
|
||||
"""Filter by 'received' field."""
|
||||
if str2bool(value):
|
||||
return queryset.exclude(received_date=None)
|
||||
return queryset.filter(received_date=None)
|
||||
|
||||
|
||||
class ReturnOrderLineItemMixin:
|
||||
"""Mixin class for ReturnOrderLineItem endpoints"""
|
||||
"""Mixin class for ReturnOrderLineItem endpoints."""
|
||||
|
||||
queryset = models.ReturnOrderLineItem.objects.all()
|
||||
serializer_class = serializers.ReturnOrderLineItemSerializer
|
||||
|
||||
def get_serializer(self, *args, **kwargs):
|
||||
"""Return serializer for this endpoint with extra data as requested"""
|
||||
"""Return serializer for this endpoint with extra data as requested."""
|
||||
try:
|
||||
params = self.request.query_params
|
||||
|
||||
@ -1256,7 +1256,7 @@ class ReturnOrderLineItemMixin:
|
||||
return self.serializer_class(*args, **kwargs)
|
||||
|
||||
def get_queryset(self, *args, **kwargs):
|
||||
"""Return annotated queryset for this endpoint"""
|
||||
"""Return annotated queryset for this endpoint."""
|
||||
queryset = super().get_queryset(*args, **kwargs)
|
||||
|
||||
queryset = queryset.prefetch_related('order', 'item', 'item__part')
|
||||
@ -1267,12 +1267,12 @@ class ReturnOrderLineItemMixin:
|
||||
class ReturnOrderLineItemList(
|
||||
ReturnOrderLineItemMixin, APIDownloadMixin, ListCreateAPI
|
||||
):
|
||||
"""API endpoint for accessing a list of ReturnOrderLineItemList objects"""
|
||||
"""API endpoint for accessing a list of ReturnOrderLineItemList objects."""
|
||||
|
||||
filterset_class = ReturnOrderLineItemFilter
|
||||
|
||||
def download_queryset(self, queryset, export_format):
|
||||
"""Download the requested queryset as a file"""
|
||||
"""Download the requested queryset as a file."""
|
||||
raise NotImplementedError(
|
||||
'download_queryset not yet implemented for this endpoint'
|
||||
)
|
||||
@ -1290,31 +1290,31 @@ class ReturnOrderLineItemList(
|
||||
|
||||
|
||||
class ReturnOrderLineItemDetail(ReturnOrderLineItemMixin, RetrieveUpdateDestroyAPI):
|
||||
"""API endpoint for detail view of a ReturnOrderLineItem object"""
|
||||
"""API endpoint for detail view of a ReturnOrderLineItem object."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class ReturnOrderExtraLineList(GeneralExtraLineList, ListCreateAPI):
|
||||
"""API endpoint for accessing a list of ReturnOrderExtraLine objects"""
|
||||
"""API endpoint for accessing a list of ReturnOrderExtraLine objects."""
|
||||
|
||||
queryset = models.ReturnOrderExtraLine.objects.all()
|
||||
serializer_class = serializers.ReturnOrderExtraLineSerializer
|
||||
|
||||
def download_queryset(self, queryset, export_format):
|
||||
"""Download this queryset as a file"""
|
||||
"""Download this queryset as a file."""
|
||||
raise NotImplementedError('download_queryset not yet implemented')
|
||||
|
||||
|
||||
class ReturnOrderExtraLineDetail(RetrieveUpdateDestroyAPI):
|
||||
"""API endpoint for detail view of a ReturnOrderExtraLine object"""
|
||||
"""API endpoint for detail view of a ReturnOrderExtraLine object."""
|
||||
|
||||
queryset = models.ReturnOrderExtraLine.objects.all()
|
||||
serializer_class = serializers.ReturnOrderExtraLineSerializer
|
||||
|
||||
|
||||
class ReturnOrderAttachmentList(AttachmentMixin, ListCreateDestroyAPIView):
|
||||
"""API endpoint for listing (and creating) a ReturnOrderAttachment (file upload)"""
|
||||
"""API endpoint for listing (and creating) a ReturnOrderAttachment (file upload)."""
|
||||
|
||||
queryset = models.ReturnOrderAttachment.objects.all()
|
||||
serializer_class = serializers.ReturnOrderAttachmentSerializer
|
||||
@ -1323,14 +1323,14 @@ class ReturnOrderAttachmentList(AttachmentMixin, ListCreateDestroyAPIView):
|
||||
|
||||
|
||||
class ReturnOrderAttachmentDetail(AttachmentMixin, RetrieveUpdateDestroyAPI):
|
||||
"""Detail endpoint for the ReturnOrderAttachment model"""
|
||||
"""Detail endpoint for the ReturnOrderAttachment model."""
|
||||
|
||||
queryset = models.ReturnOrderAttachment.objects.all()
|
||||
serializer_class = serializers.ReturnOrderAttachmentSerializer
|
||||
|
||||
|
||||
class OrderCalendarExport(ICalFeed):
|
||||
"""Calendar export for Purchase/Sales Orders
|
||||
"""Calendar export for Purchase/Sales Orders.
|
||||
|
||||
Optional parameters:
|
||||
- include_completed: true/false
|
||||
@ -1390,7 +1390,7 @@ class OrderCalendarExport(ICalFeed):
|
||||
return response
|
||||
|
||||
def get_object(self, request, *args, **kwargs):
|
||||
"""This is where settings from the URL etc will be obtained"""
|
||||
"""This is where settings from the URL etc will be obtained."""
|
||||
# Help:
|
||||
# https://django.readthedocs.io/en/stable/ref/contrib/syndication.html
|
||||
|
||||
@ -1457,11 +1457,11 @@ class OrderCalendarExport(ICalFeed):
|
||||
return outlist
|
||||
|
||||
def item_title(self, item):
|
||||
"""Set the event title to the order reference"""
|
||||
"""Set the event title to the order reference."""
|
||||
return f'{item.reference}'
|
||||
|
||||
def item_description(self, item):
|
||||
"""Set the event description"""
|
||||
"""Set the event description."""
|
||||
return f'Company: {item.company.name}\nStatus: {item.get_status_display()}\nDescription: {item.description}'
|
||||
|
||||
def item_start_datetime(self, item):
|
||||
@ -1477,11 +1477,11 @@ class OrderCalendarExport(ICalFeed):
|
||||
return item.creation_date
|
||||
|
||||
def item_class(self, item):
|
||||
"""Set item class to PUBLIC"""
|
||||
"""Set item class to PUBLIC."""
|
||||
return 'PUBLIC'
|
||||
|
||||
def item_guid(self, item):
|
||||
"""Return globally unique UID for event"""
|
||||
"""Return globally unique UID for event."""
|
||||
return f'po_{item.pk}_{item.reference.replace(" ","-")}@{self.instance_url}'
|
||||
|
||||
def item_link(self, item):
|
||||
|
@ -1,9 +1,9 @@
|
||||
"""Config for the 'order' app"""
|
||||
"""Config for the 'order' app."""
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class OrderConfig(AppConfig):
|
||||
"""Configuration class for the 'order' app"""
|
||||
"""Configuration class for the 'order' app."""
|
||||
|
||||
name = 'order'
|
||||
|
@ -67,7 +67,7 @@ logger = logging.getLogger('inventree')
|
||||
|
||||
|
||||
class TotalPriceMixin(models.Model):
|
||||
"""Mixin which provides 'total_price' field for an order"""
|
||||
"""Mixin which provides 'total_price' field for an order."""
|
||||
|
||||
class Meta:
|
||||
"""Meta for MetadataMixin."""
|
||||
@ -75,7 +75,7 @@ class TotalPriceMixin(models.Model):
|
||||
abstract = True
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
"""Update the total_price field when saved"""
|
||||
"""Update the total_price field when saved."""
|
||||
# Recalculate total_price for this order
|
||||
self.update_total_price(commit=False)
|
||||
super().save(*args, **kwargs)
|
||||
@ -99,8 +99,9 @@ class TotalPriceMixin(models.Model):
|
||||
|
||||
@property
|
||||
def currency(self):
|
||||
"""Return the currency associated with this order instance:
|
||||
"""Return the currency associated with this order instance.
|
||||
|
||||
Rules:
|
||||
- If the order_currency field is set, return that
|
||||
- Otherwise, return the currency associated with the company
|
||||
- Finally, return the default currency code
|
||||
@ -115,7 +116,7 @@ class TotalPriceMixin(models.Model):
|
||||
return currency_code_default()
|
||||
|
||||
def update_total_price(self, commit=True):
|
||||
"""Recalculate and save the total_price for this order"""
|
||||
"""Recalculate and save the total_price for this order."""
|
||||
self.total_price = self.calculate_total_price(target_currency=self.currency)
|
||||
|
||||
if commit:
|
||||
@ -205,7 +206,7 @@ class Order(
|
||||
abstract = True
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
"""Custom save method for the order models:
|
||||
"""Custom save method for the order models.
|
||||
|
||||
Ensures that the reference field is rebuilt whenever the instance is saved.
|
||||
"""
|
||||
@ -217,7 +218,7 @@ class Order(
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def clean(self):
|
||||
"""Custom clean method for the generic order class"""
|
||||
"""Custom clean method for the generic order class."""
|
||||
super().clean()
|
||||
|
||||
# Check that the referenced 'contact' matches the correct 'company'
|
||||
@ -229,7 +230,7 @@ class Order(
|
||||
|
||||
@classmethod
|
||||
def overdue_filter(cls):
|
||||
"""A generic implementation of an 'overdue' filter for the Model class
|
||||
"""A generic implementation of an 'overdue' filter for the Model class.
|
||||
|
||||
It requires any subclasses to implement the get_status_class() class method
|
||||
"""
|
||||
@ -326,7 +327,7 @@ class Order(
|
||||
|
||||
@classmethod
|
||||
def get_status_class(cls):
|
||||
"""Return the enumeration class which represents the 'status' field for this model"""
|
||||
"""Return the enumeration class which represents the 'status' field for this model."""
|
||||
raise NotImplementedError(f'get_status_class() not implemented for {__class__}')
|
||||
|
||||
|
||||
@ -341,22 +342,22 @@ class PurchaseOrder(TotalPriceMixin, Order):
|
||||
"""
|
||||
|
||||
def get_absolute_url(self):
|
||||
"""Get the 'web' URL for this order"""
|
||||
"""Get the 'web' URL for this order."""
|
||||
return reverse('po-detail', kwargs={'pk': self.pk})
|
||||
|
||||
@staticmethod
|
||||
def get_api_url():
|
||||
"""Return the API URL associated with the PurchaseOrder model"""
|
||||
"""Return the API URL associated with the PurchaseOrder model."""
|
||||
return reverse('api-po-list')
|
||||
|
||||
@classmethod
|
||||
def get_status_class(cls):
|
||||
"""Return the PurchasOrderStatus class"""
|
||||
"""Return the PurchasOrderStatus class."""
|
||||
return PurchaseOrderStatusGroups
|
||||
|
||||
@classmethod
|
||||
def api_defaults(cls, request):
|
||||
"""Return default values for this model when issuing an API OPTIONS request"""
|
||||
"""Return default values for this model when issuing an API OPTIONS request."""
|
||||
defaults = {
|
||||
'reference': order.validators.generate_next_purchase_order_reference()
|
||||
}
|
||||
@ -411,7 +412,7 @@ class PurchaseOrder(TotalPriceMixin, Order):
|
||||
return queryset
|
||||
|
||||
def __str__(self):
|
||||
"""Render a string representation of this PurchaseOrder"""
|
||||
"""Render a string representation of this PurchaseOrder."""
|
||||
return f"{self.reference} - {self.supplier.name if self.supplier else _('deleted')}"
|
||||
|
||||
reference = models.CharField(
|
||||
@ -432,7 +433,7 @@ class PurchaseOrder(TotalPriceMixin, Order):
|
||||
|
||||
@property
|
||||
def status_text(self):
|
||||
"""Return the text representation of the status field"""
|
||||
"""Return the text representation of the status field."""
|
||||
return PurchaseOrderStatus.text(self.status)
|
||||
|
||||
supplier = models.ForeignKey(
|
||||
@ -447,7 +448,7 @@ class PurchaseOrder(TotalPriceMixin, Order):
|
||||
|
||||
@property
|
||||
def company(self):
|
||||
"""Accessor helper for Order base class"""
|
||||
"""Accessor helper for Order base class."""
|
||||
return self.supplier
|
||||
|
||||
supplier_reference = models.CharField(
|
||||
@ -617,12 +618,12 @@ class PurchaseOrder(TotalPriceMixin, Order):
|
||||
|
||||
@property
|
||||
def is_pending(self):
|
||||
"""Return True if the PurchaseOrder is 'pending'"""
|
||||
"""Return True if the PurchaseOrder is 'pending'."""
|
||||
return self.status == PurchaseOrderStatus.PENDING.value
|
||||
|
||||
@property
|
||||
def is_open(self):
|
||||
"""Return True if the PurchaseOrder is 'open'"""
|
||||
"""Return True if the PurchaseOrder is 'open'."""
|
||||
return self.status in PurchaseOrderStatusGroups.OPEN
|
||||
|
||||
@property
|
||||
@ -668,17 +669,17 @@ class PurchaseOrder(TotalPriceMixin, Order):
|
||||
|
||||
@property
|
||||
def line_count(self):
|
||||
"""Return the total number of line items associated with this order"""
|
||||
"""Return the total number of line items associated with this order."""
|
||||
return self.lines.count()
|
||||
|
||||
@property
|
||||
def completed_line_count(self):
|
||||
"""Return the number of complete line items associated with this order"""
|
||||
"""Return the number of complete line items associated with this order."""
|
||||
return self.completed_line_items().count()
|
||||
|
||||
@property
|
||||
def pending_line_count(self):
|
||||
"""Return the number of pending line items associated with this order"""
|
||||
"""Return the number of pending line items associated with this order."""
|
||||
return self.pending_line_items().count()
|
||||
|
||||
@property
|
||||
@ -794,22 +795,22 @@ class SalesOrder(TotalPriceMixin, Order):
|
||||
"""A SalesOrder represents a list of goods shipped outwards to a customer."""
|
||||
|
||||
def get_absolute_url(self):
|
||||
"""Get the 'web' URL for this order"""
|
||||
"""Get the 'web' URL for this order."""
|
||||
return reverse('so-detail', kwargs={'pk': self.pk})
|
||||
|
||||
@staticmethod
|
||||
def get_api_url():
|
||||
"""Return the API URL associated with the SalesOrder model"""
|
||||
"""Return the API URL associated with the SalesOrder model."""
|
||||
return reverse('api-so-list')
|
||||
|
||||
@classmethod
|
||||
def get_status_class(cls):
|
||||
"""Return the SalesOrderStatus class"""
|
||||
"""Return the SalesOrderStatus class."""
|
||||
return SalesOrderStatusGroups
|
||||
|
||||
@classmethod
|
||||
def api_defaults(cls, request):
|
||||
"""Return default values for this model when issuing an API OPTIONS request"""
|
||||
"""Return default values for this model when issuing an API OPTIONS request."""
|
||||
defaults = {'reference': order.validators.generate_next_sales_order_reference()}
|
||||
|
||||
return defaults
|
||||
@ -862,7 +863,7 @@ class SalesOrder(TotalPriceMixin, Order):
|
||||
return queryset
|
||||
|
||||
def __str__(self):
|
||||
"""Render a string representation of this SalesOrder"""
|
||||
"""Render a string representation of this SalesOrder."""
|
||||
return f"{self.reference} - {self.customer.name if self.customer else _('deleted')}"
|
||||
|
||||
reference = models.CharField(
|
||||
@ -887,7 +888,7 @@ class SalesOrder(TotalPriceMixin, Order):
|
||||
|
||||
@property
|
||||
def company(self):
|
||||
"""Accessor helper for Order base"""
|
||||
"""Accessor helper for Order base."""
|
||||
return self.customer
|
||||
|
||||
status = models.PositiveIntegerField(
|
||||
@ -899,7 +900,7 @@ class SalesOrder(TotalPriceMixin, Order):
|
||||
|
||||
@property
|
||||
def status_text(self):
|
||||
"""Return the text representation of the status field"""
|
||||
"""Return the text representation of the status field."""
|
||||
return SalesOrderStatus.text(self.status)
|
||||
|
||||
customer_reference = models.CharField(
|
||||
@ -924,12 +925,12 @@ class SalesOrder(TotalPriceMixin, Order):
|
||||
|
||||
@property
|
||||
def is_pending(self):
|
||||
"""Return True if this order is 'pending'"""
|
||||
"""Return True if this order is 'pending'."""
|
||||
return self.status == SalesOrderStatus.PENDING
|
||||
|
||||
@property
|
||||
def is_open(self):
|
||||
"""Return True if this order is 'open' (either 'pending' or 'in_progress')"""
|
||||
"""Return True if this order is 'open' (either 'pending' or 'in_progress')."""
|
||||
return self.status in SalesOrderStatusGroups.OPEN
|
||||
|
||||
@property
|
||||
@ -997,11 +998,11 @@ class SalesOrder(TotalPriceMixin, Order):
|
||||
|
||||
# region state changes
|
||||
def place_order(self):
|
||||
"""Deprecated version of 'issue_order'"""
|
||||
"""Deprecated version of 'issue_order'."""
|
||||
self.issue_order()
|
||||
|
||||
def _action_place(self, *args, **kwargs):
|
||||
"""Change this order from 'PENDING' to 'IN_PROGRESS'"""
|
||||
"""Change this order from 'PENDING' to 'IN_PROGRESS'."""
|
||||
if self.status == SalesOrderStatus.PENDING:
|
||||
self.status = SalesOrderStatus.IN_PROGRESS.value
|
||||
self.issue_date = datetime.now().date()
|
||||
@ -1094,7 +1095,7 @@ class SalesOrder(TotalPriceMixin, Order):
|
||||
|
||||
@property
|
||||
def line_count(self):
|
||||
"""Return the total number of lines associated with this order"""
|
||||
"""Return the total number of lines associated with this order."""
|
||||
return self.lines.count()
|
||||
|
||||
def completed_line_items(self):
|
||||
@ -1107,12 +1108,12 @@ class SalesOrder(TotalPriceMixin, Order):
|
||||
|
||||
@property
|
||||
def completed_line_count(self):
|
||||
"""Return the number of completed lines for this order"""
|
||||
"""Return the number of completed lines for this order."""
|
||||
return self.completed_line_items().count()
|
||||
|
||||
@property
|
||||
def pending_line_count(self):
|
||||
"""Return the number of pending (incomplete) lines associated with this order"""
|
||||
"""Return the number of pending (incomplete) lines associated with this order."""
|
||||
return self.pending_line_items().count()
|
||||
|
||||
def completed_shipments(self):
|
||||
@ -1125,17 +1126,17 @@ class SalesOrder(TotalPriceMixin, Order):
|
||||
|
||||
@property
|
||||
def shipment_count(self):
|
||||
"""Return the total number of shipments associated with this order"""
|
||||
"""Return the total number of shipments associated with this order."""
|
||||
return self.shipments.count()
|
||||
|
||||
@property
|
||||
def completed_shipment_count(self):
|
||||
"""Return the number of completed shipments associated with this order"""
|
||||
"""Return the number of completed shipments associated with this order."""
|
||||
return self.completed_shipments().count()
|
||||
|
||||
@property
|
||||
def pending_shipment_count(self):
|
||||
"""Return the number of pending shipments associated with this order"""
|
||||
"""Return the number of pending shipments associated with this order."""
|
||||
return self.pending_shipments().count()
|
||||
|
||||
|
||||
@ -1169,11 +1170,11 @@ class PurchaseOrderAttachment(InvenTreeAttachment):
|
||||
|
||||
@staticmethod
|
||||
def get_api_url():
|
||||
"""Return the API URL associated with the PurchaseOrderAttachment model"""
|
||||
"""Return the API URL associated with the PurchaseOrderAttachment model."""
|
||||
return reverse('api-po-attachment-list')
|
||||
|
||||
def getSubdir(self):
|
||||
"""Return the directory path where PurchaseOrderAttachment files are located"""
|
||||
"""Return the directory path where PurchaseOrderAttachment files are located."""
|
||||
return os.path.join('po_files', str(self.order.id))
|
||||
|
||||
order = models.ForeignKey(
|
||||
@ -1186,11 +1187,11 @@ class SalesOrderAttachment(InvenTreeAttachment):
|
||||
|
||||
@staticmethod
|
||||
def get_api_url():
|
||||
"""Return the API URL associated with the SalesOrderAttachment class"""
|
||||
"""Return the API URL associated with the SalesOrderAttachment class."""
|
||||
return reverse('api-so-attachment-list')
|
||||
|
||||
def getSubdir(self):
|
||||
"""Return the directory path where SalesOrderAttachment files are located"""
|
||||
"""Return the directory path where SalesOrderAttachment files are located."""
|
||||
return os.path.join('so_files', str(self.order.id))
|
||||
|
||||
order = models.ForeignKey(
|
||||
@ -1214,7 +1215,7 @@ class OrderLineItem(MetadataMixin, models.Model):
|
||||
abstract = True
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
"""Custom save method for the OrderLineItem model
|
||||
"""Custom save method for the OrderLineItem model.
|
||||
|
||||
Calls save method on the linked order
|
||||
"""
|
||||
@ -1222,7 +1223,7 @@ class OrderLineItem(MetadataMixin, models.Model):
|
||||
self.order.save()
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
"""Custom delete method for the OrderLineItem model
|
||||
"""Custom delete method for the OrderLineItem model.
|
||||
|
||||
Calls save method on the linked order
|
||||
"""
|
||||
@ -1240,7 +1241,7 @@ class OrderLineItem(MetadataMixin, models.Model):
|
||||
|
||||
@property
|
||||
def total_line_price(self):
|
||||
"""Return the total price for this line item"""
|
||||
"""Return the total price for this line item."""
|
||||
if self.price:
|
||||
return self.quantity * self.price
|
||||
|
||||
@ -1325,13 +1326,13 @@ class PurchaseOrderLineItem(OrderLineItem):
|
||||
|
||||
@staticmethod
|
||||
def get_api_url():
|
||||
"""Return the API URL associated with the PurchaseOrderLineItem model"""
|
||||
"""Return the API URL associated with the PurchaseOrderLineItem model."""
|
||||
return reverse('api-po-line-list')
|
||||
|
||||
def clean(self):
|
||||
"""Custom clean method for the PurchaseOrderLineItem model:
|
||||
"""Custom clean method for the PurchaseOrderLineItem model.
|
||||
|
||||
- Ensure the supplier part matches the supplier
|
||||
Ensure the supplier part matches the supplier
|
||||
"""
|
||||
super().clean()
|
||||
|
||||
@ -1341,7 +1342,7 @@ class PurchaseOrderLineItem(OrderLineItem):
|
||||
raise ValidationError({'part': _('Supplier part must match supplier')})
|
||||
|
||||
def __str__(self):
|
||||
"""Render a string representation of a PurchaseOrderLineItem instance"""
|
||||
"""Render a string representation of a PurchaseOrderLineItem instance."""
|
||||
return '{n} x {part} from {supplier} (for {po})'.format(
|
||||
n=decimal2string(self.quantity),
|
||||
part=self.part.SKU if self.part else 'unknown part',
|
||||
@ -1395,7 +1396,7 @@ class PurchaseOrderLineItem(OrderLineItem):
|
||||
|
||||
@property
|
||||
def price(self):
|
||||
"""Return the 'purchase_price' field as 'price'"""
|
||||
"""Return the 'purchase_price' field as 'price'."""
|
||||
return self.purchase_price
|
||||
|
||||
destination = TreeForeignKey(
|
||||
@ -1442,7 +1443,7 @@ class PurchaseOrderExtraLine(OrderExtraLine):
|
||||
|
||||
@staticmethod
|
||||
def get_api_url():
|
||||
"""Return the API URL associated with the PurchaseOrderExtraLine model"""
|
||||
"""Return the API URL associated with the PurchaseOrderExtraLine model."""
|
||||
return reverse('api-po-extra-line-list')
|
||||
|
||||
order = models.ForeignKey(
|
||||
@ -1473,11 +1474,11 @@ class SalesOrderLineItem(OrderLineItem):
|
||||
|
||||
@staticmethod
|
||||
def get_api_url():
|
||||
"""Return the API URL associated with the SalesOrderLineItem model"""
|
||||
"""Return the API URL associated with the SalesOrderLineItem model."""
|
||||
return reverse('api-so-line-list')
|
||||
|
||||
def clean(self):
|
||||
"""Perform extra validation steps for this SalesOrderLineItem instance"""
|
||||
"""Perform extra validation steps for this SalesOrderLineItem instance."""
|
||||
super().clean()
|
||||
|
||||
if self.part:
|
||||
@ -1520,7 +1521,7 @@ class SalesOrderLineItem(OrderLineItem):
|
||||
|
||||
@property
|
||||
def price(self):
|
||||
"""Return the 'sale_price' field as 'price'"""
|
||||
"""Return the 'sale_price' field as 'price'."""
|
||||
return self.sale_price
|
||||
|
||||
shipped = RoundingDecimalField(
|
||||
@ -1583,14 +1584,14 @@ class SalesOrderShipment(InvenTreeNotesMixin, MetadataMixin, models.Model):
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass defines extra model options"""
|
||||
"""Metaclass defines extra model options."""
|
||||
|
||||
# Shipment reference must be unique for a given sales order
|
||||
unique_together = ['order', 'reference']
|
||||
|
||||
@staticmethod
|
||||
def get_api_url():
|
||||
"""Return the API URL associated with the SalesOrderShipment model"""
|
||||
"""Return the API URL associated with the SalesOrderShipment model."""
|
||||
return reverse('api-so-shipment-list')
|
||||
|
||||
order = models.ForeignKey(
|
||||
@ -1656,15 +1657,15 @@ class SalesOrderShipment(InvenTreeNotesMixin, MetadataMixin, models.Model):
|
||||
)
|
||||
|
||||
def is_complete(self):
|
||||
"""Return True if this shipment has already been completed"""
|
||||
"""Return True if this shipment has already been completed."""
|
||||
return self.shipment_date is not None
|
||||
|
||||
def is_delivered(self):
|
||||
"""Return True if this shipment has already been delivered"""
|
||||
"""Return True if this shipment has already been delivered."""
|
||||
return self.delivery_date is not None
|
||||
|
||||
def check_can_complete(self, raise_error=True):
|
||||
"""Check if this shipment is able to be completed"""
|
||||
"""Check if this shipment is able to be completed."""
|
||||
try:
|
||||
if self.shipment_date:
|
||||
# Shipment has already been sent!
|
||||
@ -1744,7 +1745,7 @@ class SalesOrderExtraLine(OrderExtraLine):
|
||||
|
||||
@staticmethod
|
||||
def get_api_url():
|
||||
"""Return the API URL associated with the SalesOrderExtraLine model"""
|
||||
"""Return the API URL associated with the SalesOrderExtraLine model."""
|
||||
return reverse('api-so-extra-line-list')
|
||||
|
||||
order = models.ForeignKey(
|
||||
@ -1768,7 +1769,7 @@ class SalesOrderAllocation(models.Model):
|
||||
|
||||
@staticmethod
|
||||
def get_api_url():
|
||||
"""Return the API URL associated with the SalesOrderAllocation model"""
|
||||
"""Return the API URL associated with the SalesOrderAllocation model."""
|
||||
return reverse('api-so-allocation-list')
|
||||
|
||||
def clean(self):
|
||||
@ -1869,11 +1870,11 @@ class SalesOrderAllocation(models.Model):
|
||||
)
|
||||
|
||||
def get_location(self):
|
||||
"""Return the <pk> value of the location associated with this allocation"""
|
||||
"""Return the <pk> value of the location associated with this allocation."""
|
||||
return self.item.location.id if self.item.location else None
|
||||
|
||||
def get_po(self):
|
||||
"""Return the PurchaseOrder associated with this allocation"""
|
||||
"""Return the PurchaseOrder associated with this allocation."""
|
||||
return self.item.purchase_order
|
||||
|
||||
def complete_allocation(self, user):
|
||||
@ -1900,7 +1901,7 @@ class SalesOrderAllocation(models.Model):
|
||||
|
||||
|
||||
class ReturnOrder(TotalPriceMixin, Order):
|
||||
"""A ReturnOrder represents goods returned from a customer, e.g. an RMA or warranty
|
||||
"""A ReturnOrder represents goods returned from a customer, e.g. an RMA or warranty.
|
||||
|
||||
Attributes:
|
||||
customer: Reference to the customer
|
||||
@ -1909,22 +1910,22 @@ class ReturnOrder(TotalPriceMixin, Order):
|
||||
"""
|
||||
|
||||
def get_absolute_url(self):
|
||||
"""Get the 'web' URL for this order"""
|
||||
"""Get the 'web' URL for this order."""
|
||||
return reverse('return-order-detail', kwargs={'pk': self.pk})
|
||||
|
||||
@staticmethod
|
||||
def get_api_url():
|
||||
"""Return the API URL associated with the ReturnOrder model"""
|
||||
"""Return the API URL associated with the ReturnOrder model."""
|
||||
return reverse('api-return-order-list')
|
||||
|
||||
@classmethod
|
||||
def get_status_class(cls):
|
||||
"""Return the ReturnOrderStatus class"""
|
||||
"""Return the ReturnOrderStatus class."""
|
||||
return ReturnOrderStatusGroups
|
||||
|
||||
@classmethod
|
||||
def api_defaults(cls, request):
|
||||
"""Return default values for this model when issuing an API OPTIONS request"""
|
||||
"""Return default values for this model when issuing an API OPTIONS request."""
|
||||
defaults = {
|
||||
'reference': order.validators.generate_next_return_order_reference()
|
||||
}
|
||||
@ -1934,7 +1935,7 @@ class ReturnOrder(TotalPriceMixin, Order):
|
||||
REFERENCE_PATTERN_SETTING = 'RETURNORDER_REFERENCE_PATTERN'
|
||||
|
||||
def __str__(self):
|
||||
"""Render a string representation of this ReturnOrder"""
|
||||
"""Render a string representation of this ReturnOrder."""
|
||||
return f"{self.reference} - {self.customer.name if self.customer else _('no customer')}"
|
||||
|
||||
reference = models.CharField(
|
||||
@ -1959,7 +1960,7 @@ class ReturnOrder(TotalPriceMixin, Order):
|
||||
|
||||
@property
|
||||
def company(self):
|
||||
"""Accessor helper for Order base class"""
|
||||
"""Accessor helper for Order base class."""
|
||||
return self.customer
|
||||
|
||||
status = models.PositiveIntegerField(
|
||||
@ -1993,21 +1994,21 @@ class ReturnOrder(TotalPriceMixin, Order):
|
||||
# region state changes
|
||||
@property
|
||||
def is_pending(self):
|
||||
"""Return True if this order is pending"""
|
||||
"""Return True if this order is pending."""
|
||||
return self.status == ReturnOrderStatus.PENDING
|
||||
|
||||
@property
|
||||
def is_open(self):
|
||||
"""Return True if this order is outstanding"""
|
||||
"""Return True if this order is outstanding."""
|
||||
return self.status in ReturnOrderStatusGroups.OPEN
|
||||
|
||||
@property
|
||||
def is_received(self):
|
||||
"""Return True if this order is fully received"""
|
||||
"""Return True if this order is fully received."""
|
||||
return not self.lines.filter(received_date=None).exists()
|
||||
|
||||
def _action_cancel(self, *args, **kwargs):
|
||||
"""Cancel this ReturnOrder (if not already cancelled)"""
|
||||
"""Cancel this ReturnOrder (if not already cancelled)."""
|
||||
if self.status != ReturnOrderStatus.CANCELLED:
|
||||
self.status = ReturnOrderStatus.CANCELLED.value
|
||||
self.save()
|
||||
@ -2023,7 +2024,7 @@ class ReturnOrder(TotalPriceMixin, Order):
|
||||
)
|
||||
|
||||
def _action_complete(self, *args, **kwargs):
|
||||
"""Complete this ReturnOrder (if not already completed)"""
|
||||
"""Complete this ReturnOrder (if not already completed)."""
|
||||
if self.status == ReturnOrderStatus.IN_PROGRESS:
|
||||
self.status = ReturnOrderStatus.COMPLETE.value
|
||||
self.complete_date = datetime.now().date()
|
||||
@ -2032,11 +2033,11 @@ class ReturnOrder(TotalPriceMixin, Order):
|
||||
trigger_event('returnorder.completed', id=self.pk)
|
||||
|
||||
def place_order(self):
|
||||
"""Deprecated version of 'issue_order"""
|
||||
"""Deprecated version of 'issue_order."""
|
||||
self.issue_order()
|
||||
|
||||
def _action_place(self, *args, **kwargs):
|
||||
"""Issue this ReturnOrder (if currently pending)"""
|
||||
"""Issue this ReturnOrder (if currently pending)."""
|
||||
if self.status == ReturnOrderStatus.PENDING:
|
||||
self.status = ReturnOrderStatus.IN_PROGRESS.value
|
||||
self.issue_date = datetime.now().date()
|
||||
@ -2069,8 +2070,9 @@ class ReturnOrder(TotalPriceMixin, Order):
|
||||
|
||||
@transaction.atomic
|
||||
def receive_line_item(self, line, location, user, note=''):
|
||||
"""Receive a line item against this ReturnOrder:
|
||||
"""Receive a line item against this ReturnOrder.
|
||||
|
||||
Rules:
|
||||
- Transfers the StockItem to the specified location
|
||||
- Marks the StockItem as "quarantined"
|
||||
- Adds a tracking entry to the StockItem
|
||||
@ -2126,20 +2128,20 @@ class ReturnOrder(TotalPriceMixin, Order):
|
||||
|
||||
|
||||
class ReturnOrderLineItem(OrderLineItem):
|
||||
"""Model for a single LineItem in a ReturnOrder"""
|
||||
"""Model for a single LineItem in a ReturnOrder."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options for this model"""
|
||||
"""Metaclass options for this model."""
|
||||
|
||||
unique_together = [('order', 'item')]
|
||||
|
||||
@staticmethod
|
||||
def get_api_url():
|
||||
"""Return the API URL associated with this model"""
|
||||
"""Return the API URL associated with this model."""
|
||||
return reverse('api-return-order-line-list')
|
||||
|
||||
def clean(self):
|
||||
"""Perform extra validation steps for the ReturnOrderLineItem model"""
|
||||
"""Perform extra validation steps for the ReturnOrderLineItem model."""
|
||||
super().clean()
|
||||
|
||||
if self.item and not self.item.serialized:
|
||||
@ -2172,7 +2174,7 @@ class ReturnOrderLineItem(OrderLineItem):
|
||||
|
||||
@property
|
||||
def received(self):
|
||||
"""Return True if this item has been received"""
|
||||
"""Return True if this item has been received."""
|
||||
return self.received_date is not None
|
||||
|
||||
outcome = models.PositiveIntegerField(
|
||||
@ -2191,11 +2193,11 @@ class ReturnOrderLineItem(OrderLineItem):
|
||||
|
||||
|
||||
class ReturnOrderExtraLine(OrderExtraLine):
|
||||
"""Model for a single ExtraLine in a ReturnOrder"""
|
||||
"""Model for a single ExtraLine in a ReturnOrder."""
|
||||
|
||||
@staticmethod
|
||||
def get_api_url():
|
||||
"""Return the API URL associated with the ReturnOrderExtraLine model"""
|
||||
"""Return the API URL associated with the ReturnOrderExtraLine model."""
|
||||
return reverse('api-return-order-extra-line-list')
|
||||
|
||||
order = models.ForeignKey(
|
||||
@ -2208,15 +2210,15 @@ class ReturnOrderExtraLine(OrderExtraLine):
|
||||
|
||||
|
||||
class ReturnOrderAttachment(InvenTreeAttachment):
|
||||
"""Model for storing file attachments against a ReturnOrder object"""
|
||||
"""Model for storing file attachments against a ReturnOrder object."""
|
||||
|
||||
@staticmethod
|
||||
def get_api_url():
|
||||
"""Return the API URL associated with the ReturnOrderAttachment class"""
|
||||
"""Return the API URL associated with the ReturnOrderAttachment class."""
|
||||
return reverse('api-return-order-attachment-list')
|
||||
|
||||
def getSubdir(self):
|
||||
"""Return the directory path where ReturnOrderAttachment files are located"""
|
||||
"""Return the directory path where ReturnOrderAttachment files are located."""
|
||||
return os.path.join('return_files', str(self.order.id))
|
||||
|
||||
order = models.ForeignKey(
|
||||
|
@ -43,7 +43,7 @@ from users.serializers import OwnerSerializer
|
||||
|
||||
|
||||
class TotalPriceMixin(serializers.Serializer):
|
||||
"""Serializer mixin which provides total price fields"""
|
||||
"""Serializer mixin which provides total price fields."""
|
||||
|
||||
total_price = InvenTreeMoneySerializer(allow_null=True, read_only=True)
|
||||
|
||||
@ -57,7 +57,7 @@ class TotalPriceMixin(serializers.Serializer):
|
||||
|
||||
|
||||
class AbstractOrderSerializer(serializers.Serializer):
|
||||
"""Abstract serializer class which provides fields common to all order types"""
|
||||
"""Abstract serializer class which provides fields common to all order types."""
|
||||
|
||||
# Number of line items in this order
|
||||
line_items = serializers.IntegerField(read_only=True)
|
||||
@ -98,20 +98,20 @@ class AbstractOrderSerializer(serializers.Serializer):
|
||||
barcode_hash = serializers.CharField(read_only=True)
|
||||
|
||||
def validate_reference(self, reference):
|
||||
"""Custom validation for the reference field"""
|
||||
"""Custom validation for the reference field."""
|
||||
self.Meta.model.validate_reference_field(reference)
|
||||
return reference
|
||||
|
||||
@staticmethod
|
||||
def annotate_queryset(queryset):
|
||||
"""Add extra information to the queryset"""
|
||||
"""Add extra information to the queryset."""
|
||||
queryset = queryset.annotate(line_items=SubqueryCount('lines'))
|
||||
|
||||
return queryset
|
||||
|
||||
@staticmethod
|
||||
def order_fields(extra_fields):
|
||||
"""Construct a set of fields for this serializer"""
|
||||
"""Construct a set of fields for this serializer."""
|
||||
return [
|
||||
'pk',
|
||||
'creation_date',
|
||||
@ -141,7 +141,7 @@ class AbstractExtraLineSerializer(serializers.Serializer):
|
||||
"""Abstract Serializer for a ExtraLine object."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialization routine for the serializer"""
|
||||
"""Initialization routine for the serializer."""
|
||||
order_detail = kwargs.pop('order_detail', False)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
@ -202,7 +202,7 @@ class PurchaseOrderSerializer(
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialization routine for the serializer"""
|
||||
"""Initialization routine for the serializer."""
|
||||
supplier_detail = kwargs.pop('supplier_detail', False)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
@ -257,7 +257,7 @@ class PurchaseOrderCancelSerializer(serializers.Serializer):
|
||||
return {'can_cancel': self.order.can_cancel}
|
||||
|
||||
def save(self):
|
||||
"""Save the serializer to 'cancel' the order"""
|
||||
"""Save the serializer to 'cancel' the order."""
|
||||
order = self.context['order']
|
||||
|
||||
if not order.can_cancel:
|
||||
@ -282,7 +282,7 @@ class PurchaseOrderCompleteSerializer(serializers.Serializer):
|
||||
)
|
||||
|
||||
def validate_accept_incomplete(self, value):
|
||||
"""Check if the 'accept_incomplete' field is required"""
|
||||
"""Check if the 'accept_incomplete' field is required."""
|
||||
order = self.context['order']
|
||||
|
||||
if not value and not order.is_complete:
|
||||
@ -297,7 +297,7 @@ class PurchaseOrderCompleteSerializer(serializers.Serializer):
|
||||
return {'is_complete': order.is_complete}
|
||||
|
||||
def save(self):
|
||||
"""Save the serializer to 'complete' the order"""
|
||||
"""Save the serializer to 'complete' the order."""
|
||||
order = self.context['order']
|
||||
order.complete_order()
|
||||
|
||||
@ -311,13 +311,13 @@ class PurchaseOrderIssueSerializer(serializers.Serializer):
|
||||
fields = []
|
||||
|
||||
def save(self):
|
||||
"""Save the serializer to 'place' the order"""
|
||||
"""Save the serializer to 'place' the order."""
|
||||
order = self.context['order']
|
||||
order.place_order()
|
||||
|
||||
|
||||
class PurchaseOrderLineItemSerializer(InvenTreeModelSerializer):
|
||||
"""Serializer class for the PurchaseOrderLineItem model"""
|
||||
"""Serializer class for the PurchaseOrderLineItem model."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options."""
|
||||
@ -346,7 +346,7 @@ class PurchaseOrderLineItemSerializer(InvenTreeModelSerializer):
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialization routine for the serializer"""
|
||||
"""Initialization routine for the serializer."""
|
||||
part_detail = kwargs.pop('part_detail', False)
|
||||
|
||||
order_detail = kwargs.pop('order_detail', False)
|
||||
@ -362,10 +362,10 @@ class PurchaseOrderLineItemSerializer(InvenTreeModelSerializer):
|
||||
|
||||
@staticmethod
|
||||
def annotate_queryset(queryset):
|
||||
"""Add some extra annotations to this queryset:
|
||||
"""Add some extra annotations to this queryset.
|
||||
|
||||
- Total price = purchase_price * quantity
|
||||
- "Overdue" status (boolean field)
|
||||
- "total_price" = purchase_price * quantity
|
||||
- "overdue" status (boolean field)
|
||||
"""
|
||||
queryset = queryset.annotate(
|
||||
total_price=ExpressionWrapper(
|
||||
@ -388,14 +388,14 @@ class PurchaseOrderLineItemSerializer(InvenTreeModelSerializer):
|
||||
quantity = serializers.FloatField(min_value=0, required=True)
|
||||
|
||||
def validate_quantity(self, quantity):
|
||||
"""Validation for the 'quantity' field"""
|
||||
"""Validation for the 'quantity' field."""
|
||||
if quantity <= 0:
|
||||
raise ValidationError(_('Quantity must be greater than zero'))
|
||||
|
||||
return quantity
|
||||
|
||||
def validate_purchase_order(self, purchase_order):
|
||||
"""Validation for the 'purchase_order' field"""
|
||||
"""Validation for the 'purchase_order' field."""
|
||||
if purchase_order.status not in PurchaseOrderStatusGroups.OPEN:
|
||||
raise ValidationError(_('Order is not open'))
|
||||
|
||||
@ -428,7 +428,7 @@ class PurchaseOrderLineItemSerializer(InvenTreeModelSerializer):
|
||||
order_detail = PurchaseOrderSerializer(source='order', read_only=True, many=False)
|
||||
|
||||
def validate(self, data):
|
||||
"""Custom validation for the serializer:
|
||||
"""Custom validation for the serializer.
|
||||
|
||||
- Ensure the supplier_part field is supplied
|
||||
- Ensure the purchase_order field is supplied
|
||||
@ -495,7 +495,7 @@ class PurchaseOrderLineItemReceiveSerializer(serializers.Serializer):
|
||||
)
|
||||
|
||||
def validate_line_item(self, item):
|
||||
"""Validation for the 'line_item' field"""
|
||||
"""Validation for the 'line_item' field."""
|
||||
if item.order != self.context['order']:
|
||||
raise ValidationError(_('Line item does not match purchase order'))
|
||||
|
||||
@ -515,7 +515,7 @@ class PurchaseOrderLineItemReceiveSerializer(serializers.Serializer):
|
||||
)
|
||||
|
||||
def validate_quantity(self, quantity):
|
||||
"""Validation for the 'quantity' field"""
|
||||
"""Validation for the 'quantity' field."""
|
||||
if quantity <= 0:
|
||||
raise ValidationError(_('Quantity must be greater than zero'))
|
||||
|
||||
@ -564,7 +564,7 @@ class PurchaseOrderLineItemReceiveSerializer(serializers.Serializer):
|
||||
return barcode
|
||||
|
||||
def validate(self, data):
|
||||
"""Custom validation for the serializer:
|
||||
"""Custom validation for the serializer.
|
||||
|
||||
- Integer quantity must be provided for serialized stock
|
||||
- Validate serial numbers (if provided)
|
||||
@ -619,7 +619,7 @@ class PurchaseOrderReceiveSerializer(serializers.Serializer):
|
||||
)
|
||||
|
||||
def validate(self, data):
|
||||
"""Custom validation for the serializer:
|
||||
"""Custom validation for the serializer.
|
||||
|
||||
- Ensure line items are provided
|
||||
- Check that a location is specified
|
||||
@ -714,7 +714,7 @@ class PurchaseOrderAttachmentSerializer(InvenTreeAttachmentSerializer):
|
||||
class SalesOrderSerializer(
|
||||
TotalPriceMixin, AbstractOrderSerializer, InvenTreeModelSerializer
|
||||
):
|
||||
"""Serializer for the SalesOrder model class"""
|
||||
"""Serializer for the SalesOrder model class."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options."""
|
||||
@ -735,7 +735,7 @@ class SalesOrderSerializer(
|
||||
extra_kwargs = {'order_currency': {'required': False}}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialization routine for the serializer"""
|
||||
"""Initialization routine for the serializer."""
|
||||
customer_detail = kwargs.pop('customer_detail', False)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
@ -775,15 +775,15 @@ class SalesOrderSerializer(
|
||||
|
||||
|
||||
class SalesOrderIssueSerializer(serializers.Serializer):
|
||||
"""Serializer for issuing a SalesOrder"""
|
||||
"""Serializer for issuing a SalesOrder."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options"""
|
||||
"""Metaclass options."""
|
||||
|
||||
fields = []
|
||||
|
||||
def save(self):
|
||||
"""Save the serializer to 'issue' the order"""
|
||||
"""Save the serializer to 'issue' the order."""
|
||||
order = self.context['order']
|
||||
order.issue_order()
|
||||
|
||||
@ -818,7 +818,7 @@ class SalesOrderAllocationSerializer(InvenTreeModelSerializer):
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialization routine for the serializer"""
|
||||
"""Initialization routine for the serializer."""
|
||||
order_detail = kwargs.pop('order_detail', False)
|
||||
part_detail = kwargs.pop('part_detail', True)
|
||||
item_detail = kwargs.pop('item_detail', True)
|
||||
@ -901,7 +901,7 @@ class SalesOrderLineItemSerializer(InvenTreeModelSerializer):
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialization routine for the serializer:
|
||||
"""Initialization routine for the serializer.
|
||||
|
||||
- Add extra related serializer information if required
|
||||
"""
|
||||
@ -926,7 +926,7 @@ class SalesOrderLineItemSerializer(InvenTreeModelSerializer):
|
||||
|
||||
@staticmethod
|
||||
def annotate_queryset(queryset):
|
||||
"""Add some extra annotations to this queryset:
|
||||
"""Add some extra annotations to this queryset.
|
||||
|
||||
- "overdue" status (boolean field)
|
||||
- "available_quantity"
|
||||
@ -1066,7 +1066,7 @@ class SalesOrderShipmentCompleteSerializer(serializers.ModelSerializer):
|
||||
]
|
||||
|
||||
def validate(self, data):
|
||||
"""Custom validation for the serializer:
|
||||
"""Custom validation for the serializer.
|
||||
|
||||
- Ensure the shipment reference is provided
|
||||
"""
|
||||
@ -1082,7 +1082,7 @@ class SalesOrderShipmentCompleteSerializer(serializers.ModelSerializer):
|
||||
return data
|
||||
|
||||
def save(self):
|
||||
"""Save the serializer to complete the SalesOrderShipment"""
|
||||
"""Save the serializer to complete the SalesOrderShipment."""
|
||||
shipment = self.context.get('shipment', None)
|
||||
|
||||
if not shipment:
|
||||
@ -1127,7 +1127,7 @@ class SalesOrderShipmentAllocationItemSerializer(serializers.Serializer):
|
||||
)
|
||||
|
||||
def validate_line_item(self, line_item):
|
||||
"""Custom validation for the 'line_item' field:
|
||||
"""Custom validation for the 'line_item' field.
|
||||
|
||||
- Ensure the line_item is associated with the particular SalesOrder
|
||||
"""
|
||||
@ -1152,14 +1152,14 @@ class SalesOrderShipmentAllocationItemSerializer(serializers.Serializer):
|
||||
)
|
||||
|
||||
def validate_quantity(self, quantity):
|
||||
"""Custom validation for the 'quantity' field"""
|
||||
"""Custom validation for the 'quantity' field."""
|
||||
if quantity <= 0:
|
||||
raise ValidationError(_('Quantity must be positive'))
|
||||
|
||||
return quantity
|
||||
|
||||
def validate(self, data):
|
||||
"""Custom validation for the serializer:
|
||||
"""Custom validation for the serializer.
|
||||
|
||||
- Ensure that the quantity is 1 for serialized stock
|
||||
- Quantity cannot exceed the available amount
|
||||
@ -1193,7 +1193,7 @@ class SalesOrderCompleteSerializer(serializers.Serializer):
|
||||
)
|
||||
|
||||
def validate_accept_incomplete(self, value):
|
||||
"""Check if the 'accept_incomplete' field is required"""
|
||||
"""Check if the 'accept_incomplete' field is required."""
|
||||
order = self.context['order']
|
||||
|
||||
if not value and not order.is_completed():
|
||||
@ -1202,7 +1202,7 @@ class SalesOrderCompleteSerializer(serializers.Serializer):
|
||||
return value
|
||||
|
||||
def get_context_data(self):
|
||||
"""Custom context data for this serializer"""
|
||||
"""Custom context data for this serializer."""
|
||||
order = self.context['order']
|
||||
|
||||
return {
|
||||
@ -1211,7 +1211,7 @@ class SalesOrderCompleteSerializer(serializers.Serializer):
|
||||
}
|
||||
|
||||
def validate(self, data):
|
||||
"""Custom validation for the serializer"""
|
||||
"""Custom validation for the serializer."""
|
||||
data = super().validate(data)
|
||||
|
||||
order = self.context['order']
|
||||
@ -1224,7 +1224,7 @@ class SalesOrderCompleteSerializer(serializers.Serializer):
|
||||
return data
|
||||
|
||||
def save(self):
|
||||
"""Save the serializer to complete the SalesOrder"""
|
||||
"""Save the serializer to complete the SalesOrder."""
|
||||
request = self.context['request']
|
||||
order = self.context['order']
|
||||
data = self.validated_data
|
||||
@ -1240,13 +1240,13 @@ class SalesOrderCancelSerializer(serializers.Serializer):
|
||||
"""Serializer for marking a SalesOrder as cancelled."""
|
||||
|
||||
def get_context_data(self):
|
||||
"""Add extra context data to the serializer"""
|
||||
"""Add extra context data to the serializer."""
|
||||
order = self.context['order']
|
||||
|
||||
return {'can_cancel': order.can_cancel}
|
||||
|
||||
def save(self):
|
||||
"""Save the serializer to cancel the order"""
|
||||
"""Save the serializer to cancel the order."""
|
||||
order = self.context['order']
|
||||
|
||||
order.cancel_order()
|
||||
@ -1298,7 +1298,7 @@ class SalesOrderSerialAllocationSerializer(serializers.Serializer):
|
||||
)
|
||||
|
||||
def validate_shipment(self, shipment):
|
||||
"""Validate the shipment:
|
||||
"""Validate the shipment.
|
||||
|
||||
- Must point to the same order
|
||||
- Must not be shipped
|
||||
@ -1314,7 +1314,7 @@ class SalesOrderSerialAllocationSerializer(serializers.Serializer):
|
||||
return shipment
|
||||
|
||||
def validate(self, data):
|
||||
"""Validation for the serializer:
|
||||
"""Validation for the serializer.
|
||||
|
||||
- Ensure the serial_numbers and quantity fields match
|
||||
- Check that all serial numbers exist
|
||||
@ -1374,7 +1374,7 @@ class SalesOrderSerialAllocationSerializer(serializers.Serializer):
|
||||
return data
|
||||
|
||||
def save(self):
|
||||
"""Allocate stock items against the sales order"""
|
||||
"""Allocate stock items against the sales order."""
|
||||
data = self.validated_data
|
||||
|
||||
line_item = data['line_item']
|
||||
@ -1481,10 +1481,10 @@ class SalesOrderAttachmentSerializer(InvenTreeAttachmentSerializer):
|
||||
class ReturnOrderSerializer(
|
||||
AbstractOrderSerializer, TotalPriceMixin, InvenTreeModelSerializer
|
||||
):
|
||||
"""Serializer for the ReturnOrder model class"""
|
||||
"""Serializer for the ReturnOrder model class."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options"""
|
||||
"""Metaclass options."""
|
||||
|
||||
model = order.models.ReturnOrder
|
||||
|
||||
@ -1499,7 +1499,7 @@ class ReturnOrderSerializer(
|
||||
read_only_fields = ['creation_date']
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialization routine for the serializer"""
|
||||
"""Initialization routine for the serializer."""
|
||||
customer_detail = kwargs.pop('customer_detail', False)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
@ -1509,7 +1509,7 @@ class ReturnOrderSerializer(
|
||||
|
||||
@staticmethod
|
||||
def annotate_queryset(queryset):
|
||||
"""Custom annotation for the serializer queryset"""
|
||||
"""Custom annotation for the serializer queryset."""
|
||||
queryset = AbstractOrderSerializer.annotate_queryset(queryset)
|
||||
|
||||
queryset = queryset.annotate(
|
||||
@ -1536,52 +1536,52 @@ class ReturnOrderSerializer(
|
||||
|
||||
|
||||
class ReturnOrderIssueSerializer(serializers.Serializer):
|
||||
"""Serializer for issuing a ReturnOrder"""
|
||||
"""Serializer for issuing a ReturnOrder."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options"""
|
||||
"""Metaclass options."""
|
||||
|
||||
fields = []
|
||||
|
||||
def save(self):
|
||||
"""Save the serializer to 'issue' the order"""
|
||||
"""Save the serializer to 'issue' the order."""
|
||||
order = self.context['order']
|
||||
order.issue_order()
|
||||
|
||||
|
||||
class ReturnOrderCancelSerializer(serializers.Serializer):
|
||||
"""Serializer for cancelling a ReturnOrder"""
|
||||
"""Serializer for cancelling a ReturnOrder."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options"""
|
||||
"""Metaclass options."""
|
||||
|
||||
fields = []
|
||||
|
||||
def save(self):
|
||||
"""Save the serializer to 'cancel' the order"""
|
||||
"""Save the serializer to 'cancel' the order."""
|
||||
order = self.context['order']
|
||||
order.cancel_order()
|
||||
|
||||
|
||||
class ReturnOrderCompleteSerializer(serializers.Serializer):
|
||||
"""Serializer for completing a ReturnOrder"""
|
||||
"""Serializer for completing a ReturnOrder."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options"""
|
||||
"""Metaclass options."""
|
||||
|
||||
fields = []
|
||||
|
||||
def save(self):
|
||||
"""Save the serializer to 'complete' the order"""
|
||||
"""Save the serializer to 'complete' the order."""
|
||||
order = self.context['order']
|
||||
order.complete_order()
|
||||
|
||||
|
||||
class ReturnOrderLineItemReceiveSerializer(serializers.Serializer):
|
||||
"""Serializer for receiving a single line item against a ReturnOrder"""
|
||||
"""Serializer for receiving a single line item against a ReturnOrder."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options"""
|
||||
"""Metaclass options."""
|
||||
|
||||
fields = ['item']
|
||||
|
||||
@ -1594,7 +1594,7 @@ class ReturnOrderLineItemReceiveSerializer(serializers.Serializer):
|
||||
)
|
||||
|
||||
def validate_line_item(self, item):
|
||||
"""Validation for a single line item"""
|
||||
"""Validation for a single line item."""
|
||||
if item.order != self.context['order']:
|
||||
raise ValidationError(_('Line item does not match return order'))
|
||||
|
||||
@ -1605,10 +1605,10 @@ class ReturnOrderLineItemReceiveSerializer(serializers.Serializer):
|
||||
|
||||
|
||||
class ReturnOrderReceiveSerializer(serializers.Serializer):
|
||||
"""Serializer for receiving items against a ReturnOrder"""
|
||||
"""Serializer for receiving items against a ReturnOrder."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options"""
|
||||
"""Metaclass options."""
|
||||
|
||||
fields = ['items', 'location']
|
||||
|
||||
@ -1624,7 +1624,7 @@ class ReturnOrderReceiveSerializer(serializers.Serializer):
|
||||
)
|
||||
|
||||
def validate(self, data):
|
||||
"""Perform data validation for this serializer"""
|
||||
"""Perform data validation for this serializer."""
|
||||
order = self.context['order']
|
||||
if order.status != ReturnOrderStatus.IN_PROGRESS:
|
||||
raise ValidationError(
|
||||
@ -1642,7 +1642,7 @@ class ReturnOrderReceiveSerializer(serializers.Serializer):
|
||||
|
||||
@transaction.atomic
|
||||
def save(self):
|
||||
"""Saving this serializer marks the returned items as received"""
|
||||
"""Saving this serializer marks the returned items as received."""
|
||||
order = self.context['order']
|
||||
request = self.context['request']
|
||||
|
||||
@ -1657,10 +1657,10 @@ class ReturnOrderReceiveSerializer(serializers.Serializer):
|
||||
|
||||
|
||||
class ReturnOrderLineItemSerializer(InvenTreeModelSerializer):
|
||||
"""Serializer for a ReturnOrderLineItem object"""
|
||||
"""Serializer for a ReturnOrderLineItem object."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options"""
|
||||
"""Metaclass options."""
|
||||
|
||||
model = order.models.ReturnOrderLineItem
|
||||
|
||||
@ -1683,7 +1683,7 @@ class ReturnOrderLineItemSerializer(InvenTreeModelSerializer):
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialization routine for the serializer"""
|
||||
"""Initialization routine for the serializer."""
|
||||
order_detail = kwargs.pop('order_detail', False)
|
||||
item_detail = kwargs.pop('item_detail', False)
|
||||
part_detail = kwargs.pop('part_detail', False)
|
||||
@ -1712,10 +1712,10 @@ class ReturnOrderLineItemSerializer(InvenTreeModelSerializer):
|
||||
class ReturnOrderExtraLineSerializer(
|
||||
AbstractExtraLineSerializer, InvenTreeModelSerializer
|
||||
):
|
||||
"""Serializer for a ReturnOrderExtraLine object"""
|
||||
"""Serializer for a ReturnOrderExtraLine object."""
|
||||
|
||||
class Meta(AbstractExtraLineMeta):
|
||||
"""Metaclass options"""
|
||||
"""Metaclass options."""
|
||||
|
||||
model = order.models.ReturnOrderExtraLine
|
||||
|
||||
@ -1723,10 +1723,10 @@ class ReturnOrderExtraLineSerializer(
|
||||
|
||||
|
||||
class ReturnOrderAttachmentSerializer(InvenTreeAttachmentSerializer):
|
||||
"""Serializer for the ReturnOrderAttachment model"""
|
||||
"""Serializer for the ReturnOrderAttachment model."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options"""
|
||||
"""Metaclass options."""
|
||||
|
||||
model = order.models.ReturnOrderAttachment
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
"""Background tasks for the 'order' app"""
|
||||
"""Background tasks for the 'order' app."""
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
@ -13,7 +13,7 @@ from plugin.events import trigger_event
|
||||
|
||||
|
||||
def notify_overdue_purchase_order(po: order.models.PurchaseOrder):
|
||||
"""Notify users that a PurchaseOrder has just become 'overdue'"""
|
||||
"""Notify users that a PurchaseOrder has just become 'overdue'."""
|
||||
targets = []
|
||||
|
||||
if po.created_by:
|
||||
@ -45,8 +45,9 @@ def notify_overdue_purchase_order(po: order.models.PurchaseOrder):
|
||||
|
||||
@scheduled_task(ScheduledTask.DAILY)
|
||||
def check_overdue_purchase_orders():
|
||||
"""Check if any outstanding PurchaseOrders have just become overdue:
|
||||
"""Check if any outstanding PurchaseOrders have just become overdue.
|
||||
|
||||
Rules:
|
||||
- This check is performed daily
|
||||
- Look at the 'target_date' of any outstanding PurchaseOrder objects
|
||||
- If the 'target_date' expired *yesterday* then the order is just out of date
|
||||
@ -62,7 +63,7 @@ def check_overdue_purchase_orders():
|
||||
|
||||
|
||||
def notify_overdue_sales_order(so: order.models.SalesOrder):
|
||||
"""Notify appropriate users that a SalesOrder has just become 'overdue'"""
|
||||
"""Notify appropriate users that a SalesOrder has just become 'overdue'."""
|
||||
targets = []
|
||||
|
||||
if so.created_by:
|
||||
@ -94,7 +95,7 @@ def notify_overdue_sales_order(so: order.models.SalesOrder):
|
||||
|
||||
@scheduled_task(ScheduledTask.DAILY)
|
||||
def check_overdue_sales_orders():
|
||||
"""Check if any outstanding SalesOrders have just become overdue
|
||||
"""Check if any outstanding SalesOrders have just become overdue.
|
||||
|
||||
- This check is performed daily
|
||||
- Look at the 'target_date' of any outstanding SalesOrder objects
|
||||
|
@ -30,7 +30,7 @@ from stock.models import StockItem
|
||||
|
||||
|
||||
class OrderTest(InvenTreeAPITestCase):
|
||||
"""Base class for order API unit testing"""
|
||||
"""Base class for order API unit testing."""
|
||||
|
||||
fixtures = [
|
||||
'category',
|
||||
@ -112,7 +112,7 @@ class PurchaseOrderTest(OrderTest):
|
||||
)
|
||||
|
||||
def test_po_list(self):
|
||||
"""Test the PurchaseOrder list API endpoint"""
|
||||
"""Test the PurchaseOrder list API endpoint."""
|
||||
# List *ALL* PurchaseOrder items
|
||||
self.filter({}, 7)
|
||||
|
||||
@ -150,7 +150,7 @@ class PurchaseOrderTest(OrderTest):
|
||||
self.filter({'supplier_part': 4}, 0)
|
||||
|
||||
def test_total_price(self):
|
||||
"""Unit tests for the 'total_price' field"""
|
||||
"""Unit tests for the 'total_price' field."""
|
||||
# Ensure we have exchange rate data
|
||||
self.generate_exchange_rates()
|
||||
|
||||
@ -219,7 +219,7 @@ class PurchaseOrderTest(OrderTest):
|
||||
self.filter({'overdue': False}, 6)
|
||||
|
||||
def test_po_detail(self):
|
||||
"""Test the PurchaseOrder detail API endpoint"""
|
||||
"""Test the PurchaseOrder detail API endpoint."""
|
||||
url = '/api/order/po/1/'
|
||||
|
||||
response = self.get(url)
|
||||
@ -255,7 +255,7 @@ class PurchaseOrderTest(OrderTest):
|
||||
self.assertEqual(order.reference_int, 0x7FFFFFFF)
|
||||
|
||||
def test_po_attachments(self):
|
||||
"""Test the list endpoint for the PurchaseOrderAttachment model"""
|
||||
"""Test the list endpoint for the PurchaseOrderAttachment model."""
|
||||
url = reverse('api-po-attachment-list')
|
||||
|
||||
response = self.get(url)
|
||||
@ -358,7 +358,7 @@ class PurchaseOrderTest(OrderTest):
|
||||
)
|
||||
|
||||
def test_po_duplicate(self):
|
||||
"""Test that we can duplicate a PurchaseOrder via the API"""
|
||||
"""Test that we can duplicate a PurchaseOrder via the API."""
|
||||
self.assignRole('purchase_order.add')
|
||||
|
||||
po = models.PurchaseOrder.objects.get(pk=1)
|
||||
@ -488,7 +488,7 @@ class PurchaseOrderTest(OrderTest):
|
||||
self.assertEqual(po.status, PurchaseOrderStatus.PLACED)
|
||||
|
||||
def test_po_calendar(self):
|
||||
"""Test the calendar export endpoint"""
|
||||
"""Test the calendar export endpoint."""
|
||||
# Create required purchase orders
|
||||
self.assignRole('purchase_order.add')
|
||||
|
||||
@ -578,7 +578,7 @@ class PurchaseOrderTest(OrderTest):
|
||||
self.assertEqual(number_orders_incl_completed, n_events)
|
||||
|
||||
def test_po_calendar_noauth(self):
|
||||
"""Test accessing calendar without authorization"""
|
||||
"""Test accessing calendar without authorization."""
|
||||
self.client.logout()
|
||||
response = self.client.get(
|
||||
reverse('api-po-so-calendar', kwargs={'ordertype': 'purchase-order'}),
|
||||
@ -593,7 +593,7 @@ class PurchaseOrderTest(OrderTest):
|
||||
)
|
||||
|
||||
def test_po_calendar_auth(self):
|
||||
"""Test accessing calendar with header authorization"""
|
||||
"""Test accessing calendar with header authorization."""
|
||||
self.client.logout()
|
||||
base64_token = base64.b64encode(
|
||||
f'{self.username}:{self.password}'.encode('ascii')
|
||||
@ -612,7 +612,7 @@ class PurchaseOrderLineItemTest(OrderTest):
|
||||
LIST_URL = reverse('api-po-line-list')
|
||||
|
||||
def test_po_line_list(self):
|
||||
"""Test the PurchaseOrderLine list API endpoint"""
|
||||
"""Test the PurchaseOrderLine list API endpoint."""
|
||||
# List *ALL* PurchaseOrderLine items
|
||||
self.filter({}, 5)
|
||||
|
||||
@ -688,7 +688,7 @@ class PurchaseOrderDownloadTest(OrderTest):
|
||||
self.assertEqual(order.reference, row['reference'])
|
||||
|
||||
def test_download_line_items(self):
|
||||
"""Test that the PurchaseOrderLineItems can be downloaded to a file"""
|
||||
"""Test that the PurchaseOrderLineItems can be downloaded to a file."""
|
||||
with self.download_file(
|
||||
reverse('api-po-line-list'),
|
||||
{'export': 'xlsx'},
|
||||
@ -703,7 +703,7 @@ class PurchaseOrderReceiveTest(OrderTest):
|
||||
"""Unit tests for receiving items against a PurchaseOrder."""
|
||||
|
||||
def setUp(self):
|
||||
"""Init routines for this unit test class"""
|
||||
"""Init routines for this unit test class."""
|
||||
super().setUp()
|
||||
|
||||
self.assignRole('purchase_order.add')
|
||||
@ -805,7 +805,7 @@ class PurchaseOrderReceiveTest(OrderTest):
|
||||
)
|
||||
|
||||
def test_invalid_barcodes(self):
|
||||
"""Tests for checking in items with invalid barcodes:
|
||||
"""Tests for checking in items with invalid barcodes.
|
||||
|
||||
- Cannot check in "duplicate" barcodes
|
||||
- Barcodes cannot match 'barcode_hash' field for existing StockItem
|
||||
@ -998,7 +998,7 @@ class SalesOrderTest(OrderTest):
|
||||
LIST_URL = reverse('api-so-list')
|
||||
|
||||
def test_so_list(self):
|
||||
"""Test the SalesOrder list API endpoint"""
|
||||
"""Test the SalesOrder list API endpoint."""
|
||||
# All orders
|
||||
self.filter({}, 5)
|
||||
|
||||
@ -1024,7 +1024,7 @@ class SalesOrderTest(OrderTest):
|
||||
self.filter({'assigned_to_me': 0}, 5)
|
||||
|
||||
def test_total_price(self):
|
||||
"""Unit tests for the 'total_price' field"""
|
||||
"""Unit tests for the 'total_price' field."""
|
||||
# Ensure we have exchange rate data
|
||||
self.generate_exchange_rates()
|
||||
|
||||
@ -1106,7 +1106,7 @@ class SalesOrderTest(OrderTest):
|
||||
self.filter({'overdue': False}, 3)
|
||||
|
||||
def test_so_detail(self):
|
||||
"""Test the SalesOrder detail endpoint"""
|
||||
"""Test the SalesOrder detail endpoint."""
|
||||
url = '/api/order/so/1/'
|
||||
|
||||
response = self.get(url)
|
||||
@ -1116,7 +1116,7 @@ class SalesOrderTest(OrderTest):
|
||||
self.assertEqual(data['pk'], 1)
|
||||
|
||||
def test_so_attachments(self):
|
||||
"""Test the list endpoint for the SalesOrderAttachment model"""
|
||||
"""Test the list endpoint for the SalesOrderAttachment model."""
|
||||
url = reverse('api-so-attachment-list')
|
||||
|
||||
self.get(url)
|
||||
@ -1241,7 +1241,7 @@ class SalesOrderTest(OrderTest):
|
||||
self.assertEqual(so.status, SalesOrderStatus.CANCELLED)
|
||||
|
||||
def test_so_calendar(self):
|
||||
"""Test the calendar export endpoint"""
|
||||
"""Test the calendar export endpoint."""
|
||||
# Create required sales orders
|
||||
self.assignRole('sales_order.add')
|
||||
|
||||
@ -1313,7 +1313,7 @@ class SalesOrderTest(OrderTest):
|
||||
self.assertEqual(number_orders_incl_complete, n_events)
|
||||
|
||||
def test_export(self):
|
||||
"""Test we can export the SalesOrder list"""
|
||||
"""Test we can export the SalesOrder list."""
|
||||
n = models.SalesOrder.objects.count()
|
||||
|
||||
# Check there are some sales orders
|
||||
@ -1341,7 +1341,7 @@ class SalesOrderLineItemTest(OrderTest):
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
"""Init routine for this unit test class"""
|
||||
"""Init routine for this unit test class."""
|
||||
super().setUpTestData()
|
||||
|
||||
# List of salable parts
|
||||
@ -1367,7 +1367,7 @@ class SalesOrderLineItemTest(OrderTest):
|
||||
cls.url = reverse('api-so-line-list')
|
||||
|
||||
def test_so_line_list(self):
|
||||
"""Test list endpoint"""
|
||||
"""Test list endpoint."""
|
||||
response = self.get(self.url, {}, expected_code=200)
|
||||
|
||||
n = models.SalesOrderLineItem.objects.count()
|
||||
@ -1416,7 +1416,7 @@ class SalesOrderDownloadTest(OrderTest):
|
||||
self.download_file(url, {}, expected_code=200)
|
||||
|
||||
def test_download_xls(self):
|
||||
"""Test xls file download"""
|
||||
"""Test xls file download."""
|
||||
url = reverse('api-so-list')
|
||||
|
||||
# Download .xls file
|
||||
@ -1430,7 +1430,7 @@ class SalesOrderDownloadTest(OrderTest):
|
||||
self.assertTrue(isinstance(file, io.BytesIO))
|
||||
|
||||
def test_download_csv(self):
|
||||
"""Test that the list of sales orders can be downloaded as a .csv file"""
|
||||
"""Test that the list of sales orders can be downloaded as a .csv file."""
|
||||
url = reverse('api-so-list')
|
||||
|
||||
required_cols = [
|
||||
@ -1490,7 +1490,7 @@ class SalesOrderAllocateTest(OrderTest):
|
||||
"""Unit tests for allocating stock items against a SalesOrder."""
|
||||
|
||||
def setUp(self):
|
||||
"""Init routines for this unit testing class"""
|
||||
"""Init routines for this unit testing class."""
|
||||
super().setUp()
|
||||
|
||||
self.assignRole('sales_order.add')
|
||||
@ -1599,7 +1599,7 @@ class SalesOrderAllocateTest(OrderTest):
|
||||
self.assertEqual(line.allocations.count(), 1)
|
||||
|
||||
def test_allocate_variant(self):
|
||||
"""Test that the allocation endpoint acts as expected, when provided with variant"""
|
||||
"""Test that the allocation endpoint acts as expected, when provided with variant."""
|
||||
# First, check that there are no line items allocated against this SalesOrder
|
||||
self.assertEqual(self.order.stock_allocations.count(), 0)
|
||||
|
||||
@ -1706,7 +1706,7 @@ class SalesOrderAllocateTest(OrderTest):
|
||||
self.assertEqual(self.shipment.delivery_date, datetime(2023, 5, 15).date())
|
||||
|
||||
def test_sales_order_shipment_list(self):
|
||||
"""Test the SalesOrderShipment list API endpoint"""
|
||||
"""Test the SalesOrderShipment list API endpoint."""
|
||||
url = reverse('api-so-shipment-list')
|
||||
|
||||
# Count before creation
|
||||
@ -1740,7 +1740,7 @@ class SalesOrderAllocateTest(OrderTest):
|
||||
|
||||
|
||||
class ReturnOrderTests(InvenTreeAPITestCase):
|
||||
"""Unit tests for ReturnOrder API endpoints"""
|
||||
"""Unit tests for ReturnOrder API endpoints."""
|
||||
|
||||
fixtures = [
|
||||
'category',
|
||||
@ -1753,7 +1753,7 @@ class ReturnOrderTests(InvenTreeAPITestCase):
|
||||
]
|
||||
|
||||
def test_options(self):
|
||||
"""Test the OPTIONS endpoint"""
|
||||
"""Test the OPTIONS endpoint."""
|
||||
self.assignRole('return_order.add')
|
||||
data = self.options(reverse('api-return-order-list'), expected_code=200).data
|
||||
|
||||
@ -1770,7 +1770,7 @@ class ReturnOrderTests(InvenTreeAPITestCase):
|
||||
self.assertEqual(reference['type'], 'string')
|
||||
|
||||
def test_list(self):
|
||||
"""Tests for the list endpoint"""
|
||||
"""Tests for the list endpoint."""
|
||||
url = reverse('api-return-order-list')
|
||||
|
||||
response = self.get(url, expected_code=200)
|
||||
@ -1813,7 +1813,7 @@ class ReturnOrderTests(InvenTreeAPITestCase):
|
||||
self.assertEqual(result['status'], 20)
|
||||
|
||||
def test_create(self):
|
||||
"""Test creation of ReturnOrder via the API"""
|
||||
"""Test creation of ReturnOrder via the API."""
|
||||
url = reverse('api-return-order-list')
|
||||
|
||||
# Do not have required permissions yet
|
||||
@ -1838,7 +1838,7 @@ class ReturnOrderTests(InvenTreeAPITestCase):
|
||||
self.assertEqual(data['customer_reference'], 'cr')
|
||||
|
||||
def test_update(self):
|
||||
"""Test that we can update a ReturnOrder via the API"""
|
||||
"""Test that we can update a ReturnOrder via the API."""
|
||||
url = reverse('api-return-order-detail', kwargs={'pk': 1})
|
||||
|
||||
# Test detail endpoint
|
||||
@ -1859,7 +1859,7 @@ class ReturnOrderTests(InvenTreeAPITestCase):
|
||||
self.assertEqual(rma.customer_reference, 'customer ref')
|
||||
|
||||
def test_ro_issue(self):
|
||||
"""Test the 'issue' order for a ReturnOrder"""
|
||||
"""Test the 'issue' order for a ReturnOrder."""
|
||||
order = models.ReturnOrder.objects.get(pk=1)
|
||||
self.assertEqual(order.status, ReturnOrderStatus.PENDING)
|
||||
self.assertIsNone(order.issue_date)
|
||||
@ -1877,7 +1877,7 @@ class ReturnOrderTests(InvenTreeAPITestCase):
|
||||
self.assertIsNotNone(order.issue_date)
|
||||
|
||||
def test_receive(self):
|
||||
"""Test that we can receive items against a ReturnOrder"""
|
||||
"""Test that we can receive items against a ReturnOrder."""
|
||||
customer = Company.objects.get(pk=4)
|
||||
|
||||
# Create an order
|
||||
@ -1905,7 +1905,7 @@ class ReturnOrderTests(InvenTreeAPITestCase):
|
||||
self.assertEqual(rma.lines.count(), 3)
|
||||
|
||||
def receive(items, location=None, expected_code=400):
|
||||
"""Helper function to receive items against this ReturnOrder"""
|
||||
"""Helper function to receive items against this ReturnOrder."""
|
||||
url = reverse('api-return-order-receive', kwargs={'pk': rma.pk})
|
||||
|
||||
response = self.post(
|
||||
@ -1970,7 +1970,7 @@ class ReturnOrderTests(InvenTreeAPITestCase):
|
||||
self.assertEqual(deltas['returnorder'], rma.pk)
|
||||
|
||||
def test_ro_calendar(self):
|
||||
"""Test the calendar export endpoint"""
|
||||
"""Test the calendar export endpoint."""
|
||||
# Full test is in test_po_calendar. Since these use the same backend, test only
|
||||
# that the endpoint is available
|
||||
url = reverse('api-po-so-calendar', kwargs={'ordertype': 'return-order'})
|
||||
@ -1999,7 +1999,7 @@ class OrderMetadataAPITest(InvenTreeAPITestCase):
|
||||
roles = ['purchase_order.change', 'sales_order.change', 'return_order.change']
|
||||
|
||||
def metatester(self, apikey, model):
|
||||
"""Generic tester"""
|
||||
"""Generic tester."""
|
||||
modeldata = model.objects.first()
|
||||
|
||||
# Useless test unless a model object is found
|
||||
@ -2025,7 +2025,7 @@ class OrderMetadataAPITest(InvenTreeAPITestCase):
|
||||
)
|
||||
|
||||
def test_metadata(self):
|
||||
"""Test all endpoints"""
|
||||
"""Test all endpoints."""
|
||||
for apikey, model in {
|
||||
'api-po-metadata': models.PurchaseOrder,
|
||||
'api-po-line-metadata': models.PurchaseOrderLineItem,
|
||||
|
@ -1,4 +1,4 @@
|
||||
"""Unit tests for the SalesOrder models"""
|
||||
"""Unit tests for the SalesOrder models."""
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
@ -30,7 +30,7 @@ class SalesOrderTest(TestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
"""Initial setup for this set of unit tests"""
|
||||
"""Initial setup for this set of unit tests."""
|
||||
# Create a Company to ship the goods to
|
||||
cls.customer = Company.objects.create(
|
||||
name='ABC Co', description='My customer', is_customer=True
|
||||
@ -76,14 +76,14 @@ class SalesOrderTest(TestCase):
|
||||
)
|
||||
|
||||
def test_so_reference(self):
|
||||
"""Unit tests for sales order generation"""
|
||||
"""Unit tests for sales order generation."""
|
||||
# Test that a good reference is created when we have no existing orders
|
||||
SalesOrder.objects.all().delete()
|
||||
|
||||
self.assertEqual(SalesOrder.generate_reference(), 'SO-0001')
|
||||
|
||||
def test_rebuild_reference(self):
|
||||
"""Test that the 'reference_int' field gets rebuilt when the model is saved"""
|
||||
"""Test that the 'reference_int' field gets rebuilt when the model is saved."""
|
||||
self.assertEqual(self.order.reference_int, 1234)
|
||||
|
||||
self.order.reference = '999'
|
||||
@ -112,7 +112,7 @@ class SalesOrderTest(TestCase):
|
||||
self.assertFalse(self.order.is_overdue)
|
||||
|
||||
def test_empty_order(self):
|
||||
"""Test for an empty order"""
|
||||
"""Test for an empty order."""
|
||||
self.assertEqual(self.line.quantity, 50)
|
||||
self.assertEqual(self.line.allocated_quantity(), 0)
|
||||
self.assertEqual(self.line.fulfilled_quantity(), 0)
|
||||
@ -123,14 +123,14 @@ class SalesOrderTest(TestCase):
|
||||
self.assertFalse(self.order.is_fully_allocated())
|
||||
|
||||
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):
|
||||
SalesOrderLineItem.objects.create(
|
||||
order=self.order, part=self.part, quantity=ii
|
||||
)
|
||||
|
||||
def allocate_stock(self, full=True):
|
||||
"""Allocate stock to the order"""
|
||||
"""Allocate stock to the order."""
|
||||
SalesOrderAllocation.objects.create(
|
||||
line=self.line,
|
||||
shipment=self.shipment,
|
||||
@ -146,8 +146,7 @@ class SalesOrderTest(TestCase):
|
||||
)
|
||||
|
||||
def test_over_allocate(self):
|
||||
"""Test that over allocation logic works"""
|
||||
|
||||
"""Test that over allocation logic works."""
|
||||
SA = StockItem.objects.create(part=self.part, quantity=9)
|
||||
|
||||
# First three allocations should succeed
|
||||
@ -171,7 +170,7 @@ class SalesOrderTest(TestCase):
|
||||
allocation.clean()
|
||||
|
||||
def test_allocate_partial(self):
|
||||
"""Partially allocate stock"""
|
||||
"""Partially allocate stock."""
|
||||
self.allocate_stock(False)
|
||||
|
||||
self.assertFalse(self.order.is_fully_allocated())
|
||||
@ -180,7 +179,7 @@ class SalesOrderTest(TestCase):
|
||||
self.assertEqual(self.line.fulfilled_quantity(), 0)
|
||||
|
||||
def test_allocate_full(self):
|
||||
"""Fully allocate stock"""
|
||||
"""Fully allocate stock."""
|
||||
self.allocate_stock(True)
|
||||
|
||||
self.assertTrue(self.order.is_fully_allocated())
|
||||
@ -188,7 +187,7 @@ class SalesOrderTest(TestCase):
|
||||
self.assertEqual(self.line.allocated_quantity(), 50)
|
||||
|
||||
def test_allocate_variant(self):
|
||||
"""Allocate a variant of the designated item"""
|
||||
"""Allocate a variant of the designated item."""
|
||||
SalesOrderAllocation.objects.create(
|
||||
line=self.line,
|
||||
shipment=self.shipment,
|
||||
@ -198,7 +197,7 @@ class SalesOrderTest(TestCase):
|
||||
self.assertEqual(self.line.allocated_quantity(), 50)
|
||||
|
||||
def test_order_cancel(self):
|
||||
"""Allocate line items then cancel the order"""
|
||||
"""Allocate line items then cancel the order."""
|
||||
self.allocate_stock(True)
|
||||
|
||||
self.assertEqual(SalesOrderAllocation.objects.count(), 2)
|
||||
@ -216,7 +215,7 @@ class SalesOrderTest(TestCase):
|
||||
self.assertFalse(result)
|
||||
|
||||
def test_complete_order(self):
|
||||
"""Allocate line items, then ship the order"""
|
||||
"""Allocate line items, then ship the order."""
|
||||
# Assert some stuff before we run the test
|
||||
# Initially there are three stock items
|
||||
self.assertEqual(StockItem.objects.count(), 3)
|
||||
@ -279,7 +278,7 @@ class SalesOrderTest(TestCase):
|
||||
self.assertEqual(self.line.allocated_quantity(), 50)
|
||||
|
||||
def test_default_shipment(self):
|
||||
"""Test sales order default shipment creation"""
|
||||
"""Test sales order default shipment creation."""
|
||||
# Default setting value should be False
|
||||
self.assertEqual(
|
||||
False, InvenTreeSetting.get_setting('SALESORDER_DEFAULT_SHIPMENT')
|
||||
@ -312,13 +311,13 @@ class SalesOrderTest(TestCase):
|
||||
self.assertEqual('1', order_2.pending_shipments()[0].reference)
|
||||
|
||||
def test_shipment_delivery(self):
|
||||
"""Test the shipment delivery settings"""
|
||||
"""Test the shipment delivery settings."""
|
||||
# Shipment delivery date should be empty before setting date
|
||||
self.assertIsNone(self.shipment.delivery_date)
|
||||
self.assertFalse(self.shipment.is_delivered())
|
||||
|
||||
def test_overdue_notification(self):
|
||||
"""Test overdue sales order notification"""
|
||||
"""Test overdue sales order notification."""
|
||||
self.order.created_by = get_user_model().objects.get(pk=3)
|
||||
self.order.responsible = Owner.create(obj=Group.objects.get(pk=2))
|
||||
self.order.target_date = datetime.now().date() - timedelta(days=1)
|
||||
|
@ -1,4 +1,4 @@
|
||||
"""Unit tests for Order views (see views.py)"""
|
||||
"""Unit tests for Order views (see views.py)."""
|
||||
|
||||
from django.urls import reverse
|
||||
|
||||
@ -6,7 +6,7 @@ from InvenTree.unit_test import InvenTreeTestCase
|
||||
|
||||
|
||||
class OrderViewTestCase(InvenTreeTestCase):
|
||||
"""Base unit test class for order views"""
|
||||
"""Base unit test class for order views."""
|
||||
|
||||
fixtures = [
|
||||
'category',
|
||||
@ -35,10 +35,10 @@ class OrderViewTestCase(InvenTreeTestCase):
|
||||
|
||||
|
||||
class PurchaseOrderListTest(OrderViewTestCase):
|
||||
"""Unit tests for the PurchaseOrder index page"""
|
||||
"""Unit tests for the PurchaseOrder index page."""
|
||||
|
||||
def test_order_list(self):
|
||||
"""Tests for the PurchaseOrder index page"""
|
||||
"""Tests for the PurchaseOrder index page."""
|
||||
response = self.client.get(reverse('purchase-order-index'))
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
@ -65,28 +65,28 @@ class PurchaseOrderTests(OrderViewTestCase):
|
||||
|
||||
|
||||
class SalesOrderViews(OrderViewTestCase):
|
||||
"""Unit tests for the SalesOrder pages"""
|
||||
"""Unit tests for the SalesOrder pages."""
|
||||
|
||||
def test_index(self):
|
||||
"""Test the SalesOrder index page"""
|
||||
"""Test the SalesOrder index page."""
|
||||
response = self.client.get(reverse('sales-order-index'))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_detail(self):
|
||||
"""Test SalesOrder detail view"""
|
||||
"""Test SalesOrder detail view."""
|
||||
response = self.client.get(reverse('so-detail', args=(1,)))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
class ReturnOrderVIews(OrderViewTestCase):
|
||||
"""Unit tests for the ReturnOrder pages"""
|
||||
"""Unit tests for the ReturnOrder pages."""
|
||||
|
||||
def test_index(self):
|
||||
"""Test the ReturnOrder index page"""
|
||||
"""Test the ReturnOrder index page."""
|
||||
response = self.client.get(reverse('return-order-index'))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_detail(self):
|
||||
"""Test ReturnOrder detail view"""
|
||||
"""Test ReturnOrder detail view."""
|
||||
response = self.client.get(reverse('return-order-detail', args=(1,)))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
@ -1,4 +1,4 @@
|
||||
"""Various unit tests for order models"""
|
||||
"""Various unit tests for order models."""
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from decimal import Decimal
|
||||
@ -49,7 +49,7 @@ class OrderTest(TestCase):
|
||||
self.assertEqual(str(line), '100 x ACME0001 from ACME (for PO-0001 - ACME)')
|
||||
|
||||
def test_rebuild_reference(self):
|
||||
"""Test that the reference_int field is correctly updated when the model is saved"""
|
||||
"""Test that the reference_int field is correctly updated when the model is saved."""
|
||||
order = PurchaseOrder.objects.get(pk=1)
|
||||
order.save()
|
||||
self.assertEqual(order.reference_int, 1)
|
||||
@ -214,7 +214,7 @@ class OrderTest(TestCase):
|
||||
self.assertEqual(order.status, PurchaseOrderStatus.COMPLETE)
|
||||
|
||||
def test_receive_pack_size(self):
|
||||
"""Test receiving orders from suppliers with different pack_size values"""
|
||||
"""Test receiving orders from suppliers with different pack_size values."""
|
||||
prt = Part.objects.get(pk=1)
|
||||
sup = Company.objects.get(pk=1)
|
||||
|
||||
@ -305,7 +305,7 @@ class OrderTest(TestCase):
|
||||
self.assertEqual(si.purchase_price, Money(100, 'USD'))
|
||||
|
||||
def test_overdue_notification(self):
|
||||
"""Test overdue purchase order notification
|
||||
"""Test overdue purchase order notification.
|
||||
|
||||
Ensure that a notification is sent when a PurchaseOrder becomes overdue
|
||||
"""
|
||||
@ -343,7 +343,7 @@ class OrderTest(TestCase):
|
||||
self.assertEqual(msg.name, 'Overdue Purchase Order')
|
||||
|
||||
def test_new_po_notification(self):
|
||||
"""Test that a notification is sent when a new PurchaseOrder is created
|
||||
"""Test that a notification is sent when a new PurchaseOrder is created.
|
||||
|
||||
- The responsible user(s) should receive a notification
|
||||
- The creating user should *not* receive a notification
|
||||
|
@ -1,5 +1,6 @@
|
||||
"""URL lookup for the Order app. Provides URL endpoints for:
|
||||
"""URL lookup for the Order app.
|
||||
|
||||
Provides URL endpoints for:
|
||||
- List view of Purchase Orders
|
||||
- Detail view of Purchase Orders
|
||||
"""
|
||||
|
@ -1,64 +1,64 @@
|
||||
"""Validation methods for the order app"""
|
||||
"""Validation methods for the order app."""
|
||||
|
||||
|
||||
def generate_next_sales_order_reference():
|
||||
"""Generate the next available SalesOrder reference"""
|
||||
"""Generate the next available SalesOrder reference."""
|
||||
from order.models import SalesOrder
|
||||
|
||||
return SalesOrder.generate_reference()
|
||||
|
||||
|
||||
def generate_next_purchase_order_reference():
|
||||
"""Generate the next available PurchasesOrder reference"""
|
||||
"""Generate the next available PurchasesOrder reference."""
|
||||
from order.models import PurchaseOrder
|
||||
|
||||
return PurchaseOrder.generate_reference()
|
||||
|
||||
|
||||
def generate_next_return_order_reference():
|
||||
"""Generate the next available ReturnOrder reference"""
|
||||
"""Generate the next available ReturnOrder reference."""
|
||||
from order.models import ReturnOrder
|
||||
|
||||
return ReturnOrder.generate_reference()
|
||||
|
||||
|
||||
def validate_sales_order_reference_pattern(pattern):
|
||||
"""Validate the SalesOrder reference 'pattern' setting"""
|
||||
"""Validate the SalesOrder reference 'pattern' setting."""
|
||||
from order.models import SalesOrder
|
||||
|
||||
SalesOrder.validate_reference_pattern(pattern)
|
||||
|
||||
|
||||
def validate_purchase_order_reference_pattern(pattern):
|
||||
"""Validate the PurchaseOrder reference 'pattern' setting"""
|
||||
"""Validate the PurchaseOrder reference 'pattern' setting."""
|
||||
from order.models import PurchaseOrder
|
||||
|
||||
PurchaseOrder.validate_reference_pattern(pattern)
|
||||
|
||||
|
||||
def validate_return_order_reference_pattern(pattern):
|
||||
"""Validate the ReturnOrder reference 'pattern' setting"""
|
||||
"""Validate the ReturnOrder reference 'pattern' setting."""
|
||||
from order.models import ReturnOrder
|
||||
|
||||
ReturnOrder.validate_reference_pattern(pattern)
|
||||
|
||||
|
||||
def validate_sales_order_reference(value):
|
||||
"""Validate that the SalesOrder reference field matches the required pattern"""
|
||||
"""Validate that the SalesOrder reference field matches the required pattern."""
|
||||
from order.models import SalesOrder
|
||||
|
||||
SalesOrder.validate_reference_field(value)
|
||||
|
||||
|
||||
def validate_purchase_order_reference(value):
|
||||
"""Validate that the PurchaseOrder reference field matches the required pattern"""
|
||||
"""Validate that the PurchaseOrder reference field matches the required pattern."""
|
||||
from order.models import PurchaseOrder
|
||||
|
||||
PurchaseOrder.validate_reference_field(value)
|
||||
|
||||
|
||||
def validate_return_order_reference(value):
|
||||
"""Validate that the ReturnOrder reference field matches the required pattern"""
|
||||
"""Validate that the ReturnOrder reference field matches the required pattern."""
|
||||
from order.models import ReturnOrder
|
||||
|
||||
ReturnOrder.validate_reference_field(value)
|
||||
|
@ -50,7 +50,7 @@ class PurchaseOrderIndex(InvenTreeRoleMixin, ListView):
|
||||
|
||||
|
||||
class SalesOrderIndex(InvenTreeRoleMixin, ListView):
|
||||
"""SalesOrder index (list) view class"""
|
||||
"""SalesOrder index (list) view class."""
|
||||
|
||||
model = SalesOrder
|
||||
template_name = 'order/sales_orders.html'
|
||||
@ -58,7 +58,7 @@ class SalesOrderIndex(InvenTreeRoleMixin, ListView):
|
||||
|
||||
|
||||
class ReturnOrderIndex(InvenTreeRoleMixin, ListView):
|
||||
"""ReturnOrder index (list) view"""
|
||||
"""ReturnOrder index (list) view."""
|
||||
|
||||
model = ReturnOrder
|
||||
template_name = 'order/return_orders.html'
|
||||
@ -84,7 +84,7 @@ class SalesOrderDetail(InvenTreeRoleMixin, InvenTreePluginViewMixin, DetailView)
|
||||
|
||||
|
||||
class ReturnOrderDetail(InvenTreeRoleMixin, InvenTreePluginViewMixin, DetailView):
|
||||
"""Detail view for a ReturnOrder object"""
|
||||
"""Detail view for a ReturnOrder object."""
|
||||
|
||||
context_object_name = 'order'
|
||||
queryset = ReturnOrder.objects.all()
|
||||
@ -92,10 +92,10 @@ class ReturnOrderDetail(InvenTreeRoleMixin, InvenTreePluginViewMixin, DetailView
|
||||
|
||||
|
||||
class PurchaseOrderUpload(FileManagementFormView):
|
||||
"""PurchaseOrder: Upload file, match to fields and parts (using multi-Step form)"""
|
||||
"""PurchaseOrder: Upload file, match to fields and parts (using multi-Step form)."""
|
||||
|
||||
class OrderFileManager(FileManager):
|
||||
"""Specify required fields"""
|
||||
"""Specify required fields."""
|
||||
|
||||
REQUIRED_HEADERS = ['Quantity']
|
||||
|
||||
@ -289,7 +289,7 @@ class SalesOrderExport(AjaxView):
|
||||
role_required = 'sales_order.view'
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""Perform GET request to export SalesOrder dataset"""
|
||||
"""Perform GET request to export SalesOrder dataset."""
|
||||
order = get_object_or_404(SalesOrder, pk=self.kwargs.get('pk', None))
|
||||
|
||||
export_format = request.GET.get('format', 'csv')
|
||||
@ -316,7 +316,7 @@ class PurchaseOrderExport(AjaxView):
|
||||
role_required = 'purchase_order.view'
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""Perform GET request to export PurchaseOrder dataset"""
|
||||
"""Perform GET request to export PurchaseOrder dataset."""
|
||||
order = get_object_or_404(PurchaseOrder, pk=self.kwargs.get('pk', None))
|
||||
|
||||
export_format = request.GET.get('format', 'csv')
|
||||
@ -334,7 +334,7 @@ class LineItemPricing(PartPricing):
|
||||
"""View for inspecting part pricing information."""
|
||||
|
||||
class EnhancedForm(PartPricing.form_class):
|
||||
"""Extra form options"""
|
||||
"""Extra form options."""
|
||||
|
||||
pk = IntegerField(widget=HiddenInput())
|
||||
so_line = IntegerField(widget=HiddenInput())
|
||||
@ -342,7 +342,7 @@ class LineItemPricing(PartPricing):
|
||||
form_class = EnhancedForm
|
||||
|
||||
def get_part(self, id=False):
|
||||
"""Return the Part instance associated with this view"""
|
||||
"""Return the Part instance associated with this view."""
|
||||
if 'line_item' in self.request.GET:
|
||||
try:
|
||||
part_id = self.request.GET.get('line_item')
|
||||
@ -364,7 +364,7 @@ class LineItemPricing(PartPricing):
|
||||
return part
|
||||
|
||||
def get_so(self, pk=False):
|
||||
"""Return the SalesOrderLineItem associated with this view"""
|
||||
"""Return the SalesOrderLineItem associated with this view."""
|
||||
so_line = self.request.GET.get('line_item', None)
|
||||
if not so_line:
|
||||
so_line = self.request.POST.get('so_line', None)
|
||||
@ -387,14 +387,14 @@ class LineItemPricing(PartPricing):
|
||||
return qty
|
||||
|
||||
def get_initials(self):
|
||||
"""Return initial context values for this view"""
|
||||
"""Return initial context values for this view."""
|
||||
initials = super().get_initials()
|
||||
initials['pk'] = self.get_part(id=True)
|
||||
initials['so_line'] = self.get_so(pk=True)
|
||||
return initials
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
"""Respond to a POST request to get particular pricing information"""
|
||||
"""Respond to a POST request to get particular pricing information."""
|
||||
REF = 'act-btn_'
|
||||
act_btn = [a.replace(REF, '') for a in self.request.POST if REF in a]
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
"""Admin class definitions for the 'part' app"""
|
||||
"""Admin class definitions for the 'part' app."""
|
||||
|
||||
from django.contrib import admin
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
@ -17,7 +17,7 @@ class PartResource(InvenTreeResource):
|
||||
"""Class for managing Part data import/export."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass definition"""
|
||||
"""Metaclass options."""
|
||||
|
||||
model = models.Part
|
||||
skip_unchanged = True
|
||||
@ -159,14 +159,14 @@ class PartResource(InvenTreeResource):
|
||||
)
|
||||
|
||||
def dehydrate_min_cost(self, part):
|
||||
"""Render minimum cost value for this Part"""
|
||||
"""Render minimum cost value for this Part."""
|
||||
min_cost = part.pricing.overall_min if part.pricing else None
|
||||
|
||||
if min_cost is not None:
|
||||
return float(min_cost.amount)
|
||||
|
||||
def dehydrate_max_cost(self, part):
|
||||
"""Render maximum cost value for this Part"""
|
||||
"""Render maximum cost value for this Part."""
|
||||
max_cost = part.pricing.overall_max if part.pricing else None
|
||||
|
||||
if max_cost is not None:
|
||||
@ -186,7 +186,7 @@ class PartResource(InvenTreeResource):
|
||||
return query
|
||||
|
||||
def after_import(self, dataset, result, using_transactions, dry_run, **kwargs):
|
||||
"""Rebuild MPTT tree structure after importing Part data"""
|
||||
"""Rebuild MPTT tree structure after importing Part data."""
|
||||
super().after_import(dataset, result, using_transactions, dry_run, **kwargs)
|
||||
|
||||
# Rebuild the Part tree(s)
|
||||
@ -197,7 +197,7 @@ class PartImportResource(InvenTreeResource):
|
||||
"""Class for managing Part data import/export."""
|
||||
|
||||
class Meta(PartResource.Meta):
|
||||
"""Metaclass definition"""
|
||||
"""Metaclass options."""
|
||||
|
||||
skip_unchanged = True
|
||||
report_skipped = False
|
||||
@ -223,13 +223,13 @@ class PartImportResource(InvenTreeResource):
|
||||
|
||||
|
||||
class PartParameterInline(admin.TabularInline):
|
||||
"""Inline for part parameter data"""
|
||||
"""Inline for part parameter data."""
|
||||
|
||||
model = models.PartParameter
|
||||
|
||||
|
||||
class PartAdmin(ImportExportModelAdmin):
|
||||
"""Admin class for the Part model"""
|
||||
"""Admin class for the Part model."""
|
||||
|
||||
resource_class = PartResource
|
||||
|
||||
@ -256,7 +256,7 @@ class PartAdmin(ImportExportModelAdmin):
|
||||
|
||||
|
||||
class PartPricingAdmin(admin.ModelAdmin):
|
||||
"""Admin class for PartPricing model"""
|
||||
"""Admin class for PartPricing model."""
|
||||
|
||||
list_display = ('part', 'overall_min', 'overall_max')
|
||||
|
||||
@ -264,13 +264,13 @@ class PartPricingAdmin(admin.ModelAdmin):
|
||||
|
||||
|
||||
class PartStocktakeAdmin(admin.ModelAdmin):
|
||||
"""Admin class for PartStocktake model"""
|
||||
"""Admin class for PartStocktake model."""
|
||||
|
||||
list_display = ['part', 'date', 'quantity', 'user']
|
||||
|
||||
|
||||
class PartStocktakeReportAdmin(admin.ModelAdmin):
|
||||
"""Admin class for PartStocktakeReport model"""
|
||||
"""Admin class for PartStocktakeReport model."""
|
||||
|
||||
list_display = ['date', 'user']
|
||||
|
||||
@ -279,7 +279,7 @@ class PartCategoryResource(InvenTreeResource):
|
||||
"""Class for managing PartCategory data import/export."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass definition"""
|
||||
"""Metaclass options."""
|
||||
|
||||
model = models.PartCategory
|
||||
skip_unchanged = True
|
||||
@ -326,7 +326,7 @@ class PartCategoryResource(InvenTreeResource):
|
||||
)
|
||||
|
||||
def after_import(self, dataset, result, using_transactions, dry_run, **kwargs):
|
||||
"""Rebuild MPTT tree structure after importing PartCategory data"""
|
||||
"""Rebuild MPTT tree structure after importing PartCategory data."""
|
||||
super().after_import(dataset, result, using_transactions, dry_run, **kwargs)
|
||||
|
||||
# Rebuild the PartCategory tree(s)
|
||||
@ -334,7 +334,7 @@ class PartCategoryResource(InvenTreeResource):
|
||||
|
||||
|
||||
class PartCategoryAdmin(ImportExportModelAdmin):
|
||||
"""Admin class for the PartCategory model"""
|
||||
"""Admin class for the PartCategory model."""
|
||||
|
||||
resource_class = PartCategoryResource
|
||||
|
||||
@ -352,7 +352,7 @@ class PartRelatedAdmin(admin.ModelAdmin):
|
||||
|
||||
|
||||
class PartAttachmentAdmin(admin.ModelAdmin):
|
||||
"""Admin class for the PartAttachment model"""
|
||||
"""Admin class for the PartAttachment model."""
|
||||
|
||||
list_display = ('part', 'attachment', 'comment')
|
||||
|
||||
@ -360,7 +360,7 @@ class PartAttachmentAdmin(admin.ModelAdmin):
|
||||
|
||||
|
||||
class PartTestTemplateAdmin(admin.ModelAdmin):
|
||||
"""Admin class for the PartTestTemplate model"""
|
||||
"""Admin class for the PartTestTemplate model."""
|
||||
|
||||
list_display = ('part', 'test_name', 'required')
|
||||
|
||||
@ -371,7 +371,7 @@ class BomItemResource(InvenTreeResource):
|
||||
"""Class for managing BomItem data import/export."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass definition"""
|
||||
"""Metaclass options."""
|
||||
|
||||
model = models.BomItem
|
||||
skip_unchanged = True
|
||||
@ -431,28 +431,28 @@ class BomItemResource(InvenTreeResource):
|
||||
)
|
||||
|
||||
def dehydrate_min_cost(self, item):
|
||||
"""Render minimum cost value for the BOM line item"""
|
||||
"""Render minimum cost value for the BOM line item."""
|
||||
min_price = item.sub_part.pricing.overall_min if item.sub_part.pricing else None
|
||||
|
||||
if min_price is not None:
|
||||
return float(min_price.amount) * float(item.quantity)
|
||||
|
||||
def dehydrate_max_cost(self, item):
|
||||
"""Render maximum cost value for the BOM line item"""
|
||||
"""Render maximum cost value for the BOM line item."""
|
||||
max_price = item.sub_part.pricing.overall_max if item.sub_part.pricing else None
|
||||
|
||||
if max_price is not None:
|
||||
return float(max_price.amount) * float(item.quantity)
|
||||
|
||||
def dehydrate_quantity(self, item):
|
||||
"""Special consideration for the 'quantity' field on data export. We do not want a spreadsheet full of "1.0000" (we'd rather "1")
|
||||
"""Special consideration for the 'quantity' field on data export. We do not want a spreadsheet full of "1.0000" (we'd rather "1").
|
||||
|
||||
Ref: https://django-import-export.readthedocs.io/en/latest/getting_started.html#advanced-data-manipulation-on-export
|
||||
"""
|
||||
return float(item.quantity)
|
||||
|
||||
def before_export(self, queryset, *args, **kwargs):
|
||||
"""Perform before exporting data"""
|
||||
"""Perform before exporting data."""
|
||||
self.is_importing = kwargs.get('importing', False)
|
||||
self.include_pricing = kwargs.pop('include_pricing', False)
|
||||
|
||||
@ -496,7 +496,7 @@ class BomItemResource(InvenTreeResource):
|
||||
|
||||
|
||||
class BomItemAdmin(ImportExportModelAdmin):
|
||||
"""Admin class for the BomItem model"""
|
||||
"""Admin class for the BomItem model."""
|
||||
|
||||
resource_class = BomItemResource
|
||||
|
||||
@ -513,13 +513,13 @@ class BomItemAdmin(ImportExportModelAdmin):
|
||||
|
||||
|
||||
class ParameterTemplateResource(InvenTreeResource):
|
||||
"""Class for managing ParameterTemplate import/export"""
|
||||
"""Class for managing ParameterTemplate import/export."""
|
||||
|
||||
# The following fields will be converted from None to ''
|
||||
CONVERT_NULL_FIELDS = ['choices', 'units']
|
||||
|
||||
class Meta:
|
||||
"""Metaclass definition"""
|
||||
"""Metaclass options."""
|
||||
|
||||
model = models.PartParameterTemplate
|
||||
skip_unchanged = True
|
||||
@ -530,7 +530,7 @@ class ParameterTemplateResource(InvenTreeResource):
|
||||
|
||||
|
||||
class ParameterTemplateAdmin(ImportExportModelAdmin):
|
||||
"""Admin class for the PartParameterTemplate model"""
|
||||
"""Admin class for the PartParameterTemplate model."""
|
||||
|
||||
resource_class = ParameterTemplateResource
|
||||
|
||||
@ -543,7 +543,7 @@ class ParameterResource(InvenTreeResource):
|
||||
"""Class for managing PartParameter data import/export."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass definition"""
|
||||
"""Metaclass options."""
|
||||
|
||||
model = models.PartParameter
|
||||
skip_unchanged = True
|
||||
@ -563,7 +563,7 @@ class ParameterResource(InvenTreeResource):
|
||||
|
||||
|
||||
class ParameterAdmin(ImportExportModelAdmin):
|
||||
"""Admin class for the PartParameter model"""
|
||||
"""Admin class for the PartParameter model."""
|
||||
|
||||
resource_class = ParameterResource
|
||||
|
||||
@ -573,16 +573,16 @@ class ParameterAdmin(ImportExportModelAdmin):
|
||||
|
||||
|
||||
class PartCategoryParameterAdmin(admin.ModelAdmin):
|
||||
"""Admin class for the PartCategoryParameterTemplate model"""
|
||||
"""Admin class for the PartCategoryParameterTemplate model."""
|
||||
|
||||
autocomplete_fields = ('category', 'parameter_template')
|
||||
|
||||
|
||||
class PartSellPriceBreakAdmin(admin.ModelAdmin):
|
||||
"""Admin class for the PartSellPriceBreak model"""
|
||||
"""Admin class for the PartSellPriceBreak model."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass definition"""
|
||||
"""Metaclass options."""
|
||||
|
||||
model = models.PartSellPriceBreak
|
||||
|
||||
@ -590,10 +590,10 @@ class PartSellPriceBreakAdmin(admin.ModelAdmin):
|
||||
|
||||
|
||||
class PartInternalPriceBreakAdmin(admin.ModelAdmin):
|
||||
"""Admin class for the PartInternalPriceBreak model"""
|
||||
"""Admin class for the PartInternalPriceBreak model."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass definition"""
|
||||
"""Metaclass options."""
|
||||
|
||||
model = models.PartInternalPriceBreak
|
||||
|
||||
|
@ -77,19 +77,19 @@ from .models import (
|
||||
|
||||
|
||||
class CategoryMixin:
|
||||
"""Mixin class for PartCategory endpoints"""
|
||||
"""Mixin class for PartCategory endpoints."""
|
||||
|
||||
serializer_class = part_serializers.CategorySerializer
|
||||
queryset = PartCategory.objects.all()
|
||||
|
||||
def get_queryset(self, *args, **kwargs):
|
||||
"""Return an annotated queryset for the CategoryDetail endpoint"""
|
||||
"""Return an annotated queryset for the CategoryDetail endpoint."""
|
||||
queryset = super().get_queryset(*args, **kwargs)
|
||||
queryset = part_serializers.CategorySerializer.annotate_queryset(queryset)
|
||||
return queryset
|
||||
|
||||
def get_serializer_context(self):
|
||||
"""Add extra context to the serializer for the CategoryDetail endpoint"""
|
||||
"""Add extra context to the serializer for the CategoryDetail endpoint."""
|
||||
ctx = super().get_serializer_context()
|
||||
|
||||
try:
|
||||
@ -111,7 +111,7 @@ class CategoryList(CategoryMixin, APIDownloadMixin, ListCreateAPI):
|
||||
"""
|
||||
|
||||
def download_queryset(self, queryset, export_format):
|
||||
"""Download the filtered queryset as a data file"""
|
||||
"""Download the filtered queryset as a data file."""
|
||||
dataset = PartCategoryResource().export(queryset=queryset)
|
||||
filedata = dataset.export(export_format)
|
||||
filename = f'InvenTree_Categories.{export_format}'
|
||||
@ -119,8 +119,9 @@ class CategoryList(CategoryMixin, APIDownloadMixin, ListCreateAPI):
|
||||
return DownloadFile(filedata, filename)
|
||||
|
||||
def filter_queryset(self, queryset):
|
||||
"""Custom filtering:
|
||||
"""Custom filtering.
|
||||
|
||||
Rules:
|
||||
- Allow filtering by "null" parent to retrieve top-level part categories
|
||||
"""
|
||||
queryset = super().filter_queryset(queryset)
|
||||
@ -208,7 +209,7 @@ class CategoryDetail(CategoryMixin, CustomRetrieveUpdateDestroyAPI):
|
||||
"""API endpoint for detail view of a single PartCategory object."""
|
||||
|
||||
def get_serializer(self, *args, **kwargs):
|
||||
"""Add additional context based on query parameters"""
|
||||
"""Add additional context based on query parameters."""
|
||||
try:
|
||||
params = self.request.query_params
|
||||
|
||||
@ -219,7 +220,7 @@ class CategoryDetail(CategoryMixin, CustomRetrieveUpdateDestroyAPI):
|
||||
return self.serializer_class(*args, **kwargs)
|
||||
|
||||
def update(self, request, *args, **kwargs):
|
||||
"""Perform 'update' function and mark this part as 'starred' (or not)"""
|
||||
"""Perform 'update' function and mark this part as 'starred' (or not)."""
|
||||
# Clean up input data
|
||||
data = self.clean_data(request.data)
|
||||
|
||||
@ -233,7 +234,7 @@ class CategoryDetail(CategoryMixin, CustomRetrieveUpdateDestroyAPI):
|
||||
return response
|
||||
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
"""Delete a Part category instance via the API"""
|
||||
"""Delete a Part category instance via the API."""
|
||||
delete_parts = (
|
||||
'delete_parts' in request.data and request.data['delete_parts'] == '1'
|
||||
)
|
||||
@ -274,8 +275,9 @@ class CategoryParameterList(ListCreateAPI):
|
||||
serializer_class = part_serializers.CategoryParameterTemplateSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
"""Custom filtering:
|
||||
"""Custom filtering.
|
||||
|
||||
Rules:
|
||||
- Allow filtering by "null" parent to retrieve all categories parameter templates
|
||||
- Allow filtering by category
|
||||
- Allow traversing all parent categories
|
||||
@ -305,7 +307,7 @@ class CategoryParameterList(ListCreateAPI):
|
||||
|
||||
|
||||
class CategoryParameterDetail(RetrieveUpdateDestroyAPI):
|
||||
"""Detail endpoint for the PartCategoryParameterTemplate model"""
|
||||
"""Detail endpoint for the PartCategoryParameterTemplate model."""
|
||||
|
||||
queryset = PartCategoryParameterTemplate.objects.all()
|
||||
serializer_class = part_serializers.CategoryParameterTemplateSerializer
|
||||
@ -419,7 +421,7 @@ class PartThumbs(ListAPI):
|
||||
serializer_class = part_serializers.PartThumbSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
"""Return a queryset which excludes any parts without images"""
|
||||
"""Return a queryset which excludes any parts without images."""
|
||||
queryset = super().get_queryset()
|
||||
|
||||
# Get all Parts which have an associated image
|
||||
@ -477,7 +479,7 @@ class PartScheduling(RetrieveAPI):
|
||||
queryset = Part.objects.all()
|
||||
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
"""Return scheduling information for the referenced Part instance"""
|
||||
"""Return scheduling information for the referenced Part instance."""
|
||||
part = self.get_object()
|
||||
|
||||
schedule = []
|
||||
@ -485,13 +487,13 @@ class PartScheduling(RetrieveAPI):
|
||||
def add_schedule_entry(
|
||||
date, quantity, title, label, url, speculative_quantity=0
|
||||
):
|
||||
"""Check if a scheduled entry should be added:
|
||||
"""Check if a scheduled entry should be added.
|
||||
|
||||
Rules:
|
||||
- date must be non-null
|
||||
- date cannot be in the "past"
|
||||
- quantity must not be zero
|
||||
"""
|
||||
|
||||
schedule.append({
|
||||
'date': date,
|
||||
'quantity': quantity,
|
||||
@ -646,7 +648,6 @@ class PartScheduling(RetrieveAPI):
|
||||
|
||||
Account for the fact that either date might be None
|
||||
"""
|
||||
|
||||
date_1 = entry_1['date']
|
||||
date_2 = entry_2['date']
|
||||
|
||||
@ -678,7 +679,7 @@ class PartRequirements(RetrieveAPI):
|
||||
queryset = Part.objects.all()
|
||||
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
"""Construct a response detailing Part requirements"""
|
||||
"""Construct a response detailing Part requirements."""
|
||||
part = self.get_object()
|
||||
|
||||
data = {
|
||||
@ -705,18 +706,18 @@ class PartRequirements(RetrieveAPI):
|
||||
|
||||
|
||||
class PartPricingDetail(RetrieveUpdateAPI):
|
||||
"""API endpoint for viewing part pricing data"""
|
||||
"""API endpoint for viewing part pricing data."""
|
||||
|
||||
serializer_class = part_serializers.PartPricingSerializer
|
||||
queryset = Part.objects.all()
|
||||
|
||||
def get_object(self):
|
||||
"""Return the PartPricing object associated with the linked Part"""
|
||||
"""Return the PartPricing object associated with the linked Part."""
|
||||
part = super().get_object()
|
||||
return part.pricing
|
||||
|
||||
def _get_serializer(self, *args, **kwargs):
|
||||
"""Return a part pricing serializer object"""
|
||||
"""Return a part pricing serializer object."""
|
||||
part = self.get_object()
|
||||
kwargs['instance'] = part.pricing
|
||||
|
||||
@ -729,7 +730,7 @@ class PartSerialNumberDetail(RetrieveAPI):
|
||||
queryset = Part.objects.all()
|
||||
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
"""Return serial number information for the referenced Part instance"""
|
||||
"""Return serial number information for the referenced Part instance."""
|
||||
part = self.get_object()
|
||||
|
||||
# Calculate the "latest" serial number
|
||||
@ -753,7 +754,7 @@ class PartCopyBOM(CreateAPI):
|
||||
serializer_class = part_serializers.PartCopyBOMSerializer
|
||||
|
||||
def get_serializer_context(self):
|
||||
"""Add custom information to the serializer context for this endpoint"""
|
||||
"""Add custom information to the serializer context for this endpoint."""
|
||||
ctx = super().get_serializer_context()
|
||||
|
||||
try:
|
||||
@ -768,10 +769,10 @@ class PartValidateBOM(RetrieveUpdateAPI):
|
||||
"""API endpoint for 'validating' the BOM for a given Part."""
|
||||
|
||||
class BOMValidateSerializer(serializers.ModelSerializer):
|
||||
"""Simple serializer class for validating a single BomItem instance"""
|
||||
"""Simple serializer class for validating a single BomItem instance."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass defines serializer fields"""
|
||||
"""Metaclass defines serializer fields."""
|
||||
|
||||
model = Part
|
||||
fields = ['checksum', 'valid']
|
||||
@ -786,7 +787,7 @@ class PartValidateBOM(RetrieveUpdateAPI):
|
||||
)
|
||||
|
||||
def validate_valid(self, valid):
|
||||
"""Check that the 'valid' input was flagged"""
|
||||
"""Check that the 'valid' input was flagged."""
|
||||
if not valid:
|
||||
raise ValidationError(_('This option must be selected'))
|
||||
|
||||
@ -795,7 +796,7 @@ class PartValidateBOM(RetrieveUpdateAPI):
|
||||
serializer_class = BOMValidateSerializer
|
||||
|
||||
def update(self, request, *args, **kwargs):
|
||||
"""Validate the referenced BomItem instance"""
|
||||
"""Validate the referenced BomItem instance."""
|
||||
part = self.get_object()
|
||||
|
||||
partial = kwargs.pop('partial', False)
|
||||
@ -818,7 +819,7 @@ class PartFilter(rest_filters.FilterSet):
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options for this filter set"""
|
||||
"""Metaclass options for this filter set."""
|
||||
|
||||
model = Part
|
||||
fields = []
|
||||
@ -826,7 +827,7 @@ class PartFilter(rest_filters.FilterSet):
|
||||
has_units = rest_filters.BooleanFilter(label='Has units', method='filter_has_units')
|
||||
|
||||
def filter_has_units(self, queryset, name, value):
|
||||
"""Filter by whether the Part has units or not"""
|
||||
"""Filter by whether the Part has units or not."""
|
||||
if str2bool(value):
|
||||
return queryset.exclude(Q(units=None) | Q(units=''))
|
||||
|
||||
@ -836,7 +837,7 @@ class PartFilter(rest_filters.FilterSet):
|
||||
has_ipn = rest_filters.BooleanFilter(label='Has IPN', method='filter_has_ipn')
|
||||
|
||||
def filter_has_ipn(self, queryset, name, value):
|
||||
"""Filter by whether the Part has an IPN (internal part number) or not"""
|
||||
"""Filter by whether the Part has an IPN (internal part number) or not."""
|
||||
if str2bool(value):
|
||||
return queryset.exclude(IPN='')
|
||||
return queryset.filter(IPN='')
|
||||
@ -878,7 +879,7 @@ class PartFilter(rest_filters.FilterSet):
|
||||
has_stock = rest_filters.BooleanFilter(label='Has stock', method='filter_has_stock')
|
||||
|
||||
def filter_has_stock(self, queryset, name, value):
|
||||
"""Filter by whether the Part has any stock"""
|
||||
"""Filter by whether the Part has any stock."""
|
||||
if str2bool(value):
|
||||
return queryset.filter(Q(in_stock__gt=0))
|
||||
return queryset.filter(Q(in_stock__lte=0))
|
||||
@ -889,7 +890,7 @@ class PartFilter(rest_filters.FilterSet):
|
||||
)
|
||||
|
||||
def filter_unallocated_stock(self, queryset, name, value):
|
||||
"""Filter by whether the Part has unallocated stock"""
|
||||
"""Filter by whether the Part has unallocated stock."""
|
||||
if str2bool(value):
|
||||
return queryset.filter(Q(unallocated_stock__gt=0))
|
||||
return queryset.filter(Q(unallocated_stock__lte=0))
|
||||
@ -901,7 +902,7 @@ class PartFilter(rest_filters.FilterSet):
|
||||
)
|
||||
|
||||
def filter_convert_from(self, queryset, name, part):
|
||||
"""Limit the queryset to valid conversion options for the specified part"""
|
||||
"""Limit the queryset to valid conversion options for the specified part."""
|
||||
conversion_options = part.get_conversion_options()
|
||||
|
||||
queryset = queryset.filter(pk__in=conversion_options)
|
||||
@ -915,7 +916,7 @@ class PartFilter(rest_filters.FilterSet):
|
||||
)
|
||||
|
||||
def filter_exclude_tree(self, queryset, name, part):
|
||||
"""Exclude all parts and variants 'down' from the specified part from the queryset"""
|
||||
"""Exclude all parts and variants 'down' from the specified part from the queryset."""
|
||||
children = part.get_descendants(include_self=True)
|
||||
|
||||
return queryset.exclude(id__in=children)
|
||||
@ -925,7 +926,7 @@ class PartFilter(rest_filters.FilterSet):
|
||||
)
|
||||
|
||||
def filter_ancestor(self, queryset, name, part):
|
||||
"""Limit queryset to descendants of the specified ancestor part"""
|
||||
"""Limit queryset to descendants of the specified ancestor part."""
|
||||
descendants = part.get_descendants(include_self=False)
|
||||
return queryset.filter(id__in=descendants)
|
||||
|
||||
@ -934,7 +935,7 @@ class PartFilter(rest_filters.FilterSet):
|
||||
)
|
||||
|
||||
def filter_variant_of(self, queryset, name, part):
|
||||
"""Limit queryset to direct children (variants) of the specified part"""
|
||||
"""Limit queryset to direct children (variants) of the specified part."""
|
||||
return queryset.filter(id__in=part.get_children())
|
||||
|
||||
in_bom_for = rest_filters.ModelChoiceFilter(
|
||||
@ -942,7 +943,7 @@ class PartFilter(rest_filters.FilterSet):
|
||||
)
|
||||
|
||||
def filter_in_bom(self, queryset, name, part):
|
||||
"""Limit queryset to parts in the BOM for the specified part"""
|
||||
"""Limit queryset to parts in the BOM for the specified part."""
|
||||
bom_parts = part.get_parts_in_bom()
|
||||
return queryset.filter(id__in=[p.pk for p in bom_parts])
|
||||
|
||||
@ -951,7 +952,7 @@ class PartFilter(rest_filters.FilterSet):
|
||||
)
|
||||
|
||||
def filter_has_pricing(self, queryset, name, value):
|
||||
"""Filter the queryset based on whether pricing information is available for the sub_part"""
|
||||
"""Filter the queryset based on whether pricing information is available for the sub_part."""
|
||||
q_a = Q(pricing_data=None)
|
||||
q_b = Q(pricing_data__overall_min=None, pricing_data__overall_max=None)
|
||||
|
||||
@ -965,7 +966,7 @@ class PartFilter(rest_filters.FilterSet):
|
||||
)
|
||||
|
||||
def filter_has_stocktake(self, queryset, name, value):
|
||||
"""Filter the queryset based on whether stocktake data is available"""
|
||||
"""Filter the queryset based on whether stocktake data is available."""
|
||||
if str2bool(value):
|
||||
return queryset.exclude(last_stocktake=None)
|
||||
return queryset.filter(last_stocktake=None)
|
||||
@ -975,7 +976,7 @@ class PartFilter(rest_filters.FilterSet):
|
||||
)
|
||||
|
||||
def filter_stock_to_build(self, queryset, name, value):
|
||||
"""Filter the queryset based on whether part stock is required for a pending BuildOrder"""
|
||||
"""Filter the queryset based on whether part stock is required for a pending BuildOrder."""
|
||||
if str2bool(value):
|
||||
# Return parts which are required for a build order, but have not yet been allocated
|
||||
return queryset.filter(
|
||||
@ -991,7 +992,7 @@ class PartFilter(rest_filters.FilterSet):
|
||||
)
|
||||
|
||||
def filter_depleted_stock(self, queryset, name, value):
|
||||
"""Filter the queryset based on whether the part is fully depleted of stock"""
|
||||
"""Filter the queryset based on whether the part is fully depleted of stock."""
|
||||
if str2bool(value):
|
||||
return queryset.filter(Q(in_stock=0) & ~Q(stock_item_count=0))
|
||||
return queryset.exclude(Q(in_stock=0) & ~Q(stock_item_count=0))
|
||||
@ -1030,7 +1031,7 @@ class PartFilter(rest_filters.FilterSet):
|
||||
|
||||
|
||||
class PartMixin:
|
||||
"""Mixin class for Part API endpoints"""
|
||||
"""Mixin class for Part API endpoints."""
|
||||
|
||||
serializer_class = part_serializers.PartSerializer
|
||||
queryset = Part.objects.all()
|
||||
@ -1040,7 +1041,7 @@ class PartMixin:
|
||||
is_create = False
|
||||
|
||||
def get_queryset(self, *args, **kwargs):
|
||||
"""Return an annotated queryset object for the PartDetail endpoint"""
|
||||
"""Return an annotated queryset object for the PartDetail endpoint."""
|
||||
queryset = super().get_queryset(*args, **kwargs)
|
||||
|
||||
queryset = part_serializers.PartSerializer.annotate_queryset(queryset)
|
||||
@ -1048,7 +1049,7 @@ class PartMixin:
|
||||
return queryset
|
||||
|
||||
def get_serializer(self, *args, **kwargs):
|
||||
"""Return a serializer instance for this endpoint"""
|
||||
"""Return a serializer instance for this endpoint."""
|
||||
# Ensure the request context is passed through
|
||||
kwargs['context'] = self.get_serializer_context()
|
||||
|
||||
@ -1077,7 +1078,7 @@ class PartMixin:
|
||||
return self.serializer_class(*args, **kwargs)
|
||||
|
||||
def get_serializer_context(self):
|
||||
"""Extend serializer context data"""
|
||||
"""Extend serializer context data."""
|
||||
context = super().get_serializer_context()
|
||||
context['request'] = self.request
|
||||
|
||||
@ -1085,13 +1086,13 @@ class PartMixin:
|
||||
|
||||
|
||||
class PartList(PartMixin, APIDownloadMixin, ListCreateAPI):
|
||||
"""API endpoint for accessing a list of Part objects, or creating a new Part instance"""
|
||||
"""API endpoint for accessing a list of Part objects, or creating a new Part instance."""
|
||||
|
||||
filterset_class = PartFilter
|
||||
is_create = True
|
||||
|
||||
def download_queryset(self, queryset, export_format):
|
||||
"""Download the filtered queryset as a data file"""
|
||||
"""Download the filtered queryset as a data file."""
|
||||
dataset = PartResource().export(queryset=queryset)
|
||||
|
||||
filedata = dataset.export(export_format)
|
||||
@ -1127,7 +1128,7 @@ class PartList(PartMixin, APIDownloadMixin, ListCreateAPI):
|
||||
return Response(data)
|
||||
|
||||
def filter_queryset(self, queryset):
|
||||
"""Perform custom filtering of the queryset"""
|
||||
"""Perform custom filtering of the queryset."""
|
||||
params = self.request.query_params
|
||||
|
||||
queryset = super().filter_queryset(queryset)
|
||||
@ -1311,7 +1312,7 @@ class PartList(PartMixin, APIDownloadMixin, ListCreateAPI):
|
||||
|
||||
|
||||
class PartChangeCategory(CreateAPI):
|
||||
"""API endpoint to change the location of multiple parts in bulk"""
|
||||
"""API endpoint to change the location of multiple parts in bulk."""
|
||||
|
||||
serializer_class = part_serializers.PartSetCategorySerializer
|
||||
queryset = Part.objects.none()
|
||||
@ -1321,7 +1322,7 @@ class PartDetail(PartMixin, RetrieveUpdateDestroyAPI):
|
||||
"""API endpoint for detail view of a single Part object."""
|
||||
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
"""Delete a Part instance via the API
|
||||
"""Delete a Part instance via the API.
|
||||
|
||||
- If the part is 'active' it cannot be deleted
|
||||
- It must first be marked as 'inactive'
|
||||
@ -1360,7 +1361,7 @@ class PartRelatedList(ListCreateAPI):
|
||||
serializer_class = part_serializers.PartRelationSerializer
|
||||
|
||||
def filter_queryset(self, queryset):
|
||||
"""Custom queryset filtering"""
|
||||
"""Custom queryset filtering."""
|
||||
queryset = super().filter_queryset(queryset)
|
||||
|
||||
params = self.request.query_params
|
||||
@ -1390,7 +1391,7 @@ class PartParameterTemplateFilter(rest_filters.FilterSet):
|
||||
"""FilterSet for PartParameterTemplate objects."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options"""
|
||||
"""Metaclass options."""
|
||||
|
||||
model = PartParameterTemplate
|
||||
|
||||
@ -1472,7 +1473,7 @@ class PartParameterTemplateList(ListCreateAPI):
|
||||
|
||||
|
||||
class PartParameterTemplateDetail(RetrieveUpdateDestroyAPI):
|
||||
"""API endpoint for accessing the detail view for a PartParameterTemplate object"""
|
||||
"""API endpoint for accessing the detail view for a PartParameterTemplate object."""
|
||||
|
||||
queryset = PartParameterTemplate.objects.all()
|
||||
serializer_class = part_serializers.PartParameterTemplateSerializer
|
||||
@ -1485,7 +1486,7 @@ class PartParameterAPIMixin:
|
||||
serializer_class = part_serializers.PartParameterSerializer
|
||||
|
||||
def get_queryset(self, *args, **kwargs):
|
||||
"""Override get_queryset method to prefetch related fields"""
|
||||
"""Override get_queryset method to prefetch related fields."""
|
||||
queryset = super().get_queryset(*args, **kwargs)
|
||||
queryset = queryset.prefetch_related('part', 'template')
|
||||
return queryset
|
||||
@ -1509,10 +1510,10 @@ class PartParameterAPIMixin:
|
||||
|
||||
|
||||
class PartParameterFilter(rest_filters.FilterSet):
|
||||
"""Custom filters for the PartParameterList API endpoint"""
|
||||
"""Custom filters for the PartParameterList API endpoint."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options for the filterset"""
|
||||
"""Metaclass options for the filterset."""
|
||||
|
||||
model = PartParameter
|
||||
fields = ['template']
|
||||
@ -1570,24 +1571,24 @@ class PartParameterDetail(PartParameterAPIMixin, RetrieveUpdateDestroyAPI):
|
||||
|
||||
|
||||
class PartStocktakeFilter(rest_filters.FilterSet):
|
||||
"""Custom filter for the PartStocktakeList endpoint"""
|
||||
"""Custom filter for the PartStocktakeList endpoint."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options"""
|
||||
"""Metaclass options."""
|
||||
|
||||
model = PartStocktake
|
||||
fields = ['part', 'user']
|
||||
|
||||
|
||||
class PartStocktakeList(ListCreateAPI):
|
||||
"""API endpoint for listing part stocktake information"""
|
||||
"""API endpoint for listing part stocktake information."""
|
||||
|
||||
queryset = PartStocktake.objects.all()
|
||||
serializer_class = part_serializers.PartStocktakeSerializer
|
||||
filterset_class = PartStocktakeFilter
|
||||
|
||||
def get_serializer_context(self):
|
||||
"""Extend serializer context data"""
|
||||
"""Extend serializer context data."""
|
||||
context = super().get_serializer_context()
|
||||
context['request'] = self.request
|
||||
|
||||
@ -1612,7 +1613,7 @@ class PartStocktakeDetail(RetrieveUpdateDestroyAPI):
|
||||
|
||||
|
||||
class PartStocktakeReportList(ListAPI):
|
||||
"""API endpoint for listing part stocktake report information"""
|
||||
"""API endpoint for listing part stocktake report information."""
|
||||
|
||||
queryset = PartStocktakeReport.objects.all()
|
||||
serializer_class = part_serializers.PartStocktakeReportSerializer
|
||||
@ -1626,7 +1627,7 @@ class PartStocktakeReportList(ListAPI):
|
||||
|
||||
|
||||
class PartStocktakeReportGenerate(CreateAPI):
|
||||
"""API endpoint for manually generating a new PartStocktakeReport"""
|
||||
"""API endpoint for manually generating a new PartStocktakeReport."""
|
||||
|
||||
serializer_class = part_serializers.PartStocktakeReportGenerateSerializer
|
||||
|
||||
@ -1635,7 +1636,7 @@ class PartStocktakeReportGenerate(CreateAPI):
|
||||
role_required = 'stocktake'
|
||||
|
||||
def get_serializer_context(self):
|
||||
"""Extend serializer context data"""
|
||||
"""Extend serializer context data."""
|
||||
context = super().get_serializer_context()
|
||||
context['request'] = self.request
|
||||
|
||||
@ -1646,7 +1647,7 @@ class BomFilter(rest_filters.FilterSet):
|
||||
"""Custom filters for the BOM list."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options"""
|
||||
"""Metaclass options."""
|
||||
|
||||
model = BomItem
|
||||
fields = ['optional', 'consumable', 'inherited', 'allow_variants', 'validated']
|
||||
@ -1672,7 +1673,7 @@ class BomFilter(rest_filters.FilterSet):
|
||||
)
|
||||
|
||||
def filter_available_stock(self, queryset, name, value):
|
||||
"""Filter the queryset based on whether each line item has any available stock"""
|
||||
"""Filter the queryset based on whether each line item has any available stock."""
|
||||
if str2bool(value):
|
||||
return queryset.filter(available_stock__gt=0)
|
||||
return queryset.filter(available_stock=0)
|
||||
@ -1680,7 +1681,7 @@ class BomFilter(rest_filters.FilterSet):
|
||||
on_order = rest_filters.BooleanFilter(label='On order', method='filter_on_order')
|
||||
|
||||
def filter_on_order(self, queryset, name, value):
|
||||
"""Filter the queryset based on whether each line item has any stock on order"""
|
||||
"""Filter the queryset based on whether each line item has any stock on order."""
|
||||
if str2bool(value):
|
||||
return queryset.filter(on_order__gt=0)
|
||||
return queryset.filter(on_order=0)
|
||||
@ -1690,7 +1691,7 @@ class BomFilter(rest_filters.FilterSet):
|
||||
)
|
||||
|
||||
def filter_has_pricing(self, queryset, name, value):
|
||||
"""Filter the queryset based on whether pricing information is available for the sub_part"""
|
||||
"""Filter the queryset based on whether pricing information is available for the sub_part."""
|
||||
q_a = Q(sub_part__pricing_data=None)
|
||||
q_b = Q(
|
||||
sub_part__pricing_data__overall_min=None,
|
||||
@ -1704,13 +1705,13 @@ class BomFilter(rest_filters.FilterSet):
|
||||
|
||||
|
||||
class BomMixin:
|
||||
"""Mixin class for BomItem API endpoints"""
|
||||
"""Mixin class for BomItem API endpoints."""
|
||||
|
||||
serializer_class = part_serializers.BomItemSerializer
|
||||
queryset = BomItem.objects.all()
|
||||
|
||||
def get_serializer(self, *args, **kwargs):
|
||||
"""Return the serializer instance for this API endpoint
|
||||
"""Return the serializer instance for this API endpoint.
|
||||
|
||||
If requested, extra detail fields are annotated to the queryset:
|
||||
- part_detail
|
||||
@ -1735,7 +1736,7 @@ class BomMixin:
|
||||
return self.serializer_class(*args, **kwargs)
|
||||
|
||||
def get_queryset(self, *args, **kwargs):
|
||||
"""Return the queryset object for this endpoint"""
|
||||
"""Return the queryset object for this endpoint."""
|
||||
queryset = super().get_queryset(*args, **kwargs)
|
||||
|
||||
queryset = self.get_serializer_class().setup_eager_loading(queryset)
|
||||
@ -1754,7 +1755,7 @@ class BomList(BomMixin, ListCreateDestroyAPIView):
|
||||
filterset_class = BomFilter
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
"""Return serialized list response for this endpoint"""
|
||||
"""Return serialized list response for this endpoint."""
|
||||
queryset = self.filter_queryset(self.get_queryset())
|
||||
|
||||
page = self.paginate_queryset(queryset)
|
||||
@ -1778,7 +1779,7 @@ class BomList(BomMixin, ListCreateDestroyAPIView):
|
||||
return Response(data)
|
||||
|
||||
def filter_queryset(self, queryset):
|
||||
"""Custom query filtering for the BomItem list API"""
|
||||
"""Custom query filtering for the BomItem list API."""
|
||||
queryset = super().filter_queryset(queryset)
|
||||
|
||||
params = self.request.query_params
|
||||
@ -1906,7 +1907,7 @@ class BomItemValidate(UpdateAPI):
|
||||
"""API endpoint for validating a BomItem."""
|
||||
|
||||
class BomItemValidationSerializer(serializers.Serializer):
|
||||
"""Simple serializer for passing a single boolean field"""
|
||||
"""Simple serializer for passing a single boolean field."""
|
||||
|
||||
valid = serializers.BooleanField(default=False)
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
"""part app specification"""
|
||||
"""part app specification."""
|
||||
|
||||
import logging
|
||||
|
||||
@ -11,7 +11,7 @@ logger = logging.getLogger('inventree')
|
||||
|
||||
|
||||
class PartConfig(AppConfig):
|
||||
"""Config class for the 'part' app"""
|
||||
"""Config class for the 'part' app."""
|
||||
|
||||
name = 'part'
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
"""Custom query filters for the Part models
|
||||
"""Custom query filters for the Part models.
|
||||
|
||||
The code here makes heavy use of subquery annotations!
|
||||
|
||||
@ -89,7 +89,7 @@ def annotate_on_order_quantity(reference: str = ''):
|
||||
|
||||
|
||||
def annotate_total_stock(reference: str = ''):
|
||||
"""Annotate 'total stock' quantity against a queryset:
|
||||
"""Annotate 'total stock' quantity against a queryset.
|
||||
|
||||
- This function calculates the 'total stock' for a given part
|
||||
- Finds all stock items associated with each part (using the provided filter)
|
||||
@ -128,7 +128,7 @@ def annotate_build_order_requirements(reference: str = ''):
|
||||
|
||||
|
||||
def annotate_build_order_allocations(reference: str = ''):
|
||||
"""Annotate the total quantity of each part allocated to build orders:
|
||||
"""Annotate the total quantity of each part allocated to build orders.
|
||||
|
||||
- This function calculates the total part quantity allocated to open build orders
|
||||
- Finds all build order allocations for each part (using the provided filter)
|
||||
@ -151,7 +151,7 @@ def annotate_build_order_allocations(reference: str = ''):
|
||||
|
||||
|
||||
def annotate_sales_order_allocations(reference: str = ''):
|
||||
"""Annotate the total quantity of each part allocated to sales orders:
|
||||
"""Annotate the total quantity of each part allocated to sales orders.
|
||||
|
||||
- This function calculates the total part quantity allocated to open sales orders"
|
||||
- Finds all sales order allocations for each part (using the provided filter)
|
||||
@ -180,7 +180,7 @@ def annotate_sales_order_allocations(reference: str = ''):
|
||||
def variant_stock_query(
|
||||
reference: str = '', filter: Q = stock.models.StockItem.IN_STOCK_FILTER
|
||||
):
|
||||
"""Create a queryset to retrieve all stock items for variant parts under the specified part
|
||||
"""Create a queryset to retrieve all stock items for variant parts under the specified part.
|
||||
|
||||
- Useful for annotating a queryset with aggregated information about variant parts
|
||||
|
||||
@ -196,7 +196,7 @@ def variant_stock_query(
|
||||
|
||||
|
||||
def annotate_variant_quantity(subquery: Q, reference: str = 'quantity'):
|
||||
"""Create a subquery annotation for all variant part stock items on the given parent query
|
||||
"""Create a subquery annotation for all variant part stock items on the given parent query.
|
||||
|
||||
Args:
|
||||
subquery: A 'variant_stock_query' Q object
|
||||
@ -239,15 +239,15 @@ def annotate_category_parts():
|
||||
|
||||
|
||||
def filter_by_parameter(queryset, template_id: int, value: str, func: str = ''):
|
||||
"""Filter the given queryset by a given template parameter
|
||||
"""Filter the given queryset by a given template parameter.
|
||||
|
||||
Parts which do not have a value for the given parameter are excluded.
|
||||
|
||||
Arguments:
|
||||
queryset - A queryset of Part objects
|
||||
template_id - The ID of the template parameter to filter by
|
||||
value - The value of the parameter to filter by
|
||||
func - The function to use for the filter (e.g. __gt, __lt, __contains)
|
||||
queryset: A queryset of Part objects
|
||||
template_id (int): The ID of the template parameter to filter by
|
||||
value (str): The value of the parameter to filter by
|
||||
func (str): The function to use for the filter (e.g. __gt, __lt, __contains)
|
||||
|
||||
Returns:
|
||||
A queryset of Part objects filtered by the given parameter
|
||||
@ -257,13 +257,14 @@ def filter_by_parameter(queryset, template_id: int, value: str, func: str = ''):
|
||||
|
||||
|
||||
def order_by_parameter(queryset, template_id: int, ascending=True):
|
||||
"""Order the given queryset by a given template parameter
|
||||
"""Order the given queryset by a given template parameter.
|
||||
|
||||
Parts which do not have a value for the given parameter are ordered last.
|
||||
|
||||
Arguments:
|
||||
queryset - A queryset of Part objects
|
||||
template_id - The ID of the template parameter to order by
|
||||
queryset: A queryset of Part objects
|
||||
template_id (int): The ID of the template parameter to order by
|
||||
ascending (bool): Order by ascending or descending (default = True)
|
||||
|
||||
Returns:
|
||||
A queryset of Part objects ordered by the given parameter
|
||||
|
@ -37,7 +37,7 @@ class PartPriceForm(forms.Form):
|
||||
"""Simple form for viewing part pricing information."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass defines fields for this form"""
|
||||
"""Metaclass defines fields for this form."""
|
||||
|
||||
model = Part
|
||||
fields = ['quantity']
|
||||
|
@ -1,4 +1,4 @@
|
||||
"""Various helper functions for the part app"""
|
||||
"""Various helper functions for the part app."""
|
||||
|
||||
import logging
|
||||
import os
|
||||
@ -20,7 +20,6 @@ def compile_full_name_template(*args, **kwargs):
|
||||
|
||||
This function is called whenever the 'PART_NAME_FORMAT' setting is changed.
|
||||
"""
|
||||
|
||||
from common.models import InvenTreeSetting
|
||||
|
||||
global _part_full_name_template
|
||||
@ -61,7 +60,6 @@ def render_part_full_name(part) -> str:
|
||||
Args:
|
||||
part: The Part object to render
|
||||
"""
|
||||
|
||||
template = compile_full_name_template()
|
||||
|
||||
if template:
|
||||
@ -91,7 +89,6 @@ def get_part_image_directory() -> str:
|
||||
|
||||
TODO: Future work may be needed here to support other storage backends, such as S3
|
||||
"""
|
||||
|
||||
part_image_directory = os.path.abspath(
|
||||
os.path.join(settings.MEDIA_ROOT, PART_IMAGE_DIR)
|
||||
)
|
||||
|
@ -83,7 +83,7 @@ class PartCategory(MetadataMixin, InvenTreeTree):
|
||||
ITEM_PARENT_KEY = 'category'
|
||||
|
||||
class Meta:
|
||||
"""Metaclass defines extra model properties"""
|
||||
"""Metaclass defines extra model properties."""
|
||||
|
||||
verbose_name = _('Part Category')
|
||||
verbose_name_plural = _('Part Categories')
|
||||
@ -93,7 +93,6 @@ class PartCategory(MetadataMixin, InvenTreeTree):
|
||||
|
||||
This must be handled within a transaction.atomic(), otherwise the tree structure is damaged
|
||||
"""
|
||||
|
||||
super().delete(
|
||||
delete_children=kwargs.get('delete_child_categories', False),
|
||||
delete_items=kwargs.get('delete_parts', False),
|
||||
@ -135,17 +134,17 @@ class PartCategory(MetadataMixin, InvenTreeTree):
|
||||
|
||||
@staticmethod
|
||||
def get_api_url():
|
||||
"""Return the API url associated with the PartCategory model"""
|
||||
"""Return the API url associated with the PartCategory model."""
|
||||
return reverse('api-part-category-list')
|
||||
|
||||
def get_absolute_url(self):
|
||||
"""Return the web URL associated with the detail view for this PartCategory instance"""
|
||||
"""Return the web URL associated with the detail view for this PartCategory instance."""
|
||||
return reverse('category-detail', kwargs={'pk': self.id})
|
||||
|
||||
def clean(self):
|
||||
"""Custom clean action for the PartCategory model:
|
||||
"""Custom clean action for the PartCategory model.
|
||||
|
||||
- Ensure that the structural parameter cannot get set if products already assigned to the category
|
||||
Ensure that the structural parameter cannot get set if products already assigned to the category
|
||||
"""
|
||||
if self.pk and self.structural and self.partcount(False, False) > 0:
|
||||
raise ValidationError(
|
||||
@ -177,11 +176,11 @@ class PartCategory(MetadataMixin, InvenTreeTree):
|
||||
|
||||
@property
|
||||
def item_count(self):
|
||||
"""Return the number of parts contained in this PartCategory"""
|
||||
"""Return the number of parts contained in this PartCategory."""
|
||||
return self.partcount()
|
||||
|
||||
def get_items(self, cascade=False):
|
||||
"""Return a queryset containing the parts which exist in this category"""
|
||||
"""Return a queryset containing the parts which exist in this category."""
|
||||
return self.get_parts(cascade=cascade)
|
||||
|
||||
def partcount(self, cascade=True, active=False):
|
||||
@ -312,7 +311,6 @@ def rename_part_image(instance, filename):
|
||||
Returns:
|
||||
Cleaned filename in format part_<n>_img
|
||||
"""
|
||||
|
||||
base = part_helpers.PART_IMAGE_DIR
|
||||
fname = os.path.basename(filename)
|
||||
|
||||
@ -327,7 +325,7 @@ class PartManager(TreeManager):
|
||||
"""
|
||||
|
||||
def get_queryset(self):
|
||||
"""Perform default prefetch operations when accessing Part model from the database"""
|
||||
"""Perform default prefetch operations when accessing Part model from the database."""
|
||||
return (
|
||||
super()
|
||||
.get_queryset()
|
||||
@ -385,7 +383,7 @@ class Part(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, MPTTModel)
|
||||
tags = TaggableManager(blank=True)
|
||||
|
||||
class Meta:
|
||||
"""Metaclass defines extra model properties"""
|
||||
"""Metaclass defines extra model properties."""
|
||||
|
||||
verbose_name = _('Part')
|
||||
verbose_name_plural = _('Parts')
|
||||
@ -395,14 +393,14 @@ class Part(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, MPTTModel)
|
||||
]
|
||||
|
||||
class MPTTMeta:
|
||||
"""MPTT metaclass definitions"""
|
||||
"""MPTT Metaclass options."""
|
||||
|
||||
# For legacy reasons the 'variant_of' field is used to indicate the MPTT parent
|
||||
parent_attr = 'variant_of'
|
||||
|
||||
@staticmethod
|
||||
def get_api_url():
|
||||
"""Return the list API endpoint URL associated with the Part model"""
|
||||
"""Return the list API endpoint URL associated with the Part model."""
|
||||
return reverse('api-part-list')
|
||||
|
||||
def api_instance_filters(self):
|
||||
@ -481,7 +479,7 @@ class Part(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, MPTTModel)
|
||||
raise ValidationError({'variant_of': _('Invalid choice for parent part')})
|
||||
|
||||
def __str__(self):
|
||||
"""Return a string representation of the Part (for use in the admin interface)"""
|
||||
"""Return a string representation of the Part (for use in the admin interface)."""
|
||||
return f'{self.full_name} - {self.description}'
|
||||
|
||||
def get_parts_in_bom(self, **kwargs):
|
||||
@ -560,7 +558,7 @@ class Part(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, MPTTModel)
|
||||
return result
|
||||
|
||||
def validate_name(self, raise_error=True):
|
||||
"""Validate the name field for this Part instance
|
||||
"""Validate the name field for this Part instance.
|
||||
|
||||
This function is exposed to any Validation plugins, and thus can be customized.
|
||||
"""
|
||||
@ -579,7 +577,7 @@ class Part(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, MPTTModel)
|
||||
raise ValidationError({'name': exc.message})
|
||||
|
||||
def validate_ipn(self, raise_error=True):
|
||||
"""Ensure that the IPN (internal part number) is valid for this Part"
|
||||
"""Ensure that the IPN (internal part number) is valid for this Part".
|
||||
|
||||
- Validation is handled by custom plugins
|
||||
- By default, no validation checks are performed
|
||||
@ -745,8 +743,7 @@ class Part(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, MPTTModel)
|
||||
|
||||
@property
|
||||
def full_name(self):
|
||||
"""Format a 'full name' for this Part based on the format PART_NAME_FORMAT defined in InvenTree settings"""
|
||||
|
||||
"""Format a 'full name' for this Part based on the format PART_NAME_FORMAT defined in InvenTree settings."""
|
||||
return part_helpers.render_part_full_name(self)
|
||||
|
||||
def get_absolute_url(self):
|
||||
@ -1090,7 +1087,7 @@ class Part(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, MPTTModel)
|
||||
|
||||
@property
|
||||
def category_path(self):
|
||||
"""Return the category path of this Part instance"""
|
||||
"""Return the category path of this Part instance."""
|
||||
if self.category:
|
||||
return self.category.pathstring
|
||||
return ''
|
||||
@ -1652,7 +1649,7 @@ class Part(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, MPTTModel)
|
||||
|
||||
@property
|
||||
def has_bom(self):
|
||||
"""Return True if this Part instance has any BOM items"""
|
||||
"""Return True if this Part instance has any BOM items."""
|
||||
return self.get_bom_items().exists()
|
||||
|
||||
def get_trackable_parts(self):
|
||||
@ -1756,7 +1753,7 @@ class Part(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, MPTTModel)
|
||||
return self.supplier_parts.count()
|
||||
|
||||
def update_pricing(self):
|
||||
"""Recalculate cached pricing for this Part instance"""
|
||||
"""Recalculate cached pricing for this Part instance."""
|
||||
self.pricing.update_pricing()
|
||||
|
||||
@property
|
||||
@ -1826,8 +1823,9 @@ class Part(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, MPTTModel)
|
||||
return f'{min_price} - {max_price}'
|
||||
|
||||
def get_supplier_price_range(self, quantity=1):
|
||||
"""Return the supplier price range of this part:
|
||||
"""Return the supplier price range of this part.
|
||||
|
||||
Actions:
|
||||
- Checks if there is any supplier pricing information associated with this Part
|
||||
- Iterate through available supplier pricing and select (min, max)
|
||||
- Returns tuple of (min, max)
|
||||
@ -1964,7 +1962,7 @@ class Part(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, MPTTModel)
|
||||
|
||||
@property
|
||||
def has_price_breaks(self):
|
||||
"""Return True if this part has sale price breaks"""
|
||||
"""Return True if this part has sale price breaks."""
|
||||
return self.price_breaks.exists()
|
||||
|
||||
@property
|
||||
@ -1974,7 +1972,7 @@ class Part(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, MPTTModel)
|
||||
|
||||
@property
|
||||
def unit_pricing(self):
|
||||
"""Returns the price of this Part at quantity=1"""
|
||||
"""Returns the price of this Part at quantity=1."""
|
||||
return self.get_price(1)
|
||||
|
||||
def add_price_break(self, quantity, price):
|
||||
@ -1991,14 +1989,14 @@ class Part(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, MPTTModel)
|
||||
PartSellPriceBreak.objects.create(part=self, quantity=quantity, price=price)
|
||||
|
||||
def get_internal_price(self, quantity, moq=True, multiples=True, currency=None):
|
||||
"""Return the internal price of this Part at the specified quantity"""
|
||||
"""Return the internal price of this Part at the specified quantity."""
|
||||
return common.models.get_price(
|
||||
self, quantity, moq, multiples, currency, break_name='internal_price_breaks'
|
||||
)
|
||||
|
||||
@property
|
||||
def has_internal_price_breaks(self):
|
||||
"""Return True if this Part has internal pricing information"""
|
||||
"""Return True if this Part has internal pricing information."""
|
||||
return self.internal_price_breaks.exists()
|
||||
|
||||
@property
|
||||
@ -2007,7 +2005,7 @@ class Part(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, MPTTModel)
|
||||
return self.internalpricebreaks.order_by('quantity').all()
|
||||
|
||||
def get_purchase_price(self, quantity):
|
||||
"""Calculate the purchase price for this part at the specified quantity
|
||||
"""Calculate the purchase price for this part at the specified quantity.
|
||||
|
||||
- Looks at available supplier pricing data
|
||||
- Calculates the price base on the closest price point
|
||||
@ -2091,7 +2089,7 @@ class Part(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, MPTTModel)
|
||||
|
||||
@transaction.atomic
|
||||
def copy_parameters_from(self, other, **kwargs):
|
||||
"""Copy all parameter values from another Part instance"""
|
||||
"""Copy all parameter values from another Part instance."""
|
||||
clear = kwargs.get('clear', True)
|
||||
|
||||
if clear:
|
||||
@ -2136,7 +2134,7 @@ class Part(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, MPTTModel)
|
||||
return tests
|
||||
|
||||
def getTestTemplateMap(self, **kwargs):
|
||||
"""Return a map of all test templates associated with this Part"""
|
||||
"""Return a map of all test templates associated with this Part."""
|
||||
templates = {}
|
||||
|
||||
for template in self.getTestTemplates(**kwargs):
|
||||
@ -2145,7 +2143,7 @@ class Part(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, MPTTModel)
|
||||
return templates
|
||||
|
||||
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)
|
||||
|
||||
@property
|
||||
@ -2246,7 +2244,7 @@ class Part(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, MPTTModel)
|
||||
|
||||
@property
|
||||
def latest_stocktake(self):
|
||||
"""Return the latest PartStocktake object associated with this part (if one exists)"""
|
||||
"""Return the latest PartStocktake object associated with this part (if one exists)."""
|
||||
return self.stocktakes.order_by('-pk').first()
|
||||
|
||||
@property
|
||||
@ -2303,7 +2301,7 @@ class Part(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, MPTTModel)
|
||||
return filtered_parts
|
||||
|
||||
def get_related_parts(self):
|
||||
"""Return a set of all related parts for this part"""
|
||||
"""Return a set of all related parts for this part."""
|
||||
related_parts = set()
|
||||
|
||||
related_parts_1 = self.related_parts_1.filter(part_1__id=self.pk)
|
||||
@ -2322,7 +2320,7 @@ class Part(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, MPTTModel)
|
||||
|
||||
@property
|
||||
def related_count(self):
|
||||
"""Return the number of 'related parts' which point to this Part"""
|
||||
"""Return the number of 'related parts' which point to this Part."""
|
||||
return len(self.get_related_parts())
|
||||
|
||||
def is_part_low_on_stock(self):
|
||||
@ -2356,7 +2354,7 @@ def after_save_part(sender, instance: Part, created, **kwargs):
|
||||
|
||||
|
||||
class PartPricing(common.models.MetaMixin):
|
||||
"""Model for caching min/max pricing information for a particular Part
|
||||
"""Model for caching min/max pricing information for a particular Part.
|
||||
|
||||
It is prohibitively expensive to calculate min/max pricing for a part "on the fly".
|
||||
As min/max pricing does not change very often, we pre-calculate and cache these values.
|
||||
@ -2385,7 +2383,7 @@ class PartPricing(common.models.MetaMixin):
|
||||
|
||||
@property
|
||||
def is_valid(self):
|
||||
"""Return True if the cached pricing is valid"""
|
||||
"""Return True if the cached pricing is valid."""
|
||||
return self.updated is not None
|
||||
|
||||
def convert(self, money):
|
||||
@ -2411,7 +2409,7 @@ class PartPricing(common.models.MetaMixin):
|
||||
return result
|
||||
|
||||
def schedule_for_update(self, counter: int = 0, test: bool = False):
|
||||
"""Schedule this pricing to be updated"""
|
||||
"""Schedule this pricing to be updated."""
|
||||
import InvenTree.ready
|
||||
|
||||
# If we are running within CI, only schedule the update if the test flag is set
|
||||
@ -2487,7 +2485,7 @@ class PartPricing(common.models.MetaMixin):
|
||||
)
|
||||
|
||||
def update_pricing(self, counter: int = 0, cascade: bool = True):
|
||||
"""Recalculate all cost data for the referenced Part instance"""
|
||||
"""Recalculate all cost data for the referenced Part instance."""
|
||||
# If importing data, skip pricing update
|
||||
|
||||
if InvenTree.ready.isImportingData():
|
||||
@ -2526,7 +2524,7 @@ class PartPricing(common.models.MetaMixin):
|
||||
self.update_templates(counter)
|
||||
|
||||
def update_assemblies(self, counter: int = 0):
|
||||
"""Schedule updates for any assemblies which use this part"""
|
||||
"""Schedule updates for any assemblies which use this part."""
|
||||
# If the linked Part is used in any assemblies, schedule a pricing update for those assemblies
|
||||
used_in_parts = self.part.get_used_in()
|
||||
|
||||
@ -2534,14 +2532,14 @@ class PartPricing(common.models.MetaMixin):
|
||||
p.pricing.schedule_for_update(counter + 1)
|
||||
|
||||
def update_templates(self, counter: int = 0):
|
||||
"""Schedule updates for any template parts above this part"""
|
||||
"""Schedule updates for any template parts above this part."""
|
||||
templates = self.part.get_ancestors(include_self=False)
|
||||
|
||||
for p in templates:
|
||||
p.pricing.schedule_for_update(counter + 1)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
"""Whenever pricing model is saved, automatically update overall prices"""
|
||||
"""Whenever pricing model is saved, automatically update overall prices."""
|
||||
# Update the currency which was used to perform the calculation
|
||||
self.currency = currency_code_default()
|
||||
|
||||
@ -2720,7 +2718,7 @@ class PartPricing(common.models.MetaMixin):
|
||||
self.save()
|
||||
|
||||
def update_internal_cost(self, save=True):
|
||||
"""Recalculate internal cost for the referenced Part instance"""
|
||||
"""Recalculate internal cost for the referenced Part instance."""
|
||||
min_int_cost = None
|
||||
max_int_cost = None
|
||||
|
||||
@ -2835,7 +2833,6 @@ class PartPricing(common.models.MetaMixin):
|
||||
|
||||
Here we simply take the minimum / maximum values of the other calculated fields.
|
||||
"""
|
||||
|
||||
overall_min = None
|
||||
overall_max = None
|
||||
|
||||
@ -2907,7 +2904,7 @@ class PartPricing(common.models.MetaMixin):
|
||||
self.overall_max = overall_max
|
||||
|
||||
def update_sale_cost(self, save=True):
|
||||
"""Recalculate sale cost data"""
|
||||
"""Recalculate sale cost data."""
|
||||
# Iterate through the sell price breaks
|
||||
min_sell_price = None
|
||||
max_sell_price = None
|
||||
@ -3179,7 +3176,7 @@ class PartStocktake(models.Model):
|
||||
|
||||
@receiver(post_save, sender=PartStocktake, dispatch_uid='post_save_stocktake')
|
||||
def update_last_stocktake(sender, instance, created, **kwargs):
|
||||
"""Callback function when a PartStocktake instance is created / edited"""
|
||||
"""Callback function when a PartStocktake instance is created / edited."""
|
||||
# When a new PartStocktake instance is create, update the last_stocktake date for the Part
|
||||
if created:
|
||||
try:
|
||||
@ -3191,7 +3188,7 @@ def update_last_stocktake(sender, instance, created, **kwargs):
|
||||
|
||||
|
||||
def save_stocktake_report(instance, filename):
|
||||
"""Save stocktake reports to the correct subdirectory"""
|
||||
"""Save stocktake reports to the correct subdirectory."""
|
||||
filename = os.path.basename(filename)
|
||||
return os.path.join('stocktake', 'report', filename)
|
||||
|
||||
@ -3214,11 +3211,11 @@ class PartStocktakeReport(models.Model):
|
||||
"""
|
||||
|
||||
def __str__(self):
|
||||
"""Construct a simple string representation for the report"""
|
||||
"""Construct a simple string representation for the report."""
|
||||
return os.path.basename(self.report.name)
|
||||
|
||||
def get_absolute_url(self):
|
||||
"""Return the URL for the associaed report file for download"""
|
||||
"""Return the URL for the associaed report file for download."""
|
||||
if self.report:
|
||||
return self.report.url
|
||||
return None
|
||||
@ -3255,11 +3252,11 @@ class PartAttachment(InvenTreeAttachment):
|
||||
|
||||
@staticmethod
|
||||
def get_api_url():
|
||||
"""Return the list API endpoint URL associated with the PartAttachment model"""
|
||||
"""Return the list API endpoint URL associated with the PartAttachment model."""
|
||||
return reverse('api-part-attachment-list')
|
||||
|
||||
def getSubdir(self):
|
||||
"""Returns the media subdirectory where part attachments are stored"""
|
||||
"""Returns the media subdirectory where part attachments are stored."""
|
||||
return os.path.join('part_files', str(self.part.id))
|
||||
|
||||
part = models.ForeignKey(
|
||||
@ -3274,13 +3271,13 @@ class PartSellPriceBreak(common.models.PriceBreak):
|
||||
"""Represents a price break for selling this part."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass providing extra model definition"""
|
||||
"""Metaclass providing extra model definition."""
|
||||
|
||||
unique_together = ('part', 'quantity')
|
||||
|
||||
@staticmethod
|
||||
def get_api_url():
|
||||
"""Return the list API endpoint URL associated with the PartSellPriceBreak model"""
|
||||
"""Return the list API endpoint URL associated with the PartSellPriceBreak model."""
|
||||
return reverse('api-part-sale-price-list')
|
||||
|
||||
part = models.ForeignKey(
|
||||
@ -3296,13 +3293,13 @@ class PartInternalPriceBreak(common.models.PriceBreak):
|
||||
"""Represents a price break for internally selling this part."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass providing extra model definition"""
|
||||
"""Metaclass providing extra model definition."""
|
||||
|
||||
unique_together = ('part', 'quantity')
|
||||
|
||||
@staticmethod
|
||||
def get_api_url():
|
||||
"""Return the list API endpoint URL associated with the PartInternalPriceBreak model"""
|
||||
"""Return the list API endpoint URL associated with the PartInternalPriceBreak model."""
|
||||
return reverse('api-part-internal-price-list')
|
||||
|
||||
part = models.ForeignKey(
|
||||
@ -3324,7 +3321,7 @@ class PartStar(models.Model):
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass providing extra model definition"""
|
||||
"""Metaclass providing extra model definition."""
|
||||
|
||||
unique_together = ['part', 'user']
|
||||
|
||||
@ -3352,7 +3349,7 @@ class PartCategoryStar(models.Model):
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass providing extra model definition"""
|
||||
"""Metaclass providing extra model definition."""
|
||||
|
||||
unique_together = ['category', 'user']
|
||||
|
||||
@ -3386,17 +3383,17 @@ class PartTestTemplate(MetadataMixin, models.Model):
|
||||
|
||||
@staticmethod
|
||||
def get_api_url():
|
||||
"""Return the list API endpoint URL associated with the PartTestTemplate model"""
|
||||
"""Return the list API endpoint URL associated with the PartTestTemplate model."""
|
||||
return reverse('api-part-test-template-list')
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
"""Enforce 'clean' operation when saving a PartTestTemplate instance"""
|
||||
"""Enforce 'clean' operation when saving a PartTestTemplate instance."""
|
||||
self.clean()
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def clean(self):
|
||||
"""Clean fields for the PartTestTemplate model"""
|
||||
"""Clean fields for the PartTestTemplate model."""
|
||||
self.test_name = self.test_name.strip()
|
||||
|
||||
self.validate_unique()
|
||||
@ -3496,19 +3493,20 @@ class PartParameterTemplate(MetadataMixin, models.Model):
|
||||
|
||||
@staticmethod
|
||||
def get_api_url():
|
||||
"""Return the list API endpoint URL associated with the PartParameterTemplate model"""
|
||||
"""Return the list API endpoint URL associated with the PartParameterTemplate model."""
|
||||
return reverse('api-part-parameter-template-list')
|
||||
|
||||
def __str__(self):
|
||||
"""Return a string representation of a PartParameterTemplate instance"""
|
||||
"""Return a string representation of a PartParameterTemplate instance."""
|
||||
s = str(self.name)
|
||||
if self.units:
|
||||
s += f' ({self.units})'
|
||||
return s
|
||||
|
||||
def clean(self):
|
||||
"""Custom cleaning step for this model:
|
||||
"""Custom cleaning step for this model.
|
||||
|
||||
Checks:
|
||||
- A 'checkbox' field cannot have 'choices' set
|
||||
- A 'checkbox' field cannot have 'units' set
|
||||
"""
|
||||
@ -3566,7 +3564,7 @@ class PartParameterTemplate(MetadataMixin, models.Model):
|
||||
pass
|
||||
|
||||
def get_choices(self):
|
||||
"""Return a list of choices for this parameter template"""
|
||||
"""Return a list of choices for this parameter template."""
|
||||
if not self.choices:
|
||||
return []
|
||||
|
||||
@ -3614,7 +3612,7 @@ class PartParameterTemplate(MetadataMixin, models.Model):
|
||||
dispatch_uid='post_save_part_parameter_template',
|
||||
)
|
||||
def post_save_part_parameter_template(sender, instance, created, **kwargs):
|
||||
"""Callback function when a PartParameterTemplate is created or saved"""
|
||||
"""Callback function when a PartParameterTemplate is created or saved."""
|
||||
import part.tasks as part_tasks
|
||||
|
||||
if InvenTree.ready.canAppAccessDatabase() and not InvenTree.ready.isImportingData():
|
||||
@ -3635,18 +3633,18 @@ class PartParameter(MetadataMixin, models.Model):
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass providing extra model definition"""
|
||||
"""Metaclass providing extra model definition."""
|
||||
|
||||
# Prevent multiple instances of a parameter for a single part
|
||||
unique_together = ('part', 'template')
|
||||
|
||||
@staticmethod
|
||||
def get_api_url():
|
||||
"""Return the list API endpoint URL associated with the PartParameter model"""
|
||||
"""Return the list API endpoint URL associated with the PartParameter model."""
|
||||
return reverse('api-part-parameter-list')
|
||||
|
||||
def __str__(self):
|
||||
"""String representation of a PartParameter (used in the admin interface)"""
|
||||
"""String representation of a PartParameter (used in the admin interface)."""
|
||||
return f'{self.part.full_name} : {self.template.name} = {self.data} ({self.template.units})'
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
@ -3745,22 +3743,22 @@ class PartParameter(MetadataMixin, models.Model):
|
||||
|
||||
@property
|
||||
def units(self):
|
||||
"""Return the units associated with the template"""
|
||||
"""Return the units associated with the template."""
|
||||
return self.template.units
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the template"""
|
||||
"""Return the name of the template."""
|
||||
return self.template.name
|
||||
|
||||
@property
|
||||
def description(self):
|
||||
"""Return the description of the template"""
|
||||
"""Return the description of the template."""
|
||||
return self.template.description
|
||||
|
||||
@classmethod
|
||||
def create(cls, part, template, data, save=False):
|
||||
"""Custom save method for the PartParameter class"""
|
||||
"""Custom save method for the PartParameter class."""
|
||||
part_parameter = cls(part=part, template=template, data=data)
|
||||
if save:
|
||||
part_parameter.save()
|
||||
@ -3780,7 +3778,7 @@ class PartCategoryParameterTemplate(MetadataMixin, models.Model):
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass providing extra model definition"""
|
||||
"""Metaclass providing extra model definition."""
|
||||
|
||||
constraints = [
|
||||
UniqueConstraint(
|
||||
@ -3857,21 +3855,21 @@ class BomItem(DataImportMixin, MetadataMixin, models.Model):
|
||||
}
|
||||
|
||||
class Meta:
|
||||
"""Metaclass providing extra model definition"""
|
||||
"""Metaclass providing extra model definition."""
|
||||
|
||||
verbose_name = _('BOM Item')
|
||||
|
||||
def __str__(self):
|
||||
"""Return a string representation of this BomItem instance"""
|
||||
"""Return a string representation of this BomItem instance."""
|
||||
return f'{decimal2string(self.quantity)} x {self.sub_part.full_name} to make {self.part.full_name}'
|
||||
|
||||
@staticmethod
|
||||
def get_api_url():
|
||||
"""Return the list API endpoint URL associated with the BomItem model"""
|
||||
"""Return the list API endpoint URL associated with the BomItem model."""
|
||||
return reverse('api-bom-list')
|
||||
|
||||
def get_assemblies(self):
|
||||
"""Return a list of assemblies which use this BomItem"""
|
||||
"""Return a list of assemblies which use this BomItem."""
|
||||
assemblies = [self.part]
|
||||
|
||||
if self.inherited:
|
||||
@ -3928,7 +3926,7 @@ class BomItem(DataImportMixin, MetadataMixin, models.Model):
|
||||
return Q(part__in=self.get_valid_parts_for_allocation())
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
"""Enforce 'clean' operation when saving a BomItem instance"""
|
||||
"""Enforce 'clean' operation when saving a BomItem instance."""
|
||||
self.clean()
|
||||
|
||||
# Update the 'validated' field based on checksum calculation
|
||||
@ -4212,7 +4210,7 @@ class BomItem(DataImportMixin, MetadataMixin, models.Model):
|
||||
|
||||
@receiver(post_save, sender=BomItem, dispatch_uid='update_bom_build_lines')
|
||||
def update_bom_build_lines(sender, instance, created, **kwargs):
|
||||
"""Update existing build orders when a BomItem is created or edited"""
|
||||
"""Update existing build orders when a BomItem is created or edited."""
|
||||
if InvenTree.ready.canAppAccessDatabase() and not InvenTree.ready.isImportingData():
|
||||
import build.tasks
|
||||
|
||||
@ -4229,7 +4227,7 @@ def update_bom_build_lines(sender, instance, created, **kwargs):
|
||||
dispatch_uid='post_save_internal_price_break',
|
||||
)
|
||||
def update_pricing_after_edit(sender, instance, created, **kwargs):
|
||||
"""Callback function when a part price break is created or updated"""
|
||||
"""Callback function when a part price break is created or updated."""
|
||||
# Update part pricing *unless* we are importing data
|
||||
if InvenTree.ready.canAppAccessDatabase() and not InvenTree.ready.isImportingData():
|
||||
instance.part.schedule_pricing_update(create=True)
|
||||
@ -4245,7 +4243,7 @@ def update_pricing_after_edit(sender, instance, created, **kwargs):
|
||||
dispatch_uid='post_delete_internal_price_break',
|
||||
)
|
||||
def update_pricing_after_delete(sender, instance, **kwargs):
|
||||
"""Callback function when a part price break is deleted"""
|
||||
"""Callback function when a part price break is deleted."""
|
||||
# Update part pricing *unless* we are importing data
|
||||
if InvenTree.ready.canAppAccessDatabase() and not InvenTree.ready.isImportingData():
|
||||
instance.part.schedule_pricing_update(create=False)
|
||||
@ -4260,7 +4258,7 @@ class BomItemSubstitute(MetadataMixin, models.Model):
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass providing extra model definition"""
|
||||
"""Metaclass providing extra model definition."""
|
||||
|
||||
verbose_name = _('BOM Item Substitute')
|
||||
|
||||
@ -4268,7 +4266,7 @@ class BomItemSubstitute(MetadataMixin, models.Model):
|
||||
unique_together = ('part', 'bom_item')
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
"""Enforce a full_clean when saving the BomItemSubstitute model"""
|
||||
"""Enforce a full_clean when saving the BomItemSubstitute model."""
|
||||
self.full_clean()
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
@ -4288,7 +4286,7 @@ class BomItemSubstitute(MetadataMixin, models.Model):
|
||||
|
||||
@staticmethod
|
||||
def get_api_url():
|
||||
"""Returns the list API endpoint URL associated with this model"""
|
||||
"""Returns the list API endpoint URL associated with this model."""
|
||||
return reverse('api-bom-substitute-list')
|
||||
|
||||
bom_item = models.ForeignKey(
|
||||
@ -4313,7 +4311,7 @@ class PartRelated(MetadataMixin, models.Model):
|
||||
"""Store and handle related parts (eg. mating connector, crimps, etc.)."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass defines extra model properties"""
|
||||
"""Metaclass defines extra model properties."""
|
||||
|
||||
unique_together = ('part_1', 'part_2')
|
||||
|
||||
@ -4333,11 +4331,11 @@ class PartRelated(MetadataMixin, models.Model):
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
"""Return a string representation of this Part-Part relationship"""
|
||||
"""Return a string representation of this Part-Part relationship."""
|
||||
return f'{self.part_1} <--> {self.part_2}'
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
"""Enforce a 'clean' operation when saving a PartRelated instance"""
|
||||
"""Enforce a 'clean' operation when saving a PartRelated instance."""
|
||||
self.clean()
|
||||
self.validate_unique()
|
||||
super().save(*args, **kwargs)
|
||||
|
@ -62,7 +62,7 @@ class CategorySerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
||||
"""Serializer for PartCategory."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass defining serializer fields"""
|
||||
"""Metaclass defining serializer fields."""
|
||||
|
||||
model = PartCategory
|
||||
fields = [
|
||||
@ -83,7 +83,7 @@ class CategorySerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Optionally add or remove extra fields"""
|
||||
"""Optionally add or remove extra fields."""
|
||||
path_detail = kwargs.pop('path_detail', False)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
@ -97,7 +97,7 @@ class CategorySerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
||||
|
||||
@staticmethod
|
||||
def annotate_queryset(queryset):
|
||||
"""Annotate extra information to the queryset"""
|
||||
"""Annotate extra information to the queryset."""
|
||||
# Annotate the number of 'parts' which exist in each category (including subcategories!)
|
||||
queryset = queryset.annotate(part_count=part.filters.annotate_category_parts())
|
||||
|
||||
@ -120,7 +120,7 @@ class CategoryTree(InvenTree.serializers.InvenTreeModelSerializer):
|
||||
"""Serializer for PartCategory tree."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass defining serializer fields"""
|
||||
"""Metaclass defining serializer fields."""
|
||||
|
||||
model = PartCategory
|
||||
fields = ['pk', 'name', 'parent', 'icon', 'structural']
|
||||
@ -130,7 +130,7 @@ class PartAttachmentSerializer(InvenTree.serializers.InvenTreeAttachmentSerializ
|
||||
"""Serializer for the PartAttachment class."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass defining serializer fields"""
|
||||
"""Metaclass defining serializer fields."""
|
||||
|
||||
model = PartAttachment
|
||||
|
||||
@ -143,7 +143,7 @@ class PartTestTemplateSerializer(InvenTree.serializers.InvenTreeModelSerializer)
|
||||
"""Serializer for the PartTestTemplate class."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass defining serializer fields"""
|
||||
"""Metaclass defining serializer fields."""
|
||||
|
||||
model = PartTestTemplate
|
||||
|
||||
@ -165,7 +165,7 @@ class PartSalePriceSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
||||
"""Serializer for sale prices for Part model."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass defining serializer fields"""
|
||||
"""Metaclass defining serializer fields."""
|
||||
|
||||
model = PartSellPriceBreak
|
||||
fields = ['pk', 'part', 'quantity', 'price', 'price_currency']
|
||||
@ -183,7 +183,7 @@ class PartInternalPriceSerializer(InvenTree.serializers.InvenTreeModelSerializer
|
||||
"""Serializer for internal prices for Part model."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass defining serializer fields"""
|
||||
"""Metaclass defining serializer fields."""
|
||||
|
||||
model = PartInternalPriceBreak
|
||||
fields = ['pk', 'part', 'quantity', 'price', 'price_currency']
|
||||
@ -211,7 +211,7 @@ class PartThumbSerializerUpdate(InvenTree.serializers.InvenTreeModelSerializer):
|
||||
"""Serializer for updating Part thumbnail."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass defining serializer fields"""
|
||||
"""Metaclass defining serializer fields."""
|
||||
|
||||
model = Part
|
||||
fields = ['image']
|
||||
@ -230,17 +230,17 @@ class PartParameterTemplateSerializer(InvenTree.serializers.InvenTreeModelSerial
|
||||
"""JSON serializer for the PartParameterTemplate model."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass defining serializer fields"""
|
||||
"""Metaclass defining serializer fields."""
|
||||
|
||||
model = PartParameterTemplate
|
||||
fields = ['pk', 'name', 'units', 'description', 'checkbox', 'choices']
|
||||
|
||||
|
||||
class PartBriefSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
||||
"""Serializer for Part (brief detail)"""
|
||||
"""Serializer for Part (brief detail)."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass defining serializer fields"""
|
||||
"""Metaclass defining serializer fields."""
|
||||
|
||||
model = Part
|
||||
fields = [
|
||||
@ -268,7 +268,7 @@ class PartBriefSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
||||
read_only_fields = ['barcode_hash']
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Custom initialization routine for the PartBrief serializer"""
|
||||
"""Custom initialization routine for the PartBrief serializer."""
|
||||
pricing = kwargs.pop('pricing', True)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
@ -292,7 +292,7 @@ class PartParameterSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
||||
"""JSON serializers for the PartParameter model."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass defining serializer fields"""
|
||||
"""Metaclass defining serializer fields."""
|
||||
|
||||
model = PartParameter
|
||||
fields = [
|
||||
@ -328,10 +328,10 @@ class PartParameterSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
||||
|
||||
|
||||
class PartSetCategorySerializer(serializers.Serializer):
|
||||
"""Serializer for changing PartCategory for multiple Part objects"""
|
||||
"""Serializer for changing PartCategory for multiple Part objects."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options"""
|
||||
"""Metaclass options."""
|
||||
|
||||
fields = ['parts', 'category']
|
||||
|
||||
@ -344,7 +344,7 @@ class PartSetCategorySerializer(serializers.Serializer):
|
||||
)
|
||||
|
||||
def validate_parts(self, parts):
|
||||
"""Validate the selected parts"""
|
||||
"""Validate the selected parts."""
|
||||
if len(parts) == 0:
|
||||
raise serializers.ValidationError(_('No parts selected'))
|
||||
|
||||
@ -361,7 +361,7 @@ class PartSetCategorySerializer(serializers.Serializer):
|
||||
|
||||
@transaction.atomic
|
||||
def save(self):
|
||||
"""Save the serializer to change the location of the selected parts"""
|
||||
"""Save the serializer to change the location of the selected parts."""
|
||||
data = self.validated_data
|
||||
parts = data['parts']
|
||||
category = data['category']
|
||||
@ -444,7 +444,7 @@ class InitialStockSerializer(serializers.Serializer):
|
||||
|
||||
|
||||
class InitialSupplierSerializer(serializers.Serializer):
|
||||
"""Serializer for adding initial supplier / manufacturer information"""
|
||||
"""Serializer for adding initial supplier / manufacturer information."""
|
||||
|
||||
supplier = serializers.PrimaryKeyRelatedField(
|
||||
queryset=company.models.Company.objects.all(),
|
||||
@ -479,7 +479,7 @@ class InitialSupplierSerializer(serializers.Serializer):
|
||||
)
|
||||
|
||||
def validate_supplier(self, company):
|
||||
"""Validation for the provided Supplier"""
|
||||
"""Validation for the provided Supplier."""
|
||||
if company and not company.is_supplier:
|
||||
raise serializers.ValidationError(
|
||||
_('Selected company is not a valid supplier')
|
||||
@ -488,7 +488,7 @@ class InitialSupplierSerializer(serializers.Serializer):
|
||||
return company
|
||||
|
||||
def validate_manufacturer(self, company):
|
||||
"""Validation for the provided Manufacturer"""
|
||||
"""Validation for the provided Manufacturer."""
|
||||
if company and not company.is_manufacturer:
|
||||
raise serializers.ValidationError(
|
||||
_('Selected company is not a valid manufacturer')
|
||||
@ -497,7 +497,7 @@ class InitialSupplierSerializer(serializers.Serializer):
|
||||
return company
|
||||
|
||||
def validate(self, data):
|
||||
"""Extra validation for this serializer"""
|
||||
"""Extra validation for this serializer."""
|
||||
if company.models.ManufacturerPart.objects.filter(
|
||||
manufacturer=data.get('manufacturer', None), MPN=data.get('mpn', '')
|
||||
).exists():
|
||||
@ -525,7 +525,7 @@ class PartSerializer(
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass defining serializer fields"""
|
||||
"""Metaclass defining serializer fields."""
|
||||
|
||||
model = Part
|
||||
partial = True
|
||||
@ -593,7 +593,7 @@ class PartSerializer(
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Custom initialization method for PartSerializer:
|
||||
"""Custom initialization method for PartSerializer.
|
||||
|
||||
- Allows us to optionally pass extra fields based on the query.
|
||||
"""
|
||||
@ -628,11 +628,11 @@ class PartSerializer(
|
||||
self.fields.pop('pricing_max')
|
||||
|
||||
def get_api_url(self):
|
||||
"""Return the API url associated with this serializer"""
|
||||
"""Return the API url associated with this serializer."""
|
||||
return reverse_lazy('api-part-list')
|
||||
|
||||
def skip_create_fields(self):
|
||||
"""Skip these fields when instantiating a new Part instance"""
|
||||
"""Skip these fields when instantiating a new Part instance."""
|
||||
fields = super().skip_create_fields()
|
||||
|
||||
fields += [
|
||||
@ -811,7 +811,7 @@ class PartSerializer(
|
||||
)
|
||||
|
||||
def validate_existing_image(self, img):
|
||||
"""Validate the selected image file"""
|
||||
"""Validate the selected image file."""
|
||||
if not img:
|
||||
return img
|
||||
|
||||
@ -827,7 +827,7 @@ class PartSerializer(
|
||||
|
||||
@transaction.atomic
|
||||
def create(self, validated_data):
|
||||
"""Custom method for creating a new Part instance using this serializer"""
|
||||
"""Custom method for creating a new Part instance using this serializer."""
|
||||
duplicate = validated_data.pop('duplicate', None)
|
||||
initial_stock = validated_data.pop('initial_stock', None)
|
||||
initial_supplier = validated_data.pop('initial_supplier', None)
|
||||
@ -925,7 +925,7 @@ class PartSerializer(
|
||||
return instance
|
||||
|
||||
def save(self):
|
||||
"""Save the Part instance"""
|
||||
"""Save the Part instance."""
|
||||
super().save()
|
||||
|
||||
part = self.instance
|
||||
@ -956,10 +956,10 @@ class PartSerializer(
|
||||
|
||||
|
||||
class PartStocktakeSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
||||
"""Serializer for the PartStocktake model"""
|
||||
"""Serializer for the PartStocktake model."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options"""
|
||||
"""Metaclass options."""
|
||||
|
||||
model = PartStocktake
|
||||
fields = [
|
||||
@ -992,7 +992,7 @@ class PartStocktakeSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
||||
cost_max_currency = InvenTree.serializers.InvenTreeCurrencySerializer()
|
||||
|
||||
def save(self):
|
||||
"""Called when this serializer is saved"""
|
||||
"""Called when this serializer is saved."""
|
||||
data = self.validated_data
|
||||
|
||||
# Add in user information automatically
|
||||
@ -1003,10 +1003,10 @@ class PartStocktakeSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
||||
|
||||
|
||||
class PartStocktakeReportSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
||||
"""Serializer for stocktake report class"""
|
||||
"""Serializer for stocktake report class."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass defines serializer fields"""
|
||||
"""Metaclass defines serializer fields."""
|
||||
|
||||
model = PartStocktakeReport
|
||||
fields = ['pk', 'date', 'report', 'part_count', 'user', 'user_detail']
|
||||
@ -1019,7 +1019,7 @@ class PartStocktakeReportSerializer(InvenTree.serializers.InvenTreeModelSerializ
|
||||
|
||||
|
||||
class PartStocktakeReportGenerateSerializer(serializers.Serializer):
|
||||
"""Serializer class for manually generating a new PartStocktakeReport via the API"""
|
||||
"""Serializer class for manually generating a new PartStocktakeReport via the API."""
|
||||
|
||||
part = serializers.PrimaryKeyRelatedField(
|
||||
queryset=Part.objects.all(),
|
||||
@ -1070,7 +1070,7 @@ class PartStocktakeReportGenerateSerializer(serializers.Serializer):
|
||||
)
|
||||
|
||||
def validate(self, data):
|
||||
"""Custom validation for this serializer"""
|
||||
"""Custom validation for this serializer."""
|
||||
# Stocktake functionality must be enabled
|
||||
if not common.models.InvenTreeSetting.get_setting('STOCKTAKE_ENABLE', False):
|
||||
raise serializers.ValidationError(
|
||||
@ -1084,7 +1084,7 @@ class PartStocktakeReportGenerateSerializer(serializers.Serializer):
|
||||
return data
|
||||
|
||||
def save(self):
|
||||
"""Saving this serializer instance requests generation of a new stocktake report"""
|
||||
"""Saving this serializer instance requests generation of a new stocktake report."""
|
||||
data = self.validated_data
|
||||
user = self.context['request'].user
|
||||
|
||||
@ -1103,10 +1103,10 @@ class PartStocktakeReportGenerateSerializer(serializers.Serializer):
|
||||
|
||||
|
||||
class PartPricingSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
||||
"""Serializer for Part pricing information"""
|
||||
"""Serializer for Part pricing information."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass defining serializer fields"""
|
||||
"""Metaclass defining serializer fields."""
|
||||
|
||||
model = PartPricing
|
||||
fields = [
|
||||
@ -1239,8 +1239,7 @@ class PartPricingSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
||||
)
|
||||
|
||||
def validate(self, data):
|
||||
"""Validate supplied pricing data"""
|
||||
|
||||
"""Validate supplied pricing data."""
|
||||
super().validate(data)
|
||||
|
||||
# Check that override_min is not greater than override_max
|
||||
@ -1273,8 +1272,7 @@ class PartPricingSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
||||
return data
|
||||
|
||||
def save(self):
|
||||
"""Called when the serializer is saved"""
|
||||
|
||||
"""Called when the serializer is saved."""
|
||||
super().save()
|
||||
|
||||
# Update part pricing
|
||||
@ -1286,7 +1284,7 @@ class PartRelationSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
||||
"""Serializer for a PartRelated model."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass defining serializer fields"""
|
||||
"""Metaclass defining serializer fields."""
|
||||
|
||||
model = PartRelated
|
||||
fields = ['pk', 'part_1', 'part_1_detail', 'part_2', 'part_2_detail']
|
||||
@ -1299,7 +1297,7 @@ class PartStarSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
||||
"""Serializer for a PartStar object."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass defining serializer fields"""
|
||||
"""Metaclass defining serializer fields."""
|
||||
|
||||
model = PartStar
|
||||
fields = ['pk', 'part', 'partname', 'user', 'username']
|
||||
@ -1312,7 +1310,7 @@ class BomItemSubstituteSerializer(InvenTree.serializers.InvenTreeModelSerializer
|
||||
"""Serializer for the BomItemSubstitute class."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass defining serializer fields"""
|
||||
"""Metaclass defining serializer fields."""
|
||||
|
||||
model = BomItemSubstitute
|
||||
fields = ['pk', 'bom_item', 'part', 'part_detail']
|
||||
@ -1326,7 +1324,7 @@ class BomItemSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
||||
"""Serializer for BomItem object."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass defining serializer fields"""
|
||||
"""Metaclass defining serializer fields."""
|
||||
|
||||
model = BomItem
|
||||
fields = [
|
||||
@ -1358,7 +1356,7 @@ class BomItemSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Determine if extra detail fields are to be annotated on this serializer
|
||||
"""Determine if extra detail fields are to be annotated on this serializer.
|
||||
|
||||
- part_detail and sub_part_detail serializers are only included if requested.
|
||||
- This saves a bunch of database requests
|
||||
@ -1382,7 +1380,7 @@ class BomItemSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
||||
quantity = InvenTree.serializers.InvenTreeDecimalField(required=True)
|
||||
|
||||
def validate_quantity(self, quantity):
|
||||
"""Perform validation for the BomItem quantity field"""
|
||||
"""Perform validation for the BomItem quantity field."""
|
||||
if quantity <= 0:
|
||||
raise serializers.ValidationError(_('Quantity must be greater than zero'))
|
||||
|
||||
@ -1420,7 +1418,7 @@ class BomItemSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
||||
|
||||
@staticmethod
|
||||
def setup_eager_loading(queryset):
|
||||
"""Prefetch against the provided queryset to speed up database access"""
|
||||
"""Prefetch against the provided queryset to speed up database access."""
|
||||
queryset = queryset.prefetch_related('part')
|
||||
queryset = queryset.prefetch_related('part__category')
|
||||
queryset = queryset.prefetch_related('part__stock_items')
|
||||
@ -1444,7 +1442,7 @@ class BomItemSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
||||
|
||||
@staticmethod
|
||||
def annotate_queryset(queryset):
|
||||
"""Annotate the BomItem queryset with extra information:
|
||||
"""Annotate the BomItem queryset with extra information.
|
||||
|
||||
Annotations:
|
||||
available_stock: The amount of stock available for the sub_part Part object
|
||||
@ -1552,7 +1550,7 @@ class CategoryParameterTemplateSerializer(
|
||||
"""Serializer for the PartCategoryParameterTemplate model."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass defining serializer fields"""
|
||||
"""Metaclass defining serializer fields."""
|
||||
|
||||
model = PartCategoryParameterTemplate
|
||||
fields = [
|
||||
@ -1575,7 +1573,7 @@ class PartCopyBOMSerializer(serializers.Serializer):
|
||||
"""Serializer for copying a BOM from another part."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass defining serializer fields"""
|
||||
"""Metaclass defining serializer fields."""
|
||||
|
||||
fields = [
|
||||
'part',
|
||||
@ -1643,7 +1641,7 @@ class BomImportUploadSerializer(InvenTree.serializers.DataFileUploadSerializer):
|
||||
TARGET_MODEL = BomItem
|
||||
|
||||
class Meta:
|
||||
"""Metaclass defining serializer fields"""
|
||||
"""Metaclass defining serializer fields."""
|
||||
|
||||
fields = ['data_file', 'part', 'clear_existing_bom']
|
||||
|
||||
@ -1657,7 +1655,7 @@ class BomImportUploadSerializer(InvenTree.serializers.DataFileUploadSerializer):
|
||||
)
|
||||
|
||||
def save(self):
|
||||
"""The uploaded data file has been validated, accept the submitted data"""
|
||||
"""The uploaded data file has been validated, accept the submitted data."""
|
||||
data = self.validated_data
|
||||
|
||||
if data.get('clear_existing_bom', False):
|
||||
@ -1676,7 +1674,7 @@ class BomImportExtractSerializer(InvenTree.serializers.DataFileExtractSerializer
|
||||
TARGET_MODEL = BomItem
|
||||
|
||||
def validate_extracted_columns(self):
|
||||
"""Validate that the extracted columns are correct"""
|
||||
"""Validate that the extracted columns are correct."""
|
||||
super().validate_extracted_columns()
|
||||
|
||||
part_columns = ['part', 'part_name', 'part_ipn', 'part_id']
|
||||
@ -1687,7 +1685,7 @@ class BomImportExtractSerializer(InvenTree.serializers.DataFileExtractSerializer
|
||||
|
||||
@staticmethod
|
||||
def process_row(row):
|
||||
"""Process a single row from the loaded BOM file"""
|
||||
"""Process a single row from the loaded BOM file."""
|
||||
# Skip any rows which are at a lower "level"
|
||||
level = row.get('level', None)
|
||||
|
||||
@ -1764,9 +1762,9 @@ class BomImportSubmitSerializer(serializers.Serializer):
|
||||
items = BomItemSerializer(many=True, required=True)
|
||||
|
||||
def validate(self, data):
|
||||
"""Validate the submitted BomItem data:
|
||||
"""Validate the submitted BomItem data.
|
||||
|
||||
- At least one line (BomItem) is required
|
||||
At least one line (BomItem) is required
|
||||
"""
|
||||
items = data['items']
|
||||
|
||||
@ -1778,8 +1776,9 @@ class BomImportSubmitSerializer(serializers.Serializer):
|
||||
return data
|
||||
|
||||
def save(self):
|
||||
"""POST: Perform final save of submitted BOM data:
|
||||
"""POST: Perform final save of submitted BOM data.
|
||||
|
||||
Actions:
|
||||
- By this stage each line in the BOM has been validated
|
||||
- Individually 'save' (create) each BomItem line
|
||||
"""
|
||||
|
@ -1,4 +1,4 @@
|
||||
"""Stocktake report functionality"""
|
||||
"""Stocktake report functionality."""
|
||||
|
||||
import io
|
||||
import logging
|
||||
@ -28,8 +28,9 @@ def perform_stocktake(
|
||||
|
||||
Arguments:
|
||||
target: A single Part model instance
|
||||
commit: If True (default) save the result to the database
|
||||
user: User who requested this stocktake
|
||||
note: Optional note to attach to the stocktake
|
||||
commit: If True (default) save the result to the database
|
||||
|
||||
kwargs:
|
||||
exclude_external: If True, exclude stock items in external locations (default = False)
|
||||
|
@ -1,4 +1,4 @@
|
||||
"""Background task definitions for the 'part' app"""
|
||||
"""Background task definitions for the 'part' app."""
|
||||
|
||||
import logging
|
||||
import random
|
||||
@ -28,8 +28,9 @@ logger = logging.getLogger('inventree')
|
||||
|
||||
|
||||
def notify_low_stock(part: part.models.Part):
|
||||
"""Notify interested users that a part is 'low stock':
|
||||
"""Notify interested users that a part is 'low stock'.
|
||||
|
||||
Rules:
|
||||
- Triggered when the available stock for a given part falls be low the configured threhsold
|
||||
- A notification is delivered to any users who are 'subscribed' to this part
|
||||
"""
|
||||
@ -64,7 +65,7 @@ def notify_low_stock_if_required(part: part.models.Part):
|
||||
|
||||
|
||||
def update_part_pricing(pricing: part.models.PartPricing, counter: int = 0):
|
||||
"""Update cached pricing data for the specified PartPricing instance
|
||||
"""Update cached pricing data for the specified PartPricing instance.
|
||||
|
||||
Arguments:
|
||||
pricing: The target PartPricing instance to be updated
|
||||
@ -77,8 +78,9 @@ def update_part_pricing(pricing: part.models.PartPricing, counter: int = 0):
|
||||
|
||||
@scheduled_task(ScheduledTask.DAILY)
|
||||
def check_missing_pricing(limit=250):
|
||||
"""Check for parts with missing or outdated pricing information:
|
||||
"""Check for parts with missing or outdated pricing information.
|
||||
|
||||
Tests for the following conditions:
|
||||
- Pricing information does not exist
|
||||
- Pricing information is "old"
|
||||
- Pricing information is in the wrong currency
|
||||
|
@ -1 +1 @@
|
||||
"""Custom InvenTree template tags for HTML template rendering"""
|
||||
"""Custom InvenTree template tags for HTML template rendering."""
|
||||
|
@ -17,7 +17,7 @@ register = template.Library()
|
||||
|
||||
@register.simple_tag()
|
||||
def translation_stats(lang_code):
|
||||
"""Return the translation percentage for the given language code"""
|
||||
"""Return the translation percentage for the given language code."""
|
||||
if lang_code is None:
|
||||
return None
|
||||
|
||||
@ -25,10 +25,10 @@ def translation_stats(lang_code):
|
||||
|
||||
|
||||
class CustomTranslateNode(TranslateNode):
|
||||
"""Custom translation node class, which sanitizes the translated strings for javascript use"""
|
||||
"""Custom translation node class, which sanitizes the translated strings for javascript use."""
|
||||
|
||||
def render(self, context):
|
||||
"""Custom render function overrides / extends default behaviour"""
|
||||
"""Custom render function overrides / extends default behaviour."""
|
||||
result = super().render(context)
|
||||
|
||||
result = bleach.clean(result)
|
||||
@ -52,7 +52,7 @@ class CustomTranslateNode(TranslateNode):
|
||||
@register.tag('translate')
|
||||
@register.tag('trans')
|
||||
def do_translate(parser, token):
|
||||
"""Custom translation function, lifted from https://github.com/django/django/blob/main/django/templatetags/i18n.py
|
||||
"""Custom translation function, lifted from https://github.com/django/django/blob/main/django/templatetags/i18n.py.
|
||||
|
||||
The only difference is that we pass this to our custom rendering node class
|
||||
"""
|
||||
|
@ -98,7 +98,7 @@ def render_date(context, date_object):
|
||||
|
||||
@register.simple_tag
|
||||
def render_currency(money, **kwargs):
|
||||
"""Render a currency / Money object"""
|
||||
"""Render a currency / Money object."""
|
||||
return InvenTree.helpers_model.render_currency(money, **kwargs)
|
||||
|
||||
|
||||
@ -122,7 +122,7 @@ def to_list(*args):
|
||||
|
||||
@register.simple_tag()
|
||||
def part_allocation_count(build, part, *args, **kwargs):
|
||||
"""Return the total number of <part> allocated to <build>"""
|
||||
"""Return the total number of <part> allocated to <build>."""
|
||||
return InvenTree.helpers.decimal2string(build.getAllocatedQuantity(part))
|
||||
|
||||
|
||||
@ -185,7 +185,7 @@ def inventree_instance_name(*args, **kwargs):
|
||||
|
||||
@register.simple_tag()
|
||||
def inventree_title(*args, **kwargs):
|
||||
"""Return the title for the current instance - respecting the settings"""
|
||||
"""Return the title for the current instance - respecting the settings."""
|
||||
return version.inventreeInstanceTitle()
|
||||
|
||||
|
||||
@ -206,7 +206,7 @@ def inventree_splash(**kwargs):
|
||||
|
||||
@register.simple_tag()
|
||||
def inventree_base_url(*args, **kwargs):
|
||||
"""Return the base URL of the InvenTree server"""
|
||||
"""Return the base URL of the InvenTree server."""
|
||||
return InvenTree.helpers_model.get_base_url()
|
||||
|
||||
|
||||
@ -226,19 +226,19 @@ def inventree_version(shortstring=False, *args, **kwargs):
|
||||
|
||||
@register.simple_tag()
|
||||
def inventree_is_development(*args, **kwargs):
|
||||
"""Returns True if this is a development version of InvenTree"""
|
||||
"""Returns True if this is a development version of InvenTree."""
|
||||
return version.isInvenTreeDevelopmentVersion()
|
||||
|
||||
|
||||
@register.simple_tag()
|
||||
def inventree_is_release(*args, **kwargs):
|
||||
"""Returns True if this is a release version of InvenTree"""
|
||||
"""Returns True if this is a release version of InvenTree."""
|
||||
return not version.isInvenTreeDevelopmentVersion()
|
||||
|
||||
|
||||
@register.simple_tag()
|
||||
def inventree_docs_version(*args, **kwargs):
|
||||
"""Returns the InvenTree documentation version"""
|
||||
"""Returns the InvenTree documentation version."""
|
||||
return version.inventreeDocsVersion()
|
||||
|
||||
|
||||
@ -429,7 +429,7 @@ def progress_bar(val, max_val, *args, **kwargs):
|
||||
|
||||
@register.simple_tag()
|
||||
def get_color_theme_css(username):
|
||||
"""Return the custom theme .css file for the selected user"""
|
||||
"""Return the custom theme .css file for the selected user."""
|
||||
user_theme_name = get_user_color_theme(username)
|
||||
# Build path to CSS sheet
|
||||
inventree_css_sheet = os.path.join('css', 'color-themes', user_theme_name + '.css')
|
||||
@ -443,7 +443,6 @@ def get_color_theme_css(username):
|
||||
@register.simple_tag()
|
||||
def get_user_color_theme(username):
|
||||
"""Get current user color theme."""
|
||||
|
||||
from common.models import ColorTheme
|
||||
|
||||
try:
|
||||
@ -488,7 +487,7 @@ def primitive_to_javascript(primitive):
|
||||
|
||||
@register.simple_tag()
|
||||
def js_bool(val):
|
||||
"""Return a javascript boolean value (true or false)"""
|
||||
"""Return a javascript boolean value (true or false)."""
|
||||
if val:
|
||||
return 'true'
|
||||
return 'false'
|
||||
@ -599,14 +598,14 @@ if settings.DEBUG:
|
||||
|
||||
@register.simple_tag()
|
||||
def i18n_static(url_name):
|
||||
"""Simple tag to enable {% url %} functionality instead of {% static %}"""
|
||||
"""Simple tag to enable {% url %} functionality instead of {% static %}."""
|
||||
return reverse(url_name)
|
||||
|
||||
else: # pragma: no cover
|
||||
|
||||
@register.tag('i18n_static')
|
||||
def do_i18n_static(parser, token):
|
||||
"""Overrides normal static, adds language - lookup for prerenderd files #1485
|
||||
"""Overrides normal static, adds language - lookup for prerenderd files #1485.
|
||||
|
||||
Usage (like static):
|
||||
{% i18n_static path [as varname] %}
|
||||
@ -623,8 +622,7 @@ else: # pragma: no cover
|
||||
|
||||
@register.simple_tag()
|
||||
def admin_index(user):
|
||||
"""Return a URL for the admin interface"""
|
||||
|
||||
"""Return a URL for the admin interface."""
|
||||
if not djangosettings.INVENTREE_ADMIN_ENABLED:
|
||||
return ''
|
||||
|
||||
@ -642,7 +640,6 @@ def admin_url(user, table, pk):
|
||||
- If the user is not a staff user, an empty URL is returned
|
||||
- If the user does not have the correct permission, an empty URL is returned
|
||||
"""
|
||||
|
||||
app, model = table.strip().split('.')
|
||||
|
||||
from django.urls import reverse
|
||||
|
@ -1,4 +1,4 @@
|
||||
"""This module provides template tags pertaining to SSO functionality"""
|
||||
"""This module provides template tags pertaining to SSO functionality."""
|
||||
|
||||
from django import template
|
||||
|
||||
@ -9,24 +9,23 @@ register = template.Library()
|
||||
|
||||
@register.simple_tag()
|
||||
def sso_login_enabled():
|
||||
"""Return True if single-sign-on is enabled"""
|
||||
"""Return True if single-sign-on is enabled."""
|
||||
return InvenTree.sso.login_enabled()
|
||||
|
||||
|
||||
@register.simple_tag()
|
||||
def sso_reg_enabled():
|
||||
"""Return True if single-sign-on is enabled for self-registration"""
|
||||
"""Return True if single-sign-on is enabled for self-registration."""
|
||||
return InvenTree.sso.registration_enabled()
|
||||
|
||||
|
||||
@register.simple_tag()
|
||||
def sso_auto_enabled():
|
||||
"""Return True if single-sign-on is enabled for auto-registration"""
|
||||
"""Return True if single-sign-on is enabled for auto-registration."""
|
||||
return InvenTree.sso.auto_registration_enabled()
|
||||
|
||||
|
||||
@register.simple_tag()
|
||||
def sso_check_provider(provider):
|
||||
"""Return True if the given provider is correctly configured"""
|
||||
|
||||
"""Return True if the given provider is correctly configured."""
|
||||
return InvenTree.sso.check_provider(provider)
|
||||
|
@ -1,4 +1,4 @@
|
||||
"""Unit tests for the various part API endpoints"""
|
||||
"""Unit tests for the various part API endpoints."""
|
||||
|
||||
import os
|
||||
from datetime import datetime
|
||||
@ -64,7 +64,7 @@ class PartCategoryAPITest(InvenTreeAPITestCase):
|
||||
]
|
||||
|
||||
def test_category_list(self):
|
||||
"""Test the PartCategoryList API endpoint"""
|
||||
"""Test the PartCategoryList API endpoint."""
|
||||
url = reverse('api-part-category-list')
|
||||
|
||||
# star categories manually for tests as it is not possible with fixures
|
||||
@ -167,7 +167,7 @@ class PartCategoryAPITest(InvenTreeAPITestCase):
|
||||
)
|
||||
|
||||
def test_part_count(self):
|
||||
"""Test that the 'part_count' field is annotated correctly"""
|
||||
"""Test that the 'part_count' field is annotated correctly."""
|
||||
url = reverse('api-part-category-list')
|
||||
|
||||
# Create a parent category
|
||||
@ -206,7 +206,7 @@ class PartCategoryAPITest(InvenTreeAPITestCase):
|
||||
self.assertEqual(response.data['part_count'], 100)
|
||||
|
||||
def test_category_parameters(self):
|
||||
"""Test that the PartCategoryParameterTemplate API function work"""
|
||||
"""Test that the PartCategoryParameterTemplate API function work."""
|
||||
url = reverse('api-part-category-parameter-list')
|
||||
|
||||
response = self.get(url, {}, expected_code=200)
|
||||
@ -288,7 +288,7 @@ class PartCategoryAPITest(InvenTreeAPITestCase):
|
||||
self.assertEqual(response.data['description'], val)
|
||||
|
||||
def test_invisible_chars(self):
|
||||
"""Test that invisible characters are removed from the input data"""
|
||||
"""Test that invisible characters are removed from the input data."""
|
||||
url = reverse('api-part-category-detail', kwargs={'pk': 1})
|
||||
|
||||
values = [
|
||||
@ -304,7 +304,7 @@ class PartCategoryAPITest(InvenTreeAPITestCase):
|
||||
self.assertEqual(response.data['description'], 'A part category')
|
||||
|
||||
def test_category_delete(self):
|
||||
"""Test category deletion with different parameters"""
|
||||
"""Test category deletion with different parameters."""
|
||||
|
||||
class Target(IntEnum):
|
||||
move_subcategories_to_parent_move_parts_to_parent = (0,)
|
||||
@ -419,7 +419,7 @@ class PartCategoryAPITest(InvenTreeAPITestCase):
|
||||
self.assertEqual(child.parent, parent_category)
|
||||
|
||||
def test_structural(self):
|
||||
"""Test the effectiveness of structural categories
|
||||
"""Test the effectiveness of structural categories.
|
||||
|
||||
Make sure:
|
||||
- Parts cannot be created in structural categories
|
||||
@ -471,7 +471,7 @@ class PartCategoryAPITest(InvenTreeAPITestCase):
|
||||
self.assertEqual(part.category.pk, non_structural_category.pk)
|
||||
|
||||
def test_path_detail(self):
|
||||
"""Test path_detail information"""
|
||||
"""Test path_detail information."""
|
||||
url = reverse('api-part-category-detail', kwargs={'pk': 5})
|
||||
|
||||
# First, request without path detail
|
||||
@ -578,7 +578,7 @@ class PartOptionsAPITest(InvenTreeAPITestCase):
|
||||
|
||||
|
||||
class PartAPITestBase(InvenTreeAPITestCase):
|
||||
"""Base class for running tests on the Part API endpoints"""
|
||||
"""Base class for running tests on the Part API endpoints."""
|
||||
|
||||
fixtures = [
|
||||
'category',
|
||||
@ -669,7 +669,7 @@ class PartAPITest(PartAPITestBase):
|
||||
self.assertEqual(len(response.data), 12)
|
||||
|
||||
def test_cat_detail(self):
|
||||
"""Test the PartCategoryDetail API endpoint"""
|
||||
"""Test the PartCategoryDetail API endpoint."""
|
||||
url = reverse('api-part-category-detail', kwargs={'pk': 4})
|
||||
response = self.get(url)
|
||||
|
||||
@ -688,7 +688,7 @@ class PartAPITest(PartAPITestBase):
|
||||
self.assertIsNone(response.data['parent'])
|
||||
|
||||
def test_filter_parts(self):
|
||||
"""Test part filtering using the API"""
|
||||
"""Test part filtering using the API."""
|
||||
url = reverse('api-part-list')
|
||||
data = {'cascade': True}
|
||||
response = self.get(url, data)
|
||||
@ -707,7 +707,7 @@ class PartAPITest(PartAPITestBase):
|
||||
self.assertEqual(part['category'], 2)
|
||||
|
||||
def test_filter_by_in_bom(self):
|
||||
"""Test that we can filter part list by the 'in_bom_for' parameter"""
|
||||
"""Test that we can filter part list by the 'in_bom_for' parameter."""
|
||||
url = reverse('api-part-list')
|
||||
|
||||
response = self.get(url, {'in_bom_for': 100}, expected_code=200)
|
||||
@ -715,7 +715,7 @@ class PartAPITest(PartAPITestBase):
|
||||
self.assertEqual(len(response.data), 4)
|
||||
|
||||
def test_filter_by_related(self):
|
||||
"""Test that we can filter by the 'related' status"""
|
||||
"""Test that we can filter by the 'related' status."""
|
||||
url = reverse('api-part-list')
|
||||
|
||||
# Initially there are no relations, so this should return zero results
|
||||
@ -735,7 +735,7 @@ class PartAPITest(PartAPITestBase):
|
||||
self.assertEqual(len(response.data), 2)
|
||||
|
||||
def test_filter_by_convert(self):
|
||||
"""Test that we can correctly filter the Part list by conversion options"""
|
||||
"""Test that we can correctly filter the Part list by conversion options."""
|
||||
category = PartCategory.objects.get(pk=3)
|
||||
|
||||
# First, construct a set of template / variant parts
|
||||
@ -799,7 +799,7 @@ class PartAPITest(PartAPITestBase):
|
||||
self.assertEqual(len(response.data), 3)
|
||||
|
||||
def test_test_templates(self):
|
||||
"""Test the PartTestTemplate API"""
|
||||
"""Test the PartTestTemplate API."""
|
||||
url = reverse('api-part-test-template-list')
|
||||
|
||||
# List ALL items
|
||||
@ -870,8 +870,9 @@ class PartAPITest(PartAPITestBase):
|
||||
self.assertEqual(len(data['results']), n)
|
||||
|
||||
def test_template_filters(self):
|
||||
"""Unit tests for API filters related to template parts:
|
||||
"""Unit tests for API filters related to template parts.
|
||||
|
||||
Test:
|
||||
- variant_of : Return children of specified part
|
||||
- ancestor : Return descendants of specified part
|
||||
|
||||
@ -1029,8 +1030,7 @@ class PartAPITest(PartAPITestBase):
|
||||
self.assertEqual(part.category.name, row['Category Name'])
|
||||
|
||||
def test_date_filters(self):
|
||||
"""Test that the creation date filters work correctly"""
|
||||
|
||||
"""Test that the creation date filters work correctly."""
|
||||
url = reverse('api-part-list')
|
||||
|
||||
response = self.get(url)
|
||||
@ -1064,10 +1064,10 @@ class PartAPITest(PartAPITestBase):
|
||||
|
||||
|
||||
class PartCreationTests(PartAPITestBase):
|
||||
"""Tests for creating new Part instances via the API"""
|
||||
"""Tests for creating new Part instances via the API."""
|
||||
|
||||
def test_default_values(self):
|
||||
"""Tests for 'default' values:
|
||||
"""Tests for 'default' values.
|
||||
|
||||
Ensure that unspecified fields revert to "default" values
|
||||
(as specified in the model field definition)
|
||||
@ -1121,7 +1121,7 @@ class PartCreationTests(PartAPITestBase):
|
||||
"""Tests for initial stock quantity creation."""
|
||||
|
||||
def submit(stock_data, expected_code=None):
|
||||
"""Helper function for submitting with initial stock data"""
|
||||
"""Helper function for submitting with initial stock data."""
|
||||
data = {
|
||||
'category': 1,
|
||||
'name': "My lil' test part",
|
||||
@ -1162,7 +1162,7 @@ class PartCreationTests(PartAPITestBase):
|
||||
"""Tests for initial creation of supplier / manufacturer data."""
|
||||
|
||||
def submit(supplier_data, expected_code=400):
|
||||
"""Helper function for submitting with supplier data"""
|
||||
"""Helper function for submitting with supplier data."""
|
||||
data = {
|
||||
'name': 'My test part',
|
||||
'description': 'A test part thingy',
|
||||
@ -1248,7 +1248,7 @@ class PartCreationTests(PartAPITestBase):
|
||||
self.assertEqual(response.data['description'], description)
|
||||
|
||||
def test_duplication(self):
|
||||
"""Test part duplication options"""
|
||||
"""Test part duplication options."""
|
||||
# Run a matrix of tests
|
||||
for bom in [True, False]:
|
||||
for img in [True, False]:
|
||||
@ -1276,7 +1276,7 @@ class PartCreationTests(PartAPITestBase):
|
||||
self.assertEqual(part.parameters.count(), 2 if params else 0)
|
||||
|
||||
def test_category_parameters(self):
|
||||
"""Test that category parameters are correctly applied"""
|
||||
"""Test that category parameters are correctly applied."""
|
||||
cat = PartCategory.objects.get(pk=1)
|
||||
|
||||
# Add some parameter template to the parent category
|
||||
@ -1325,7 +1325,7 @@ class PartDetailTests(PartAPITestBase):
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
"""Custom setup routine for this class"""
|
||||
"""Custom setup routine for this class."""
|
||||
super().setUpTestData()
|
||||
|
||||
# Create a custom APIClient for file uploads
|
||||
@ -1334,7 +1334,7 @@ class PartDetailTests(PartAPITestBase):
|
||||
cls.upload_client.force_authenticate(user=cls.user)
|
||||
|
||||
def test_part_operations(self):
|
||||
"""Test that Part instances can be adjusted via the API"""
|
||||
"""Test that Part instances can be adjusted via the API."""
|
||||
n = Part.objects.count()
|
||||
|
||||
# Create a part
|
||||
@ -1530,8 +1530,7 @@ class PartDetailTests(PartAPITestBase):
|
||||
self.assertIsNotNone(p.image)
|
||||
|
||||
def test_existing_image(self):
|
||||
"""Test that we can allocate an existing uploaded image to a new Part"""
|
||||
|
||||
"""Test that we can allocate an existing uploaded image to a new Part."""
|
||||
# First, upload an image for an existing part
|
||||
p = Part.objects.first()
|
||||
|
||||
@ -1662,7 +1661,7 @@ class PartDetailTests(PartAPITestBase):
|
||||
self.assertEqual(data['unallocated_stock'], 9000)
|
||||
|
||||
def test_path_detail(self):
|
||||
"""Check that path_detail can be requested against the serializer"""
|
||||
"""Check that path_detail can be requested against the serializer."""
|
||||
response = self.get(
|
||||
reverse('api-part-detail', kwargs={'pk': 1}),
|
||||
{'path_detail': True},
|
||||
@ -1674,10 +1673,10 @@ class PartDetailTests(PartAPITestBase):
|
||||
|
||||
|
||||
class PartListTests(PartAPITestBase):
|
||||
"""Unit tests for the Part List API endpoint"""
|
||||
"""Unit tests for the Part List API endpoint."""
|
||||
|
||||
def test_query_count(self):
|
||||
"""Test that the query count is unchanged, independent of query results"""
|
||||
"""Test that the query count is unchanged, independent of query results."""
|
||||
queries = [{'limit': 1}, {'limit': 10}, {'limit': 50}, {'category': 1}, {}]
|
||||
|
||||
url = reverse('api-part-list')
|
||||
@ -1722,14 +1721,14 @@ class PartListTests(PartAPITestBase):
|
||||
|
||||
|
||||
class PartNotesTests(InvenTreeAPITestCase):
|
||||
"""Tests for the 'notes' field (markdown field)"""
|
||||
"""Tests for the 'notes' field (markdown field)."""
|
||||
|
||||
fixtures = ['category', 'part', 'location', 'company']
|
||||
|
||||
roles = ['part.change', 'part.add']
|
||||
|
||||
def test_long_notes(self):
|
||||
"""Test that very long notes field is rejected"""
|
||||
"""Test that very long notes field is rejected."""
|
||||
# Ensure that we cannot upload a very long piece of text
|
||||
url = reverse('api-part-detail', kwargs={'pk': 1})
|
||||
|
||||
@ -1741,7 +1740,7 @@ class PartNotesTests(InvenTreeAPITestCase):
|
||||
)
|
||||
|
||||
def test_multiline_formatting(self):
|
||||
"""Ensure that markdown formatting is retained"""
|
||||
"""Ensure that markdown formatting is retained."""
|
||||
url = reverse('api-part-detail', kwargs={'pk': 1})
|
||||
|
||||
notes = """
|
||||
@ -1765,18 +1764,18 @@ class PartNotesTests(InvenTreeAPITestCase):
|
||||
|
||||
|
||||
class PartPricingDetailTests(InvenTreeAPITestCase):
|
||||
"""Tests for the part pricing API endpoint"""
|
||||
"""Tests for the part pricing API endpoint."""
|
||||
|
||||
fixtures = ['category', 'part', 'location']
|
||||
|
||||
roles = ['part.change']
|
||||
|
||||
def url(self, pk):
|
||||
"""Construct a pricing URL"""
|
||||
"""Construct a pricing URL."""
|
||||
return reverse('api-part-pricing', kwargs={'pk': pk})
|
||||
|
||||
def test_pricing_detail(self):
|
||||
"""Test an empty pricing detail"""
|
||||
"""Test an empty pricing detail."""
|
||||
response = self.get(self.url(1), expected_code=200)
|
||||
|
||||
# Check for expected fields
|
||||
@ -1823,7 +1822,7 @@ class PartAPIAggregationTest(InvenTreeAPITestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
"""Create test data as part of setup routine"""
|
||||
"""Create test data as part of setup routine."""
|
||||
super().setUpTestData()
|
||||
|
||||
# Ensure the part "variant" tree is correctly structured
|
||||
@ -1849,7 +1848,7 @@ class PartAPIAggregationTest(InvenTreeAPITestCase):
|
||||
)
|
||||
|
||||
def get_part_data(self):
|
||||
"""Helper function for retrieving part data"""
|
||||
"""Helper function for retrieving part data."""
|
||||
url = reverse('api-part-list')
|
||||
|
||||
response = self.get(url)
|
||||
@ -2119,7 +2118,7 @@ class BomItemTest(InvenTreeAPITestCase):
|
||||
roles = ['part.add', 'part.change', 'part.delete']
|
||||
|
||||
def setUp(self):
|
||||
"""Set up the test case"""
|
||||
"""Set up the test case."""
|
||||
super().setUp()
|
||||
|
||||
# Rebuild part tree so BOM items validate correctly
|
||||
@ -2166,7 +2165,7 @@ class BomItemTest(InvenTreeAPITestCase):
|
||||
self.assertTrue(key in el)
|
||||
|
||||
def test_bom_list_search(self):
|
||||
"""Test that we can search the BOM list API endpoint"""
|
||||
"""Test that we can search the BOM list API endpoint."""
|
||||
url = reverse('api-bom-list')
|
||||
|
||||
response = self.get(url, expected_code=200)
|
||||
@ -2191,7 +2190,7 @@ class BomItemTest(InvenTreeAPITestCase):
|
||||
self.assertEqual(len(response.data), 0)
|
||||
|
||||
def test_bom_list_ordering(self):
|
||||
"""Test that the BOM list results can be ordered"""
|
||||
"""Test that the BOM list results can be ordered."""
|
||||
url = reverse('api-bom-list')
|
||||
|
||||
# Order by increasing quantity
|
||||
@ -2227,7 +2226,6 @@ class BomItemTest(InvenTreeAPITestCase):
|
||||
|
||||
def test_get_bom_detail(self):
|
||||
"""Get the detail view for a single BomItem object."""
|
||||
|
||||
url = reverse('api-bom-item-detail', kwargs={'pk': 3})
|
||||
|
||||
response = self.get(url, expected_code=200)
|
||||
@ -2476,12 +2474,12 @@ class BomItemTest(InvenTreeAPITestCase):
|
||||
|
||||
|
||||
class PartAttachmentTest(InvenTreeAPITestCase):
|
||||
"""Unit tests for the PartAttachment API endpoint"""
|
||||
"""Unit tests for the PartAttachment API endpoint."""
|
||||
|
||||
fixtures = ['category', 'part', 'location']
|
||||
|
||||
def test_add_attachment(self):
|
||||
"""Test that we can create a new PartAttachment via the API"""
|
||||
"""Test that we can create a new PartAttachment via the API."""
|
||||
url = reverse('api-part-attachment-list')
|
||||
|
||||
# Upload without permission
|
||||
@ -2525,7 +2523,7 @@ class PartAttachmentTest(InvenTreeAPITestCase):
|
||||
|
||||
|
||||
class PartInternalPriceBreakTest(InvenTreeAPITestCase):
|
||||
"""Unit tests for the PartInternalPrice API endpoints"""
|
||||
"""Unit tests for the PartInternalPrice API endpoints."""
|
||||
|
||||
fixtures = [
|
||||
'category',
|
||||
@ -2551,7 +2549,7 @@ class PartInternalPriceBreakTest(InvenTreeAPITestCase):
|
||||
]
|
||||
|
||||
def test_create_price_breaks(self):
|
||||
"""Test we can create price breaks at various quantities"""
|
||||
"""Test we can create price breaks at various quantities."""
|
||||
url = reverse('api-part-internal-price-list')
|
||||
|
||||
breaks = [
|
||||
@ -2589,7 +2587,7 @@ class PartInternalPriceBreakTest(InvenTreeAPITestCase):
|
||||
|
||||
|
||||
class PartStocktakeTest(InvenTreeAPITestCase):
|
||||
"""Unit tests for the part stocktake functionality"""
|
||||
"""Unit tests for the part stocktake functionality."""
|
||||
|
||||
superuser = False
|
||||
is_staff = False
|
||||
@ -2597,7 +2595,7 @@ class PartStocktakeTest(InvenTreeAPITestCase):
|
||||
fixtures = ['category', 'part', 'location', 'stock']
|
||||
|
||||
def test_list_endpoint(self):
|
||||
"""Test the list endpoint for the stocktake data"""
|
||||
"""Test the list endpoint for the stocktake data."""
|
||||
url = reverse('api-part-stocktake-list')
|
||||
|
||||
self.assignRole('part.view')
|
||||
@ -2636,7 +2634,7 @@ class PartStocktakeTest(InvenTreeAPITestCase):
|
||||
self.assertEqual(len(response.data), total)
|
||||
|
||||
def test_create_stocktake(self):
|
||||
"""Test that stocktake entries can be created via the API"""
|
||||
"""Test that stocktake entries can be created via the API."""
|
||||
url = reverse('api-part-stocktake-list')
|
||||
|
||||
self.assignRole('stocktake.add')
|
||||
@ -2695,7 +2693,7 @@ class PartStocktakeTest(InvenTreeAPITestCase):
|
||||
self.delete(url, expected_code=204)
|
||||
|
||||
def test_report_list(self):
|
||||
"""Test for PartStocktakeReport list endpoint"""
|
||||
"""Test for PartStocktakeReport list endpoint."""
|
||||
from part.stocktake import generate_stocktake_report
|
||||
|
||||
# Initially, no stocktake records are available
|
||||
@ -2728,7 +2726,7 @@ class PartStocktakeTest(InvenTreeAPITestCase):
|
||||
self.assertTrue(data['report'].endswith('.csv'))
|
||||
|
||||
def test_report_generate(self):
|
||||
"""Test API functionality for generating a new stocktake report"""
|
||||
"""Test API functionality for generating a new stocktake report."""
|
||||
url = reverse('api-part-stocktake-report-generate')
|
||||
|
||||
# Permission denied, initially
|
||||
@ -2767,12 +2765,12 @@ class PartMetadataAPITest(InvenTreeAPITestCase):
|
||||
roles = ['part.change', 'part_category.change']
|
||||
|
||||
def setUp(self):
|
||||
"""Setup unit tets"""
|
||||
"""Setup unit tets."""
|
||||
super().setUp()
|
||||
Part.objects.rebuild()
|
||||
|
||||
def metatester(self, apikey, model):
|
||||
"""Generic tester"""
|
||||
"""Generic tester."""
|
||||
modeldata = model.objects.first()
|
||||
|
||||
# Useless test unless a model object is found
|
||||
@ -2798,7 +2796,7 @@ class PartMetadataAPITest(InvenTreeAPITestCase):
|
||||
)
|
||||
|
||||
def test_metadata(self):
|
||||
"""Test all endpoints"""
|
||||
"""Test all endpoints."""
|
||||
for apikey, model in {
|
||||
'api-part-category-parameter-metadata': PartCategoryParameterTemplate,
|
||||
'api-part-category-metadata': PartCategory,
|
||||
@ -2814,10 +2812,10 @@ class PartMetadataAPITest(InvenTreeAPITestCase):
|
||||
|
||||
|
||||
class PartSchedulingTest(PartAPITestBase):
|
||||
"""Unit tests for the 'part scheduling' API endpoint"""
|
||||
"""Unit tests for the 'part scheduling' API endpoint."""
|
||||
|
||||
def test_get_schedule(self):
|
||||
"""Test that the scheduling endpoint returns OK"""
|
||||
"""Test that the scheduling endpoint returns OK."""
|
||||
part_ids = [1, 3, 100, 101]
|
||||
|
||||
for pk in part_ids:
|
||||
|
@ -9,14 +9,14 @@ from InvenTree.unit_test import InvenTreeTestCase
|
||||
|
||||
|
||||
class BomExportTest(InvenTreeTestCase):
|
||||
"""Class for performing unit testing of BOM export functionality"""
|
||||
"""Class for performing unit testing of BOM export functionality."""
|
||||
|
||||
fixtures = ['category', 'part', 'location', 'bom']
|
||||
|
||||
roles = 'all'
|
||||
|
||||
def setUp(self):
|
||||
"""Perform test setup functions"""
|
||||
"""Perform test setup functions."""
|
||||
super().setUp()
|
||||
|
||||
part.models.Part.objects.rebuild()
|
||||
|
@ -16,7 +16,7 @@ class BomUploadTest(InvenTreeAPITestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
"""Create BOM data as part of setup routine"""
|
||||
"""Create BOM data as part of setup routine."""
|
||||
super().setUpTestData()
|
||||
|
||||
Part.objects.rebuild()
|
||||
@ -55,7 +55,7 @@ class BomUploadTest(InvenTreeAPITestCase):
|
||||
expected_code=None,
|
||||
content_type='text/plain',
|
||||
):
|
||||
"""Helper function for submitting a BOM file"""
|
||||
"""Helper function for submitting a BOM file."""
|
||||
bom_file = SimpleUploadedFile(filename, file_data, content_type=content_type)
|
||||
|
||||
if clear_existing is None:
|
||||
@ -102,7 +102,7 @@ class BomUploadTest(InvenTreeAPITestCase):
|
||||
)
|
||||
|
||||
def test_missing_rows(self):
|
||||
"""Test upload of an invalid file (without data rows)"""
|
||||
"""Test upload of an invalid file (without data rows)."""
|
||||
dataset = tablib.Dataset()
|
||||
|
||||
dataset.headers = ['apple', 'banana']
|
||||
|
@ -1,4 +1,4 @@
|
||||
"""Unit tests for the BomItem model"""
|
||||
"""Unit tests for the BomItem model."""
|
||||
|
||||
from decimal import Decimal
|
||||
|
||||
@ -12,7 +12,7 @@ from .models import BomItem, BomItemSubstitute, Part
|
||||
|
||||
|
||||
class BomItemTest(TestCase):
|
||||
"""Class for unit testing BomItem model"""
|
||||
"""Class for unit testing BomItem model."""
|
||||
|
||||
fixtures = [
|
||||
'category',
|
||||
@ -26,7 +26,7 @@ class BomItemTest(TestCase):
|
||||
]
|
||||
|
||||
def setUp(self):
|
||||
"""Create initial data"""
|
||||
"""Create initial data."""
|
||||
super().setUp()
|
||||
|
||||
Part.objects.rebuild()
|
||||
@ -36,19 +36,19 @@ class BomItemTest(TestCase):
|
||||
self.r1 = Part.objects.get(name='R_2K2_0805')
|
||||
|
||||
def test_str(self):
|
||||
"""Test the string representation of a BOMItem"""
|
||||
"""Test the string representation of a BOMItem."""
|
||||
b = BomItem.objects.get(id=1)
|
||||
self.assertEqual(str(b), '10 x M2x4 LPHS to make BOB | Bob | A2')
|
||||
|
||||
def test_has_bom(self):
|
||||
"""Test the has_bom attribute"""
|
||||
"""Test the has_bom attribute."""
|
||||
self.assertFalse(self.orphan.has_bom)
|
||||
self.assertTrue(self.bob.has_bom)
|
||||
|
||||
self.assertEqual(self.bob.bom_count, 4)
|
||||
|
||||
def test_in_bom(self):
|
||||
"""Test BOM aggregation"""
|
||||
"""Test BOM aggregation."""
|
||||
parts = self.bob.getRequiredParts()
|
||||
|
||||
self.assertIn(self.orphan, parts)
|
||||
@ -56,7 +56,7 @@ class BomItemTest(TestCase):
|
||||
self.assertTrue(self.bob.check_if_part_in_bom(self.orphan))
|
||||
|
||||
def test_used_in(self):
|
||||
"""Test that the 'used_in_count' attribute is calculated correctly"""
|
||||
"""Test that the 'used_in_count' attribute is calculated correctly."""
|
||||
self.assertEqual(self.bob.used_in_count, 1)
|
||||
self.assertEqual(self.orphan.used_in_count, 1)
|
||||
|
||||
@ -131,7 +131,7 @@ class BomItemTest(TestCase):
|
||||
self.assertNotEqual(h1, h2)
|
||||
|
||||
def test_pricing(self):
|
||||
"""Test BOM pricing"""
|
||||
"""Test BOM pricing."""
|
||||
self.bob.get_price(1)
|
||||
self.assertEqual(
|
||||
self.bob.get_bom_price_range(1, internal=True),
|
||||
@ -193,7 +193,7 @@ class BomItemTest(TestCase):
|
||||
self.assertEqual(bom_item.substitutes.count(), 0)
|
||||
|
||||
def test_consumable(self):
|
||||
"""Tests for the 'consumable' BomItem field"""
|
||||
"""Tests for the 'consumable' BomItem field."""
|
||||
# Create an assembly part
|
||||
assembly = Part.objects.create(
|
||||
name='An assembly', description='Made with parts', assembly=True
|
||||
@ -252,8 +252,7 @@ class BomItemTest(TestCase):
|
||||
self.assertEqual(len(p.metadata.keys()), 4)
|
||||
|
||||
def test_invalid_bom(self):
|
||||
"""Test that ValidationError is correctly raised for an invalid BOM item"""
|
||||
|
||||
"""Test that ValidationError is correctly raised for an invalid BOM item."""
|
||||
# First test: A BOM item which points to itself
|
||||
with self.assertRaises(django_exceptions.ValidationError):
|
||||
BomItem.objects.create(part=self.bob, sub_part=self.bob, quantity=1)
|
||||
|
@ -1,4 +1,4 @@
|
||||
"""Unit tests for the PartCategory model"""
|
||||
"""Unit tests for the PartCategory model."""
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.test import TestCase
|
||||
@ -17,7 +17,7 @@ class CategoryTest(TestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
"""Extract some interesting categories for time-saving"""
|
||||
"""Extract some interesting categories for time-saving."""
|
||||
super().setUpTestData()
|
||||
|
||||
cls.electronics = PartCategory.objects.get(name='Electronics')
|
||||
@ -216,7 +216,7 @@ class CategoryTest(TestCase):
|
||||
self.assertIsNone(w.get_default_location())
|
||||
|
||||
def test_category_tree(self):
|
||||
"""Unit tests for the part category tree structure (MPTT)
|
||||
"""Unit tests for the part category tree structure (MPTT).
|
||||
|
||||
Ensure that the MPTT structure is rebuilt correctly,
|
||||
and the correct ancestor tree is observed.
|
||||
|
@ -32,7 +32,7 @@ class TestForwardMigrations(MigratorTestCase):
|
||||
print(p.is_template)
|
||||
|
||||
def test_models_exist(self):
|
||||
"""Test that the Part model can still be accessed at the end of schema migration"""
|
||||
"""Test that the Part model can still be accessed at the end of schema migration."""
|
||||
Part = self.new_state.apps.get_model('part', 'part')
|
||||
|
||||
self.assertEqual(Part.objects.count(), 5)
|
||||
@ -49,13 +49,13 @@ class TestForwardMigrations(MigratorTestCase):
|
||||
|
||||
|
||||
class TestBomItemMigrations(MigratorTestCase):
|
||||
"""Tests for BomItem migrations"""
|
||||
"""Tests for BomItem migrations."""
|
||||
|
||||
migrate_from = ('part', '0002_auto_20190520_2204')
|
||||
migrate_to = ('part', unit_test.getNewestMigrationFile('part'))
|
||||
|
||||
def prepare(self):
|
||||
"""Create initial dataset"""
|
||||
"""Create initial dataset."""
|
||||
Part = self.old_state.apps.get_model('part', 'part')
|
||||
BomItem = self.old_state.apps.get_model('part', 'bomitem')
|
||||
|
||||
@ -73,7 +73,7 @@ class TestBomItemMigrations(MigratorTestCase):
|
||||
print(b.validated)
|
||||
|
||||
def test_validated_field(self):
|
||||
"""Test that the 'validated' field is added to the BomItem objects"""
|
||||
"""Test that the 'validated' field is added to the BomItem objects."""
|
||||
BomItem = self.new_state.apps.get_model('part', 'bomitem')
|
||||
|
||||
self.assertEqual(BomItem.objects.count(), 2)
|
||||
@ -83,13 +83,13 @@ class TestBomItemMigrations(MigratorTestCase):
|
||||
|
||||
|
||||
class TestParameterMigrations(MigratorTestCase):
|
||||
"""Unit test for part parameter migrations"""
|
||||
"""Unit test for part parameter migrations."""
|
||||
|
||||
migrate_from = ('part', '0106_part_tags')
|
||||
migrate_to = ('part', unit_test.getNewestMigrationFile('part'))
|
||||
|
||||
def prepare(self):
|
||||
"""Create some parts, and templates with parameters"""
|
||||
"""Create some parts, and templates with parameters."""
|
||||
Part = self.old_state.apps.get_model('part', 'part')
|
||||
PartParameter = self.old_state.apps.get_model('part', 'partparameter')
|
||||
PartParameterTemlate = self.old_state.apps.get_model(
|
||||
@ -117,7 +117,7 @@ class TestParameterMigrations(MigratorTestCase):
|
||||
PartParameter.objects.create(part=b, template=t2, data='abc')
|
||||
|
||||
def test_data_migration(self):
|
||||
"""Test that the template units and values have been updated correctly"""
|
||||
"""Test that the template units and values have been updated correctly."""
|
||||
Part = self.new_state.apps.get_model('part', 'part')
|
||||
PartParameter = self.new_state.apps.get_model('part', 'partparameter')
|
||||
PartParameterTemlate = self.new_state.apps.get_model(
|
||||
@ -155,13 +155,13 @@ class TestParameterMigrations(MigratorTestCase):
|
||||
|
||||
|
||||
class PartUnitsMigrationTest(MigratorTestCase):
|
||||
"""Test for data migration of Part.units field"""
|
||||
"""Test for data migration of Part.units field."""
|
||||
|
||||
migrate_from = ('part', '0109_auto_20230517_1048')
|
||||
migrate_to = ('part', unit_test.getNewestMigrationFile('part'))
|
||||
|
||||
def prepare(self):
|
||||
"""Prepare some parts with units"""
|
||||
"""Prepare some parts with units."""
|
||||
Part = self.old_state.apps.get_model('part', 'part')
|
||||
|
||||
units = ['mm', 'INCH', '', '%']
|
||||
@ -178,7 +178,7 @@ class PartUnitsMigrationTest(MigratorTestCase):
|
||||
)
|
||||
|
||||
def test_units_migration(self):
|
||||
"""Test that the units have migrated OK"""
|
||||
"""Test that the units have migrated OK."""
|
||||
Part = self.new_state.apps.get_model('part', 'part')
|
||||
|
||||
part_1 = Part.objects.get(name='Part 1')
|
||||
@ -193,7 +193,7 @@ class PartUnitsMigrationTest(MigratorTestCase):
|
||||
|
||||
|
||||
class TestPartParameterTemplateMigration(MigratorTestCase):
|
||||
"""Test for data migration of PartParameterTemplate
|
||||
"""Test for data migration of PartParameterTemplate.
|
||||
|
||||
Ref: https://github.com/inventree/InvenTree/pull/4987
|
||||
"""
|
||||
@ -202,7 +202,7 @@ class TestPartParameterTemplateMigration(MigratorTestCase):
|
||||
migrate_to = ('part', '0113_auto_20230531_1205')
|
||||
|
||||
def prepare(self):
|
||||
"""Prepare some parts with units"""
|
||||
"""Prepare some parts with units."""
|
||||
PartParameterTemplate = self.old_state.apps.get_model(
|
||||
'part', 'partparametertemplate'
|
||||
)
|
||||
@ -220,7 +220,7 @@ class TestPartParameterTemplateMigration(MigratorTestCase):
|
||||
template.checkbox
|
||||
|
||||
def test_units_migration(self):
|
||||
"""Test that the new fields have been added correctly"""
|
||||
"""Test that the new fields have been added correctly."""
|
||||
PartParameterTemplate = self.new_state.apps.get_model(
|
||||
'part', 'partparametertemplate'
|
||||
)
|
||||
|
@ -1,4 +1,4 @@
|
||||
"""Various unit tests for Part Parameters"""
|
||||
"""Various unit tests for Part Parameters."""
|
||||
|
||||
import django.core.exceptions as django_exceptions
|
||||
from django.test import TestCase, TransactionTestCase
|
||||
@ -17,12 +17,12 @@ from .models import (
|
||||
|
||||
|
||||
class TestParams(TestCase):
|
||||
"""Unit test class for testing the PartParameter model"""
|
||||
"""Unit test class for testing the PartParameter model."""
|
||||
|
||||
fixtures = ['location', 'category', 'part', 'params']
|
||||
|
||||
def test_str(self):
|
||||
"""Test the str representation of the PartParameterTemplate model"""
|
||||
"""Test the str representation of the PartParameterTemplate model."""
|
||||
t1 = PartParameterTemplate.objects.get(pk=1)
|
||||
self.assertEqual(str(t1), 'Length (mm)')
|
||||
|
||||
@ -33,7 +33,7 @@ class TestParams(TestCase):
|
||||
self.assertEqual(str(c1), 'Mechanical | Length | 2.8')
|
||||
|
||||
def test_validate(self):
|
||||
"""Test validation for part templates"""
|
||||
"""Test validation for part templates."""
|
||||
n = PartParameterTemplate.objects.all().count()
|
||||
|
||||
t1 = PartParameterTemplate(name='abcde', units='dd')
|
||||
@ -65,7 +65,7 @@ class TestParams(TestCase):
|
||||
self.assertEqual(len(p.metadata.keys()), 4)
|
||||
|
||||
def test_get_parameter(self):
|
||||
"""Test the Part.get_parameter method"""
|
||||
"""Test the Part.get_parameter method."""
|
||||
prt = Part.objects.get(pk=3)
|
||||
|
||||
# Check that we can get a parameter by name
|
||||
@ -79,12 +79,12 @@ class TestParams(TestCase):
|
||||
|
||||
|
||||
class TestCategoryTemplates(TransactionTestCase):
|
||||
"""Test class for PartCategoryParameterTemplate model"""
|
||||
"""Test class for PartCategoryParameterTemplate model."""
|
||||
|
||||
fixtures = ['location', 'category', 'part', 'params']
|
||||
|
||||
def test_validate(self):
|
||||
"""Test that category templates are correctly applied to Part instances"""
|
||||
"""Test that category templates are correctly applied to Part instances."""
|
||||
# Category templates
|
||||
n = PartCategoryParameterTemplate.objects.all().count()
|
||||
self.assertEqual(n, 2)
|
||||
@ -102,12 +102,12 @@ class TestCategoryTemplates(TransactionTestCase):
|
||||
|
||||
|
||||
class ParameterTests(TestCase):
|
||||
"""Unit tests for parameter validation"""
|
||||
"""Unit tests for parameter validation."""
|
||||
|
||||
fixtures = ['location', 'category', 'part', 'params']
|
||||
|
||||
def test_choice_validation(self):
|
||||
"""Test that parameter choices are correctly validated"""
|
||||
"""Test that parameter choices are correctly validated."""
|
||||
template = PartParameterTemplate.objects.create(
|
||||
name='My Template',
|
||||
description='A template with choices',
|
||||
@ -129,7 +129,7 @@ class ParameterTests(TestCase):
|
||||
param.full_clean()
|
||||
|
||||
def test_unit_validation(self):
|
||||
"""Test validation of 'units' field for PartParameterTemplate"""
|
||||
"""Test validation of 'units' field for PartParameterTemplate."""
|
||||
# Test that valid units pass
|
||||
for unit in [
|
||||
None,
|
||||
@ -156,7 +156,7 @@ class ParameterTests(TestCase):
|
||||
tmp.full_clean()
|
||||
|
||||
def test_param_unit_validation(self):
|
||||
"""Test that parameters are correctly validated against template units"""
|
||||
"""Test that parameters are correctly validated against template units."""
|
||||
template = PartParameterTemplate.objects.create(name='My Template', units='m')
|
||||
|
||||
prt = Part.objects.get(pk=1)
|
||||
@ -208,7 +208,7 @@ class ParameterTests(TestCase):
|
||||
param.full_clean()
|
||||
|
||||
def test_param_unit_conversion(self):
|
||||
"""Test that parameters are correctly converted to template units"""
|
||||
"""Test that parameters are correctly converted to template units."""
|
||||
template = PartParameterTemplate.objects.create(name='My Template', units='m')
|
||||
|
||||
tests = {
|
||||
@ -325,7 +325,7 @@ class PartParameterTest(InvenTreeAPITestCase):
|
||||
"""Test that we can order parts by a specified parameter."""
|
||||
|
||||
def get_param_value(response, template, index):
|
||||
"""Helper function to extract a parameter value from a response"""
|
||||
"""Helper function to extract a parameter value from a response."""
|
||||
params = response.data[index]['parameters']
|
||||
|
||||
for param in params:
|
||||
|
@ -37,47 +37,47 @@ class TemplateTagTest(InvenTreeTestCase):
|
||||
"""Tests for the custom template tag code."""
|
||||
|
||||
def test_define(self):
|
||||
"""Test the 'define' template tag"""
|
||||
"""Test the 'define' template tag."""
|
||||
self.assertEqual(int(inventree_extras.define(3)), 3)
|
||||
|
||||
def test_str2bool(self):
|
||||
"""Various test for the str2bool template tag"""
|
||||
"""Various test for the str2bool template tag."""
|
||||
self.assertEqual(int(inventree_extras.str2bool('true')), True)
|
||||
self.assertEqual(int(inventree_extras.str2bool('yes')), True)
|
||||
self.assertEqual(int(inventree_extras.str2bool('none')), False)
|
||||
self.assertEqual(int(inventree_extras.str2bool('off')), False)
|
||||
|
||||
def test_add(self):
|
||||
"""Test that the 'add"""
|
||||
"""Test that the 'add."""
|
||||
self.assertEqual(int(inventree_extras.add(3, 5)), 8)
|
||||
|
||||
def test_plugins_enabled(self):
|
||||
"""Test the plugins_enabled tag"""
|
||||
"""Test the plugins_enabled tag."""
|
||||
self.assertEqual(inventree_extras.plugins_enabled(), True)
|
||||
|
||||
def test_inventree_instance_name(self):
|
||||
"""Test the 'instance name' setting"""
|
||||
"""Test the 'instance name' setting."""
|
||||
self.assertEqual(inventree_extras.inventree_instance_name(), 'InvenTree')
|
||||
|
||||
def test_inventree_base_url(self):
|
||||
"""Test that the base URL tag returns correctly"""
|
||||
"""Test that the base URL tag returns correctly."""
|
||||
self.assertEqual(inventree_extras.inventree_base_url(), '')
|
||||
|
||||
def test_inventree_is_release(self):
|
||||
"""Test that the release version check functions as expected"""
|
||||
"""Test that the release version check functions as expected."""
|
||||
self.assertEqual(
|
||||
inventree_extras.inventree_is_release(),
|
||||
not version.isInvenTreeDevelopmentVersion(),
|
||||
)
|
||||
|
||||
def test_inventree_docs_version(self):
|
||||
"""Test that the documentation version template tag returns correctly"""
|
||||
"""Test that the documentation version template tag returns correctly."""
|
||||
self.assertEqual(
|
||||
inventree_extras.inventree_docs_version(), version.inventreeDocsVersion()
|
||||
)
|
||||
|
||||
def test_hash(self):
|
||||
"""Test that the commit hash template tag returns correctly"""
|
||||
"""Test that the commit hash template tag returns correctly."""
|
||||
result_hash = inventree_extras.inventree_commit_hash()
|
||||
if settings.DOCKER: # pragma: no cover
|
||||
# Testing inside docker environment *may* return an empty git commit hash
|
||||
@ -87,7 +87,7 @@ class TemplateTagTest(InvenTreeTestCase):
|
||||
self.assertGreater(len(result_hash), 5)
|
||||
|
||||
def test_date(self):
|
||||
"""Test that the commit date template tag returns correctly"""
|
||||
"""Test that the commit date template tag returns correctly."""
|
||||
d = inventree_extras.inventree_commit_date()
|
||||
if settings.DOCKER: # pragma: no cover
|
||||
# Testing inside docker environment *may* return an empty git commit hash
|
||||
@ -97,33 +97,33 @@ class TemplateTagTest(InvenTreeTestCase):
|
||||
self.assertEqual(len(d.split('-')), 3)
|
||||
|
||||
def test_github(self):
|
||||
"""Test that the github URL template tag returns correctly"""
|
||||
"""Test that the github URL template tag returns correctly."""
|
||||
self.assertIn('github.com', inventree_extras.inventree_github_url())
|
||||
|
||||
def test_docs(self):
|
||||
"""Test that the documentation URL template tag returns correctly"""
|
||||
"""Test that the documentation URL template tag returns correctly."""
|
||||
self.assertIn('docs.inventree.org', inventree_extras.inventree_docs_url())
|
||||
|
||||
def test_keyvalue(self):
|
||||
"""Test keyvalue template tag"""
|
||||
"""Test keyvalue template tag."""
|
||||
self.assertEqual(inventree_extras.keyvalue({'a': 'a'}, 'a'), 'a')
|
||||
|
||||
def test_mail_configured(self):
|
||||
"""Test that mail configuration returns False"""
|
||||
"""Test that mail configuration returns False."""
|
||||
self.assertEqual(inventree_extras.mail_configured(), False)
|
||||
|
||||
def test_user_settings(self):
|
||||
"""Test user settings"""
|
||||
"""Test user settings."""
|
||||
result = inventree_extras.user_settings(self.user)
|
||||
self.assertEqual(len(result), len(InvenTreeUserSetting.SETTINGS))
|
||||
|
||||
def test_global_settings(self):
|
||||
"""Test global settings"""
|
||||
"""Test global settings."""
|
||||
result = inventree_extras.global_settings()
|
||||
self.assertEqual(len(result), len(InvenTreeSetting.SETTINGS))
|
||||
|
||||
def test_visible_global_settings(self):
|
||||
"""Test that hidden global settings are actually hidden"""
|
||||
"""Test that hidden global settings are actually hidden."""
|
||||
result = inventree_extras.visible_global_settings()
|
||||
|
||||
n = len(result)
|
||||
@ -147,7 +147,7 @@ class PartTest(TestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
"""Create some Part instances as part of init routine"""
|
||||
"""Create some Part instances as part of init routine."""
|
||||
super().setUpTestData()
|
||||
|
||||
cls.r1 = Part.objects.get(name='R_2K2_0805')
|
||||
@ -158,7 +158,7 @@ class PartTest(TestCase):
|
||||
Part.objects.rebuild()
|
||||
|
||||
def test_barcode_mixin(self):
|
||||
"""Test the barcode mixin functionality"""
|
||||
"""Test the barcode mixin functionality."""
|
||||
self.assertEqual(Part.barcode_model_type(), 'part')
|
||||
|
||||
p = Part.objects.get(pk=1)
|
||||
@ -166,7 +166,7 @@ class PartTest(TestCase):
|
||||
self.assertEqual(barcode, '{"part": 1}')
|
||||
|
||||
def test_tree(self):
|
||||
"""Test that the part variant tree is working properly"""
|
||||
"""Test that the part variant tree is working properly."""
|
||||
chair = Part.objects.get(pk=10000)
|
||||
self.assertEqual(chair.get_children().count(), 3)
|
||||
self.assertEqual(chair.get_descendant_count(), 4)
|
||||
@ -178,7 +178,7 @@ class PartTest(TestCase):
|
||||
self.assertEqual(Part.objects.filter(tree_id=chair.tree_id).count(), 5)
|
||||
|
||||
def test_str(self):
|
||||
"""Test string representation of a Part"""
|
||||
"""Test string representation of a Part."""
|
||||
p = Part.objects.get(pk=100)
|
||||
self.assertEqual(str(p), 'BOB | Bob | A2 - Can we build it? Yes we can!')
|
||||
|
||||
@ -235,12 +235,12 @@ class PartTest(TestCase):
|
||||
part_2.validate_unique()
|
||||
|
||||
def test_attributes(self):
|
||||
"""Test Part attributes"""
|
||||
"""Test Part attributes."""
|
||||
self.assertEqual(self.r1.name, 'R_2K2_0805')
|
||||
self.assertEqual(self.r1.get_absolute_url(), '/part/3/')
|
||||
|
||||
def test_category(self):
|
||||
"""Test PartCategory path"""
|
||||
"""Test PartCategory path."""
|
||||
self.c1.category.save()
|
||||
self.assertEqual(str(self.c1.category), 'Electronics/Capacitors - Capacitors')
|
||||
|
||||
@ -249,25 +249,25 @@ class PartTest(TestCase):
|
||||
self.assertEqual(orphan.category_path, '')
|
||||
|
||||
def test_rename_img(self):
|
||||
"""Test that an image can be renamed"""
|
||||
"""Test that an image can be renamed."""
|
||||
img = rename_part_image(self.r1, 'hello.png')
|
||||
self.assertEqual(img, os.path.join('part_images', 'hello.png'))
|
||||
|
||||
def test_stock(self):
|
||||
"""Test case where there is zero stock"""
|
||||
"""Test case where there is zero stock."""
|
||||
res = Part.objects.filter(description__contains='resistor')
|
||||
for r in res:
|
||||
self.assertEqual(r.total_stock, 0)
|
||||
self.assertEqual(r.available_stock, 0)
|
||||
|
||||
def test_barcode(self):
|
||||
"""Test barcode format functionality"""
|
||||
"""Test barcode format functionality."""
|
||||
barcode = self.r1.format_barcode(brief=False)
|
||||
self.assertIn('InvenTree', barcode)
|
||||
self.assertIn('"part": {"id": 3}', barcode)
|
||||
|
||||
def test_sell_pricing(self):
|
||||
"""Check that the sell pricebreaks were loaded"""
|
||||
"""Check that the sell pricebreaks were loaded."""
|
||||
self.assertTrue(self.r1.has_price_breaks)
|
||||
self.assertEqual(self.r1.price_breaks.count(), 2)
|
||||
# check that the sell pricebreaks work
|
||||
@ -275,7 +275,7 @@ class PartTest(TestCase):
|
||||
self.assertEqual(float(self.r1.get_price(10)), 1.0)
|
||||
|
||||
def test_internal_pricing(self):
|
||||
"""Check that the sell pricebreaks were loaded"""
|
||||
"""Check that the sell pricebreaks were loaded."""
|
||||
self.assertTrue(self.r1.has_internal_price_breaks)
|
||||
self.assertEqual(self.r1.internal_price_breaks.count(), 2)
|
||||
# check that the sell pricebreaks work
|
||||
@ -300,7 +300,7 @@ class PartTest(TestCase):
|
||||
self.assertEqual(len(p.metadata.keys()), 4)
|
||||
|
||||
def test_related(self):
|
||||
"""Unit tests for the PartRelated model"""
|
||||
"""Unit tests for the PartRelated model."""
|
||||
# Create a part relationship
|
||||
# Count before creation
|
||||
countbefore = PartRelated.objects.count()
|
||||
@ -348,7 +348,7 @@ class PartTest(TestCase):
|
||||
self.assertEqual(PartRelated.objects.count(), countbefore)
|
||||
|
||||
def test_stocktake(self):
|
||||
"""Test for adding stocktake data"""
|
||||
"""Test for adding stocktake data."""
|
||||
# Grab a part
|
||||
p = Part.objects.all().first()
|
||||
|
||||
@ -361,12 +361,12 @@ class PartTest(TestCase):
|
||||
|
||||
|
||||
class TestTemplateTest(TestCase):
|
||||
"""Unit test for the TestTemplate class"""
|
||||
"""Unit test for the TestTemplate class."""
|
||||
|
||||
fixtures = ['category', 'part', 'location', 'test_templates']
|
||||
|
||||
def test_template_count(self):
|
||||
"""Tests for the test template functions"""
|
||||
"""Tests for the test template functions."""
|
||||
chair = Part.objects.get(pk=10000)
|
||||
|
||||
# Tests for the top-level chair object (nothing above it!)
|
||||
@ -383,7 +383,7 @@ class TestTemplateTest(TestCase):
|
||||
self.assertEqual(variant.getTestTemplates(required=True).count(), 5)
|
||||
|
||||
def test_uniqueness(self):
|
||||
"""Test names must be unique for this part and also parts above"""
|
||||
"""Test names must be unique for this part and also parts above."""
|
||||
variant = Part.objects.get(pk=10004)
|
||||
|
||||
with self.assertRaises(ValidationError):
|
||||
@ -434,7 +434,7 @@ class PartSettingsTest(InvenTreeTestCase):
|
||||
self.assertFalse(part.settings.part_trackable_default())
|
||||
|
||||
def test_initial(self):
|
||||
"""Test the 'initial' default values (no default values have been set)"""
|
||||
"""Test the 'initial' default values (no default values have been set)."""
|
||||
cache.clear()
|
||||
|
||||
part = self.make_part()
|
||||
@ -525,13 +525,13 @@ class PartSettingsTest(InvenTreeTestCase):
|
||||
|
||||
|
||||
class PartSubscriptionTests(InvenTreeTestCase):
|
||||
"""Unit tests for part 'subscription'"""
|
||||
"""Unit tests for part 'subscription'."""
|
||||
|
||||
fixtures = ['location', 'category', 'part']
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
"""Create category and part data as part of setup routine"""
|
||||
"""Create category and part data as part of setup routine."""
|
||||
super().setUpTestData()
|
||||
|
||||
# Electronics / IC / MCU
|
||||
@ -625,7 +625,7 @@ class BaseNotificationIntegrationTest(InvenTreeTestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
"""Add an email address as part of initialization"""
|
||||
"""Add an email address as part of initialization."""
|
||||
super().setUpTestData()
|
||||
|
||||
# Add email address
|
||||
@ -668,7 +668,7 @@ class PartNotificationTest(BaseNotificationIntegrationTest):
|
||||
"""Integration test for part notifications."""
|
||||
|
||||
def test_notification(self):
|
||||
"""Test that a notification is generated"""
|
||||
"""Test that a notification is generated."""
|
||||
self._notification_run(UIMessageNotification)
|
||||
|
||||
# There should be 1 notification message right now
|
||||
|
@ -1,4 +1,4 @@
|
||||
"""Unit tests for Part pricing calculations"""
|
||||
"""Unit tests for Part pricing calculations."""
|
||||
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
|
||||
@ -16,10 +16,10 @@ from InvenTree.unit_test import InvenTreeTestCase
|
||||
|
||||
|
||||
class PartPricingTests(InvenTreeTestCase):
|
||||
"""Unit tests for part pricing calculations"""
|
||||
"""Unit tests for part pricing calculations."""
|
||||
|
||||
def setUp(self):
|
||||
"""Setup routines"""
|
||||
"""Setup routines."""
|
||||
super().setUp()
|
||||
|
||||
self.generate_exchange_rates()
|
||||
@ -35,7 +35,7 @@ class PartPricingTests(InvenTreeTestCase):
|
||||
)
|
||||
|
||||
def create_price_breaks(self):
|
||||
"""Create some price breaks for the part, in various currencies"""
|
||||
"""Create some price breaks for the part, in various currencies."""
|
||||
# First supplier part (CAD)
|
||||
self.supplier_1 = company.models.Company.objects.create(
|
||||
name='Supplier 1', is_supplier=True
|
||||
@ -87,7 +87,7 @@ class PartPricingTests(InvenTreeTestCase):
|
||||
)
|
||||
|
||||
def test_pricing_data(self):
|
||||
"""Test link between Part and PartPricing model"""
|
||||
"""Test link between Part and PartPricing model."""
|
||||
# Initially there is no associated Pricing data
|
||||
with self.assertRaises(ObjectDoesNotExist):
|
||||
pricing = self.part.pricing_data
|
||||
@ -108,11 +108,11 @@ class PartPricingTests(InvenTreeTestCase):
|
||||
self.assertIsNone(pricing.overall_max)
|
||||
|
||||
def test_invalid_rate(self):
|
||||
"""Ensure that conversion behaves properly with missing rates"""
|
||||
"""Ensure that conversion behaves properly with missing rates."""
|
||||
...
|
||||
|
||||
def test_simple(self):
|
||||
"""Tests for hard-coded values"""
|
||||
"""Tests for hard-coded values."""
|
||||
pricing = self.part.pricing
|
||||
|
||||
# Add internal pricing
|
||||
@ -143,7 +143,7 @@ class PartPricingTests(InvenTreeTestCase):
|
||||
self.assertEqual(pricing.overall_max, Money('25', 'USD'))
|
||||
|
||||
def test_supplier_part_pricing(self):
|
||||
"""Test for supplier part pricing"""
|
||||
"""Test for supplier part pricing."""
|
||||
pricing = self.part.pricing
|
||||
|
||||
# Initially, no information (not yet calculated)
|
||||
@ -169,7 +169,7 @@ class PartPricingTests(InvenTreeTestCase):
|
||||
self.assertIsNone(pricing.supplier_price_max)
|
||||
|
||||
def test_internal_pricing(self):
|
||||
"""Tests for internal price breaks"""
|
||||
"""Tests for internal price breaks."""
|
||||
# Ensure internal pricing is enabled
|
||||
common.models.InvenTreeSetting.set_setting('PART_INTERNAL_PRICE', True, None)
|
||||
|
||||
@ -201,7 +201,7 @@ class PartPricingTests(InvenTreeTestCase):
|
||||
self.assertEqual(pricing.overall_max, Money(10, currency))
|
||||
|
||||
def test_stock_item_pricing(self):
|
||||
"""Test for stock item pricing data"""
|
||||
"""Test for stock item pricing data."""
|
||||
# Create a part
|
||||
p = part.models.Part.objects.create(
|
||||
name='Test part for pricing',
|
||||
@ -247,7 +247,7 @@ class PartPricingTests(InvenTreeTestCase):
|
||||
self.assertEqual(pricing.overall_max, Money(6.666667, 'USD'))
|
||||
|
||||
def test_bom_pricing(self):
|
||||
"""Unit test for BOM pricing calculations"""
|
||||
"""Unit test for BOM pricing calculations."""
|
||||
pricing = self.part.pricing
|
||||
|
||||
self.assertIsNone(pricing.bom_cost_min)
|
||||
@ -286,7 +286,7 @@ class PartPricingTests(InvenTreeTestCase):
|
||||
self.assertEqual(pricing.overall_max, Money('550', 'USD'))
|
||||
|
||||
def test_purchase_pricing(self):
|
||||
"""Unit tests for historical purchase pricing"""
|
||||
"""Unit tests for historical purchase pricing."""
|
||||
self.create_price_breaks()
|
||||
|
||||
pricing = self.part.pricing
|
||||
@ -353,7 +353,7 @@ class PartPricingTests(InvenTreeTestCase):
|
||||
self.assertAlmostEqual(float(pricing.purchase_cost_max.amount), 6.95, places=2)
|
||||
|
||||
def test_delete_with_pricing(self):
|
||||
"""Test for deleting a part which has pricing information"""
|
||||
"""Test for deleting a part which has pricing information."""
|
||||
# Create some pricing data
|
||||
self.create_price_breaks()
|
||||
|
||||
@ -377,7 +377,7 @@ class PartPricingTests(InvenTreeTestCase):
|
||||
pricing.refresh_from_db()
|
||||
|
||||
def test_delete_without_pricing(self):
|
||||
"""Test that we can delete a part which does not have pricing information"""
|
||||
"""Test that we can delete a part which does not have pricing information."""
|
||||
pricing = self.part.pricing
|
||||
|
||||
self.assertIsNone(pricing.pk)
|
||||
@ -392,7 +392,7 @@ class PartPricingTests(InvenTreeTestCase):
|
||||
self.part.refresh_from_db()
|
||||
|
||||
def test_check_missing_pricing(self):
|
||||
"""Tests for check_missing_pricing background task
|
||||
"""Tests for check_missing_pricing background task.
|
||||
|
||||
Calling the check_missing_pricing task should:
|
||||
- Create PartPricing objects where there are none
|
||||
|
@ -1,4 +1,4 @@
|
||||
"""Unit tests for Part Views (see views.py)"""
|
||||
"""Unit tests for Part Views (see views.py)."""
|
||||
|
||||
from django.urls import reverse
|
||||
|
||||
@ -8,7 +8,7 @@ from .models import Part
|
||||
|
||||
|
||||
class PartViewTestCase(InvenTreeTestCase):
|
||||
"""Base class for unit testing the various Part views"""
|
||||
"""Base class for unit testing the various Part views."""
|
||||
|
||||
fixtures = ['category', 'part', 'bom', 'location', 'company', 'supplier_part']
|
||||
|
||||
@ -17,10 +17,10 @@ class PartViewTestCase(InvenTreeTestCase):
|
||||
|
||||
|
||||
class PartListTest(PartViewTestCase):
|
||||
"""Unit tests for the PartList view"""
|
||||
"""Unit tests for the PartList view."""
|
||||
|
||||
def test_part_index(self):
|
||||
"""Test that the PartIndex page returns successfully"""
|
||||
"""Test that the PartIndex page returns successfully."""
|
||||
response = self.client.get(reverse('part-index'))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
@ -31,7 +31,7 @@ class PartListTest(PartViewTestCase):
|
||||
|
||||
|
||||
class PartDetailTest(PartViewTestCase):
|
||||
"""Unit tests for the PartDetail view"""
|
||||
"""Unit tests for the PartDetail view."""
|
||||
|
||||
def test_part_detail(self):
|
||||
"""Test that we can retrieve a part detail page."""
|
||||
@ -51,8 +51,9 @@ class PartDetailTest(PartViewTestCase):
|
||||
self.assertEqual(response.context['category'], part.category)
|
||||
|
||||
def test_part_detail_from_ipn(self):
|
||||
"""Test that we can retrieve a part detail page from part IPN:
|
||||
"""Test that we can retrieve a part detail page from part IPN.
|
||||
|
||||
Rules:
|
||||
- if no part with matching IPN -> return part index
|
||||
- if unique IPN match -> return part detail page
|
||||
- if multiple IPN matches -> return part index
|
||||
@ -61,7 +62,7 @@ class PartDetailTest(PartViewTestCase):
|
||||
pk = 1
|
||||
|
||||
def test_ipn_match(index_result=False, detail_result=False):
|
||||
"""Helper function for matching IPN detail view"""
|
||||
"""Helper function for matching IPN detail view."""
|
||||
index_redirect = False
|
||||
detail_redirect = False
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user