mirror of
https://github.com/inventree/InvenTree.git
synced 2025-04-28 19:46:46 +00:00
refactor: remove blank lines after docstring (#5736)
There shouldn't be any blank lines after the function docstring. Remove the blank lines to fix this issue. Co-authored-by: deepsource-autofix[bot] <62050782+deepsource-autofix[bot]@users.noreply.github.com>
This commit is contained in:
parent
158a209a0f
commit
faac6b6bf5
@ -30,7 +30,6 @@ class InvenTreeResource(ModelResource):
|
|||||||
**kwargs
|
**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:
|
if len(dataset) > self.MAX_IMPORT_ROWS:
|
||||||
raise ImportExportError(f"Dataset contains too many rows (max {self.MAX_IMPORT_ROWS})")
|
raise ImportExportError(f"Dataset contains too many rows (max {self.MAX_IMPORT_ROWS})")
|
||||||
|
|
||||||
@ -71,7 +70,6 @@ class InvenTreeResource(ModelResource):
|
|||||||
|
|
||||||
def get_fields(self, **kwargs):
|
def get_fields(self, **kwargs):
|
||||||
"""Return fields, with some common exclusions"""
|
"""Return fields, with some common exclusions"""
|
||||||
|
|
||||||
fields = super().get_fields(**kwargs)
|
fields = super().get_fields(**kwargs)
|
||||||
|
|
||||||
fields_to_exclude = [
|
fields_to_exclude = [
|
||||||
|
@ -35,7 +35,6 @@ class InfoView(AjaxView):
|
|||||||
|
|
||||||
def worker_pending_tasks(self):
|
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()
|
return OrmQ.objects.count()
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
@ -256,7 +255,6 @@ class APISearchView(APIView):
|
|||||||
|
|
||||||
def get_result_types(self):
|
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 build.api
|
||||||
import company.api
|
import company.api
|
||||||
import order.api
|
import order.api
|
||||||
@ -279,7 +277,6 @@ class APISearchView(APIView):
|
|||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
"""Perform search query against available models"""
|
"""Perform search query against available models"""
|
||||||
|
|
||||||
data = request.data
|
data = request.data
|
||||||
|
|
||||||
results = {}
|
results = {}
|
||||||
|
@ -79,7 +79,6 @@ class InvenTreeConfig(AppConfig):
|
|||||||
|
|
||||||
def start_background_tasks(self):
|
def start_background_tasks(self):
|
||||||
"""Start all background tests for InvenTree."""
|
"""Start all background tests for InvenTree."""
|
||||||
|
|
||||||
logger.info("Starting background tasks...")
|
logger.info("Starting background tasks...")
|
||||||
|
|
||||||
from django_q.models import Schedule
|
from django_q.models import Schedule
|
||||||
@ -140,7 +139,6 @@ class InvenTreeConfig(AppConfig):
|
|||||||
|
|
||||||
def collect_tasks(self):
|
def collect_tasks(self):
|
||||||
"""Collect all background tasks."""
|
"""Collect all background tasks."""
|
||||||
|
|
||||||
for app_name, app in apps.app_configs.items():
|
for app_name, app in apps.app_configs.items():
|
||||||
if app_name == 'InvenTree':
|
if app_name == 'InvenTree':
|
||||||
continue
|
continue
|
||||||
|
@ -23,7 +23,6 @@ def to_list(value, delimiter=','):
|
|||||||
However, the same setting may be specified via an environment variable,
|
However, the same setting may be specified via an environment variable,
|
||||||
using a comma delimited string!
|
using a comma delimited string!
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if type(value) in [list, tuple]:
|
if type(value) in [list, tuple]:
|
||||||
return value
|
return value
|
||||||
|
|
||||||
@ -70,7 +69,6 @@ def ensure_dir(path: Path) -> None:
|
|||||||
|
|
||||||
If it does not exist, create it.
|
If it does not exist, create it.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not path.exists():
|
if not path.exists():
|
||||||
path.mkdir(parents=True, exist_ok=True)
|
path.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
@ -143,7 +141,6 @@ def get_setting(env_var=None, config_key=None, default_value=None, typecast=None
|
|||||||
"""
|
"""
|
||||||
def try_typecasting(value, source: str):
|
def try_typecasting(value, source: str):
|
||||||
"""Attempt to typecast the value"""
|
"""Attempt to typecast the value"""
|
||||||
|
|
||||||
# Force 'list' of strings
|
# Force 'list' of strings
|
||||||
if typecast is list:
|
if typecast is list:
|
||||||
value = to_list(value)
|
value = to_list(value)
|
||||||
@ -201,13 +198,11 @@ 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):
|
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))
|
return is_true(get_setting(env_var, config_key, default_value))
|
||||||
|
|
||||||
|
|
||||||
def get_media_dir(create=True):
|
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')
|
md = get_setting('INVENTREE_MEDIA_ROOT', 'media_root')
|
||||||
|
|
||||||
if not md:
|
if not md:
|
||||||
@ -223,7 +218,6 @@ def get_media_dir(create=True):
|
|||||||
|
|
||||||
def get_static_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')
|
sd = get_setting('INVENTREE_STATIC_ROOT', 'static_root')
|
||||||
|
|
||||||
if not sd:
|
if not sd:
|
||||||
@ -239,7 +233,6 @@ def get_static_dir(create=True):
|
|||||||
|
|
||||||
def get_backup_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')
|
bd = get_setting('INVENTREE_BACKUP_DIR', 'backup_dir')
|
||||||
|
|
||||||
if not bd:
|
if not bd:
|
||||||
@ -258,7 +251,6 @@ def get_plugin_file():
|
|||||||
|
|
||||||
Note: It will be created if it does not already exist!
|
Note: It will be created if it does not already exist!
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Check if the plugin.txt file (specifying required plugins) is specified
|
# Check if the plugin.txt file (specifying required plugins) is specified
|
||||||
plugin_file = get_setting('INVENTREE_PLUGIN_FILE', 'plugin_file')
|
plugin_file = get_setting('INVENTREE_PLUGIN_FILE', 'plugin_file')
|
||||||
|
|
||||||
@ -283,7 +275,6 @@ def get_plugin_file():
|
|||||||
|
|
||||||
def get_plugin_dir():
|
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')
|
return get_setting('INVENTREE_PLUGIN_DIR', 'plugin_dir')
|
||||||
|
|
||||||
|
|
||||||
@ -297,7 +288,6 @@ def get_secret_key():
|
|||||||
C) Look for default key file "secret_key.txt"
|
C) Look for default key file "secret_key.txt"
|
||||||
D) Create "secret_key.txt" if it does not exist
|
D) Create "secret_key.txt" if it does not exist
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Look for environment variable
|
# Look for environment variable
|
||||||
if secret_key := get_setting('INVENTREE_SECRET_KEY', 'secret_key'):
|
if secret_key := get_setting('INVENTREE_SECRET_KEY', 'secret_key'):
|
||||||
logger.info("SECRET_KEY loaded by INVENTREE_SECRET_KEY") # pragma: no cover
|
logger.info("SECRET_KEY loaded by INVENTREE_SECRET_KEY") # pragma: no cover
|
||||||
|
@ -15,7 +15,6 @@ logger = logging.getLogger('inventree')
|
|||||||
|
|
||||||
def get_unit_registry():
|
def get_unit_registry():
|
||||||
"""Return a custom instance of the Pint UnitRegistry."""
|
"""Return a custom instance of the Pint UnitRegistry."""
|
||||||
|
|
||||||
global _unit_registry
|
global _unit_registry
|
||||||
|
|
||||||
# Cache the unit registry for speedier access
|
# Cache the unit registry for speedier access
|
||||||
@ -30,7 +29,6 @@ def reload_unit_registry():
|
|||||||
|
|
||||||
This function is called at startup, and whenever the database is updated.
|
This function is called at startup, and whenever the database is updated.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import time
|
import time
|
||||||
t_start = time.time()
|
t_start = time.time()
|
||||||
|
|
||||||
@ -84,7 +82,6 @@ def convert_physical_value(value: str, unit: str = None, strip_units=True):
|
|||||||
Returns:
|
Returns:
|
||||||
The converted quantity, in the specified units
|
The converted quantity, in the specified units
|
||||||
"""
|
"""
|
||||||
|
|
||||||
original = str(value).strip()
|
original = str(value).strip()
|
||||||
|
|
||||||
# Ensure that the value is a string
|
# Ensure that the value is a string
|
||||||
|
@ -52,7 +52,6 @@ def is_email_configured():
|
|||||||
|
|
||||||
def send_email(subject, body, recipients, from_email=None, html_message=None):
|
def send_email(subject, body, recipients, from_email=None, html_message=None):
|
||||||
"""Send an email with the specified subject and body, to the specified recipients list."""
|
"""Send an email with the specified subject and body, to the specified recipients list."""
|
||||||
|
|
||||||
if isinstance(recipients, str):
|
if isinstance(recipients, str):
|
||||||
recipients = [recipients]
|
recipients = [recipients]
|
||||||
|
|
||||||
|
@ -31,7 +31,6 @@ def log_error(path):
|
|||||||
Arguments:
|
Arguments:
|
||||||
path: The 'path' (most likely a URL) associated with this error (optional)
|
path: The 'path' (most likely a URL) associated with this error (optional)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
kind, info, data = sys.exc_info()
|
kind, info, data = sys.exc_info()
|
||||||
|
|
||||||
# Check if the error is on the ignore list
|
# Check if the error is on the ignore list
|
||||||
|
@ -22,7 +22,6 @@ class InvenTreeExchange(SimpleExchangeBackend):
|
|||||||
|
|
||||||
def get_rates(self, **kwargs) -> None:
|
def get_rates(self, **kwargs) -> None:
|
||||||
"""Set the requested currency codes and get rates."""
|
"""Set the requested currency codes and get rates."""
|
||||||
|
|
||||||
from common.models import InvenTreeSetting
|
from common.models import InvenTreeSetting
|
||||||
from plugin import registry
|
from plugin import registry
|
||||||
|
|
||||||
@ -74,7 +73,6 @@ class InvenTreeExchange(SimpleExchangeBackend):
|
|||||||
@atomic
|
@atomic
|
||||||
def update_rates(self, base_currency=None, **kwargs):
|
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})
|
backend, _ = ExchangeBackend.objects.update_or_create(name=self.name, defaults={"base_currency": base_currency})
|
||||||
|
|
||||||
if base_currency is None:
|
if base_currency is None:
|
||||||
|
@ -22,7 +22,6 @@ class InvenTreeRestURLField(RestURLField):
|
|||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
"""Update schemes."""
|
"""Update schemes."""
|
||||||
|
|
||||||
# Enforce 'max length' parameter in form validation
|
# Enforce 'max length' parameter in form validation
|
||||||
if 'max_length' not in kwargs:
|
if 'max_length' not in kwargs:
|
||||||
kwargs['max_length'] = 200
|
kwargs['max_length'] = 200
|
||||||
@ -38,7 +37,6 @@ class InvenTreeURLField(models.URLField):
|
|||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
"""Initialization method for InvenTreeURLField"""
|
"""Initialization method for InvenTreeURLField"""
|
||||||
|
|
||||||
# Max length for InvenTreeURLField is set to 200
|
# Max length for InvenTreeURLField is set to 200
|
||||||
kwargs['max_length'] = 200
|
kwargs['max_length'] = 200
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
@ -117,7 +115,6 @@ class InvenTreeMoneyField(MoneyField):
|
|||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
"""Override initial values with the real info from database."""
|
"""Override initial values with the real info from database."""
|
||||||
|
|
||||||
kwargs = money_kwargs(**kwargs)
|
kwargs = money_kwargs(**kwargs)
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
@ -150,7 +147,6 @@ class DatePickerFormField(forms.DateField):
|
|||||||
|
|
||||||
def round_decimal(value, places, normalize=False):
|
def round_decimal(value, places, normalize=False):
|
||||||
"""Round value to the specified number of places."""
|
"""Round value to the specified number of places."""
|
||||||
|
|
||||||
if type(value) in [Decimal, float]:
|
if type(value) in [Decimal, float]:
|
||||||
value = round(value, places)
|
value = round(value, places)
|
||||||
|
|
||||||
@ -187,7 +183,6 @@ class RoundingDecimalField(models.DecimalField):
|
|||||||
|
|
||||||
def formfield(self, **kwargs):
|
def formfield(self, **kwargs):
|
||||||
"""Return a Field instance for this field."""
|
"""Return a Field instance for this field."""
|
||||||
|
|
||||||
kwargs['form_class'] = RoundingDecimalFormField
|
kwargs['form_class'] = RoundingDecimalFormField
|
||||||
|
|
||||||
return super().formfield(**kwargs)
|
return super().formfield(**kwargs)
|
||||||
|
@ -15,7 +15,6 @@ class InvenTreeSearchFilter(filters.SearchFilter):
|
|||||||
The following query params are available to 'augment' the search (in decreasing order of priority)
|
The following query params are available to 'augment' the search (in decreasing order of priority)
|
||||||
- search_regex: If True, search is performed on 'regex' comparison
|
- search_regex: If True, search is performed on 'regex' comparison
|
||||||
"""
|
"""
|
||||||
|
|
||||||
regex = InvenTree.helpers.str2bool(request.query_params.get('search_regex', False))
|
regex = InvenTree.helpers.str2bool(request.query_params.get('search_regex', False))
|
||||||
|
|
||||||
search_fields = super().get_search_fields(view, request)
|
search_fields = super().get_search_fields(view, request)
|
||||||
@ -36,7 +35,6 @@ class InvenTreeSearchFilter(filters.SearchFilter):
|
|||||||
|
|
||||||
Depending on the request parameters, we may "augment" these somewhat
|
Depending on the request parameters, we may "augment" these somewhat
|
||||||
"""
|
"""
|
||||||
|
|
||||||
whole = InvenTree.helpers.str2bool(request.query_params.get('search_whole', False))
|
whole = InvenTree.helpers.str2bool(request.query_params.get('search_whole', False))
|
||||||
|
|
||||||
terms = []
|
terms = []
|
||||||
|
@ -11,7 +11,6 @@ def parse_format_string(fmt_string: str) -> dict:
|
|||||||
|
|
||||||
Returns a dict object which contains structured information about the format groups
|
Returns a dict object which contains structured information about the format groups
|
||||||
"""
|
"""
|
||||||
|
|
||||||
groups = string.Formatter().parse(fmt_string)
|
groups = string.Formatter().parse(fmt_string)
|
||||||
|
|
||||||
info = {}
|
info = {}
|
||||||
@ -62,7 +61,6 @@ def construct_format_regex(fmt_string: str) -> str:
|
|||||||
Raises:
|
Raises:
|
||||||
ValueError: Format string is invalid
|
ValueError: Format string is invalid
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pattern = "^"
|
pattern = "^"
|
||||||
|
|
||||||
for group in string.Formatter().parse(fmt_string):
|
for group in string.Formatter().parse(fmt_string):
|
||||||
@ -121,7 +119,6 @@ def validate_string(value: str, fmt_string: str) -> str:
|
|||||||
Raises:
|
Raises:
|
||||||
ValueError: The provided format string is invalid
|
ValueError: The provided format string is invalid
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pattern = construct_format_regex(fmt_string)
|
pattern = construct_format_regex(fmt_string)
|
||||||
|
|
||||||
result = re.match(pattern, value)
|
result = re.match(pattern, value)
|
||||||
@ -145,7 +142,6 @@ def extract_named_group(name: str, value: str, fmt_string: str) -> str:
|
|||||||
NameError: named value does not exist in the format string
|
NameError: named value does not exist in the format string
|
||||||
IndexError: named value could not be found in the provided entry
|
IndexError: named value could not be found in the provided entry
|
||||||
"""
|
"""
|
||||||
|
|
||||||
info = parse_format_string(fmt_string)
|
info = parse_format_string(fmt_string)
|
||||||
|
|
||||||
if name not in info.keys():
|
if name not in info.keys():
|
||||||
|
@ -176,7 +176,6 @@ class CustomLoginForm(LoginForm):
|
|||||||
First check that:
|
First check that:
|
||||||
- A valid user has been supplied
|
- A valid user has been supplied
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not self.user:
|
if not self.user:
|
||||||
# No user supplied - redirect to the login page
|
# No user supplied - redirect to the login page
|
||||||
return HttpResponseRedirect(reverse('account_login'))
|
return HttpResponseRedirect(reverse('account_login'))
|
||||||
@ -313,7 +312,6 @@ class CustomAccountAdapter(CustomUrlMixin, RegistratonMixin, OTPAdapter, Default
|
|||||||
|
|
||||||
def get_email_confirmation_url(self, request, emailconfirmation):
|
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
|
from InvenTree.helpers_model import construct_absolute_url
|
||||||
|
|
||||||
url = super().get_email_confirmation_url(request, emailconfirmation)
|
url = super().get_email_confirmation_url(request, emailconfirmation)
|
||||||
|
@ -51,7 +51,6 @@ def constructPathString(path, max_chars=250):
|
|||||||
path: A list of strings e.g. ['path', 'to', 'location']
|
path: A list of strings e.g. ['path', 'to', 'location']
|
||||||
max_chars: Maximum number of characters
|
max_chars: Maximum number of characters
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pathstring = '/'.join(path)
|
pathstring = '/'.join(path)
|
||||||
|
|
||||||
# Replace middle elements to limit the pathstring
|
# Replace middle elements to limit the pathstring
|
||||||
@ -93,7 +92,6 @@ def getBlankThumbnail():
|
|||||||
|
|
||||||
def getLogoImage(as_file=False, custom=True):
|
def getLogoImage(as_file=False, custom=True):
|
||||||
"""Return the InvenTree logo image, or a custom logo if available."""
|
"""Return the InvenTree logo image, or a custom logo if available."""
|
||||||
|
|
||||||
"""Return the path to the logo-file."""
|
"""Return the path to the logo-file."""
|
||||||
if custom and settings.CUSTOM_LOGO:
|
if custom and settings.CUSTOM_LOGO:
|
||||||
|
|
||||||
@ -122,7 +120,6 @@ def getLogoImage(as_file=False, custom=True):
|
|||||||
|
|
||||||
def getSplashScreen(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()
|
static_storage = StaticFilesStorage()
|
||||||
|
|
||||||
if custom and settings.CUSTOM_SPLASH:
|
if custom and settings.CUSTOM_SPLASH:
|
||||||
@ -338,7 +335,6 @@ def MakeBarcode(cls_name, object_pk: int, object_data=None, **kwargs):
|
|||||||
Returns:
|
Returns:
|
||||||
json string of the supplied data plus some other data
|
json string of the supplied data plus some other data
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if object_data is None:
|
if object_data is None:
|
||||||
object_data = {}
|
object_data = {}
|
||||||
|
|
||||||
@ -415,7 +411,6 @@ def increment_serial_number(serial: str):
|
|||||||
Returns:
|
Returns:
|
||||||
incremented value, or None if incrementing could not be performed.
|
incremented value, or None if incrementing could not be performed.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from plugin.registry import registry
|
from plugin.registry import registry
|
||||||
|
|
||||||
# Ensure we start with a string value
|
# Ensure we start with a string value
|
||||||
@ -452,7 +447,6 @@ def extract_serial_numbers(input_string, expected_quantity: int, starting_value=
|
|||||||
expected_quantity: The number of (unique) serial numbers we expect
|
expected_quantity: The number of (unique) serial numbers we expect
|
||||||
starting_value: Provide a starting value for the sequence (or None)
|
starting_value: Provide a starting value for the sequence (or None)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if starting_value is None:
|
if starting_value is None:
|
||||||
starting_value = increment_serial_number(None)
|
starting_value = increment_serial_number(None)
|
||||||
|
|
||||||
@ -724,7 +718,6 @@ def strip_html_tags(value: str, raise_error=True, field_name=None):
|
|||||||
|
|
||||||
If raise_error is True, a ValidationError will be thrown if HTML tags are detected
|
If raise_error is True, a ValidationError will be thrown if HTML tags are detected
|
||||||
"""
|
"""
|
||||||
|
|
||||||
cleaned = clean(
|
cleaned = clean(
|
||||||
value,
|
value,
|
||||||
strip=True,
|
strip=True,
|
||||||
@ -756,7 +749,6 @@ 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):
|
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
|
cleaned = value
|
||||||
|
|
||||||
if remove_ascii:
|
if remove_ascii:
|
||||||
@ -787,7 +779,6 @@ def hash_barcode(barcode_data):
|
|||||||
We first remove any non-printable characters from the barcode data,
|
We first remove any non-printable characters from the barcode data,
|
||||||
as some browsers have issues scanning characters in.
|
as some browsers have issues scanning characters in.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
barcode_data = str(barcode_data).strip()
|
barcode_data = str(barcode_data).strip()
|
||||||
barcode_data = remove_non_printable_characters(barcode_data)
|
barcode_data = remove_non_printable_characters(barcode_data)
|
||||||
|
|
||||||
@ -813,7 +804,6 @@ def get_objectreference(obj, type_ref: str = 'content_type', object_ref: str = '
|
|||||||
|
|
||||||
The method name must always be the name of the field prefixed by 'get_'
|
The method name must always be the name of the field prefixed by 'get_'
|
||||||
"""
|
"""
|
||||||
|
|
||||||
model_cls = getattr(obj, type_ref)
|
model_cls = getattr(obj, type_ref)
|
||||||
obj_id = getattr(obj, object_ref)
|
obj_id = getattr(obj, object_ref)
|
||||||
|
|
||||||
|
@ -41,7 +41,6 @@ def construct_absolute_url(*arg, **kwargs):
|
|||||||
2. If the InvenTree setting INVENTREE_BASE_URL is set, use that
|
2. If the InvenTree setting INVENTREE_BASE_URL is set, use that
|
||||||
3. Otherwise, use the current request URL (if available)
|
3. Otherwise, use the current request URL (if available)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
relative_url = '/'.join(arg)
|
relative_url = '/'.join(arg)
|
||||||
|
|
||||||
# If a site URL is provided, use that
|
# If a site URL is provided, use that
|
||||||
@ -96,7 +95,6 @@ def download_image_from_url(remote_url, timeout=2.5):
|
|||||||
ValueError: Server responded with invalid 'Content-Length' value
|
ValueError: Server responded with invalid 'Content-Length' value
|
||||||
TypeError: Response is not a valid image
|
TypeError: Response is not a valid image
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Check that the provided URL at least looks valid
|
# Check that the provided URL at least looks valid
|
||||||
validator = URLValidator()
|
validator = URLValidator()
|
||||||
validator(remote_url)
|
validator(remote_url)
|
||||||
@ -180,7 +178,6 @@ def render_currency(money, decimal_places=None, currency=None, include_symbol=Tr
|
|||||||
min_decimal_places: The minimum number of decimal places to render to. If unspecified, uses the PRICING_DECIMAL_PLACES_MIN setting.
|
min_decimal_places: The minimum number of decimal places to render to. If unspecified, uses the PRICING_DECIMAL_PLACES_MIN setting.
|
||||||
max_decimal_places: The maximum number of decimal places to render to. If unspecified, uses the PRICING_DECIMAL_PLACES setting.
|
max_decimal_places: The maximum number of decimal places to render to. If unspecified, uses the PRICING_DECIMAL_PLACES setting.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if money in [None, '']:
|
if money in [None, '']:
|
||||||
return '-'
|
return '-'
|
||||||
|
|
||||||
@ -234,7 +231,6 @@ def getModelsWithMixin(mixin_class) -> list:
|
|||||||
Returns:
|
Returns:
|
||||||
List of models that inherit from the given mixin class
|
List of models that inherit from the given mixin class
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
|
||||||
db_models = [x.model_class() for x in ContentType.objects.all() if x is not None]
|
db_models = [x.model_class() for x in ContentType.objects.all() if x is not None]
|
||||||
|
@ -12,7 +12,6 @@ from django.utils.translation import override as lang_over
|
|||||||
|
|
||||||
def render_file(file_name, source, target, locales, ctx):
|
def render_file(file_name, source, target, locales, ctx):
|
||||||
"""Renders a file into all provided locales."""
|
"""Renders a file into all provided locales."""
|
||||||
|
|
||||||
for locale in locales:
|
for locale in locales:
|
||||||
|
|
||||||
# Enforce lower-case for locale names
|
# Enforce lower-case for locale names
|
||||||
|
@ -125,7 +125,6 @@ class Check2FAMiddleware(BaseRequire2FAMiddleware):
|
|||||||
"""Check if user is required to have MFA enabled."""
|
"""Check if user is required to have MFA enabled."""
|
||||||
def require_2fa(self, request):
|
def require_2fa(self, request):
|
||||||
"""Use setting to check if MFA should be enforced for frontend page."""
|
"""Use setting to check if MFA should be enforced for frontend page."""
|
||||||
|
|
||||||
from common.models import InvenTreeSetting
|
from common.models import InvenTreeSetting
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -49,7 +49,6 @@ class CleanMixin():
|
|||||||
Ref: https://github.com/mozilla/bleach/issues/192
|
Ref: https://github.com/mozilla/bleach/issues/192
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
cleaned = strip_html_tags(data, field_name=field)
|
cleaned = strip_html_tags(data, field_name=field)
|
||||||
|
|
||||||
# By default, newline characters are removed
|
# By default, newline characters are removed
|
||||||
@ -93,7 +92,6 @@ class CleanMixin():
|
|||||||
Returns:
|
Returns:
|
||||||
dict: Provided data Sanitized; still in the same order.
|
dict: Provided data Sanitized; still in the same order.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
clean_data = {}
|
clean_data = {}
|
||||||
|
|
||||||
for k, v in data.items():
|
for k, v in data.items():
|
||||||
|
@ -73,7 +73,6 @@ class MetadataMixin(models.Model):
|
|||||||
|
|
||||||
def validate_metadata(self):
|
def validate_metadata(self):
|
||||||
"""Validate the metadata field."""
|
"""Validate the metadata field."""
|
||||||
|
|
||||||
# Ensure that the 'metadata' field is a valid dict object
|
# Ensure that the 'metadata' field is a valid dict object
|
||||||
if self.metadata is None:
|
if self.metadata is None:
|
||||||
self.metadata = {}
|
self.metadata = {}
|
||||||
@ -202,7 +201,6 @@ class ReferenceIndexingMixin(models.Model):
|
|||||||
|
|
||||||
This is defined by a global setting object, specified by the REFERENCE_PATTERN_SETTING attribute
|
This is defined by a global setting object, specified by the REFERENCE_PATTERN_SETTING attribute
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# By default, we return an empty string
|
# By default, we return an empty string
|
||||||
if cls.REFERENCE_PATTERN_SETTING is None:
|
if cls.REFERENCE_PATTERN_SETTING is None:
|
||||||
return ''
|
return ''
|
||||||
@ -218,7 +216,6 @@ class ReferenceIndexingMixin(models.Model):
|
|||||||
- Returns a python dict object which contains the context data for formatting the reference string.
|
- Returns a python dict object which contains the context data for formatting the reference string.
|
||||||
- The default implementation provides some default context information
|
- The default implementation provides some default context information
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'ref': cls.get_next_reference(),
|
'ref': cls.get_next_reference(),
|
||||||
'date': datetime.now(),
|
'date': datetime.now(),
|
||||||
@ -230,7 +227,6 @@ class ReferenceIndexingMixin(models.Model):
|
|||||||
|
|
||||||
In practice, this means the item with the highest reference value
|
In practice, this means the item with the highest reference value
|
||||||
"""
|
"""
|
||||||
|
|
||||||
query = cls.objects.all().order_by('-reference_int', '-pk')
|
query = cls.objects.all().order_by('-reference_int', '-pk')
|
||||||
|
|
||||||
if query.exists():
|
if query.exists():
|
||||||
@ -241,7 +237,6 @@ class ReferenceIndexingMixin(models.Model):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def get_next_reference(cls):
|
def get_next_reference(cls):
|
||||||
"""Return the next available reference value for this particular class."""
|
"""Return the next available reference value for this particular class."""
|
||||||
|
|
||||||
# Find the "most recent" item
|
# Find the "most recent" item
|
||||||
latest = cls.get_most_recent_item()
|
latest = cls.get_most_recent_item()
|
||||||
|
|
||||||
@ -270,7 +265,6 @@ class ReferenceIndexingMixin(models.Model):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def generate_reference(cls):
|
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()
|
fmt = cls.get_reference_pattern()
|
||||||
ctx = cls.get_reference_context()
|
ctx = cls.get_reference_context()
|
||||||
|
|
||||||
@ -310,7 +304,6 @@ class ReferenceIndexingMixin(models.Model):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def validate_reference_pattern(cls, pattern):
|
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()
|
ctx = cls.get_reference_context()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -336,7 +329,6 @@ class ReferenceIndexingMixin(models.Model):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def validate_reference_field(cls, value):
|
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()
|
pattern = cls.get_reference_pattern()
|
||||||
|
|
||||||
value = str(value).strip()
|
value = str(value).strip()
|
||||||
@ -368,7 +360,6 @@ class ReferenceIndexingMixin(models.Model):
|
|||||||
|
|
||||||
If we cannot extract using the pattern for some reason, fallback to the entire reference
|
If we cannot extract using the pattern for some reason, fallback to the entire reference
|
||||||
"""
|
"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Extract named group based on provided pattern
|
# Extract named group based on provided pattern
|
||||||
reference = InvenTree.format.extract_named_group('ref', reference, cls.get_reference_pattern())
|
reference = InvenTree.format.extract_named_group('ref', reference, cls.get_reference_pattern())
|
||||||
@ -390,7 +381,6 @@ class ReferenceIndexingMixin(models.Model):
|
|||||||
|
|
||||||
def extract_int(reference, clip=0x7fffffff, allow_negative=False):
|
def extract_int(reference, clip=0x7fffffff, allow_negative=False):
|
||||||
"""Extract an integer out of reference."""
|
"""Extract an integer out of reference."""
|
||||||
|
|
||||||
# Default value if we cannot convert to an integer
|
# Default value if we cannot convert to an integer
|
||||||
ref_int = 0
|
ref_int = 0
|
||||||
|
|
||||||
@ -571,7 +561,6 @@ class InvenTreeAttachment(models.Model):
|
|||||||
- If the attachment is a link to an external resource, return the link
|
- If the attachment is a link to an external resource, return the link
|
||||||
- If the attachment is an uploaded file, return the fully qualified media URL
|
- If the attachment is an uploaded file, return the fully qualified media URL
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if self.link:
|
if self.link:
|
||||||
return self.link
|
return self.link
|
||||||
|
|
||||||
@ -608,7 +597,6 @@ class InvenTreeTree(MPTTModel):
|
|||||||
Note that a 'unique_together' requirement for ('name', 'parent') is insufficient,
|
Note that a 'unique_together' requirement for ('name', 'parent') is insufficient,
|
||||||
as it ignores cases where parent=None (i.e. top-level items)
|
as it ignores cases where parent=None (i.e. top-level items)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
super().validate_unique(exclude)
|
super().validate_unique(exclude)
|
||||||
|
|
||||||
results = self.__class__.objects.filter(
|
results = self.__class__.objects.filter(
|
||||||
@ -631,7 +619,6 @@ class InvenTreeTree(MPTTModel):
|
|||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
"""Custom save method for InvenTreeTree abstract model"""
|
"""Custom save method for InvenTreeTree abstract model"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
except InvalidMove:
|
except InvalidMove:
|
||||||
@ -769,7 +756,6 @@ class InvenTreeTree(MPTTModel):
|
|||||||
name: <name>,
|
name: <name>,
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
'pk': item.pk,
|
'pk': item.pk,
|
||||||
@ -839,13 +825,11 @@ class InvenTreeBarcodeMixin(models.Model):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def barcode_model_type(cls):
|
def barcode_model_type(cls):
|
||||||
"""Return the model 'type' for creating a custom QR code."""
|
"""Return the model 'type' for creating a custom QR code."""
|
||||||
|
|
||||||
# By default, use the name of the class
|
# By default, use the name of the class
|
||||||
return cls.__name__.lower()
|
return cls.__name__.lower()
|
||||||
|
|
||||||
def format_barcode(self, **kwargs):
|
def format_barcode(self, **kwargs):
|
||||||
"""Return a JSON string for formatting a QR code for this model instance."""
|
"""Return a JSON string for formatting a QR code for this model instance."""
|
||||||
|
|
||||||
return InvenTree.helpers.MakeBarcode(
|
return InvenTree.helpers.MakeBarcode(
|
||||||
self.__class__.barcode_model_type(),
|
self.__class__.barcode_model_type(),
|
||||||
self.pk,
|
self.pk,
|
||||||
@ -855,18 +839,15 @@ class InvenTreeBarcodeMixin(models.Model):
|
|||||||
@property
|
@property
|
||||||
def barcode(self):
|
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)
|
return self.format_barcode(brief=True)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def lookup_barcode(cls, barcode_hash):
|
def lookup_barcode(cls, barcode_hash):
|
||||||
"""Check if a model instance exists with the specified third-party barcode hash."""
|
"""Check if a model instance exists with the specified third-party barcode hash."""
|
||||||
|
|
||||||
return cls.objects.filter(barcode_hash=barcode_hash).first()
|
return cls.objects.filter(barcode_hash=barcode_hash).first()
|
||||||
|
|
||||||
def assign_barcode(self, barcode_hash=None, barcode_data=None, raise_error=True, save=True):
|
def assign_barcode(self, barcode_hash=None, barcode_data=None, raise_error=True, save=True):
|
||||||
"""Assign an external (third-party) barcode to this object."""
|
"""Assign an external (third-party) barcode to this object."""
|
||||||
|
|
||||||
# Must provide either barcode_hash or barcode_data
|
# Must provide either barcode_hash or barcode_data
|
||||||
if barcode_hash is None and barcode_data is None:
|
if barcode_hash is None and barcode_data is None:
|
||||||
raise ValueError("Provide either 'barcode_hash' or 'barcode_data'")
|
raise ValueError("Provide either 'barcode_hash' or 'barcode_data'")
|
||||||
@ -894,7 +875,6 @@ class InvenTreeBarcodeMixin(models.Model):
|
|||||||
|
|
||||||
def unassign_barcode(self):
|
def unassign_barcode(self):
|
||||||
"""Unassign custom barcode from this model"""
|
"""Unassign custom barcode from this model"""
|
||||||
|
|
||||||
self.barcode_data = ''
|
self.barcode_data = ''
|
||||||
self.barcode_hash = ''
|
self.barcode_hash = ''
|
||||||
|
|
||||||
@ -919,7 +899,6 @@ def after_error_logged(sender, instance: Error, created: bool, **kwargs):
|
|||||||
|
|
||||||
- Send a UI notification to all users with staff status
|
- Send a UI notification to all users with staff status
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if created:
|
if created:
|
||||||
try:
|
try:
|
||||||
import common.models
|
import common.models
|
||||||
|
@ -9,7 +9,6 @@ import users.models
|
|||||||
|
|
||||||
def get_model_for_view(view, raise_error=True):
|
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'):
|
if hasattr(view, 'get_permission_model'):
|
||||||
return view.get_permission_model()
|
return view.get_permission_model()
|
||||||
|
|
||||||
|
@ -55,7 +55,6 @@ def sanitize_svg(file_data, strip: bool = True, elements: str = ALLOWED_ELEMENTS
|
|||||||
Returns:
|
Returns:
|
||||||
str: Sanitzied SVG file.
|
str: Sanitzied SVG file.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Handle byte-encoded data
|
# Handle byte-encoded data
|
||||||
if isinstance(file_data, bytes):
|
if isinstance(file_data, bytes):
|
||||||
file_data = file_data.decode('utf-8')
|
file_data = file_data.decode('utf-8')
|
||||||
|
@ -17,7 +17,6 @@ logger = logging.getLogger('inventree')
|
|||||||
|
|
||||||
def default_sentry_dsn():
|
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'
|
return 'https://3928ccdba1d34895abde28031fd00100@o378676.ingest.sentry.io/6494600'
|
||||||
|
|
||||||
|
|
||||||
@ -26,7 +25,6 @@ def sentry_ignore_errors():
|
|||||||
|
|
||||||
These error types will *not* be reported to sentry.io.
|
These error types will *not* be reported to sentry.io.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return [
|
return [
|
||||||
Http404,
|
Http404,
|
||||||
ValidationError,
|
ValidationError,
|
||||||
@ -39,7 +37,6 @@ def sentry_ignore_errors():
|
|||||||
|
|
||||||
def init_sentry(dsn, sample_rate, tags):
|
def init_sentry(dsn, sample_rate, tags):
|
||||||
"""Initialize sentry.io error reporting"""
|
"""Initialize sentry.io error reporting"""
|
||||||
|
|
||||||
logger.info("Initializing sentry.io integration")
|
logger.info("Initializing sentry.io integration")
|
||||||
|
|
||||||
sentry_sdk.init(
|
sentry_sdk.init(
|
||||||
@ -64,7 +61,6 @@ def init_sentry(dsn, sample_rate, tags):
|
|||||||
|
|
||||||
def report_exception(exc):
|
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 settings.SENTRY_ENABLED and settings.SENTRY_DSN:
|
||||||
|
|
||||||
if not any(isinstance(exc, e) for e in sentry_ignore_errors()):
|
if not any(isinstance(exc, e) for e in sentry_ignore_errors()):
|
||||||
|
@ -43,7 +43,6 @@ class InvenTreeMoneySerializer(MoneyField):
|
|||||||
|
|
||||||
def get_value(self, data):
|
def get_value(self, data):
|
||||||
"""Test that the returned amount is a valid Decimal."""
|
"""Test that the returned amount is a valid Decimal."""
|
||||||
|
|
||||||
amount = super(DecimalField, self).get_value(data)
|
amount = super(DecimalField, self).get_value(data)
|
||||||
|
|
||||||
# Convert an empty string to None
|
# Convert an empty string to None
|
||||||
@ -73,7 +72,6 @@ class InvenTreeCurrencySerializer(serializers.ChoiceField):
|
|||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
"""Initialize the currency serializer"""
|
"""Initialize the currency serializer"""
|
||||||
|
|
||||||
choices = currency_code_mappings()
|
choices = currency_code_mappings()
|
||||||
|
|
||||||
allow_blank = kwargs.get('allow_blank', False) or kwargs.get('allow_null', False)
|
allow_blank = kwargs.get('allow_blank', False) or kwargs.get('allow_null', False)
|
||||||
@ -197,7 +195,6 @@ class InvenTreeModelSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
"""Custom create method which supports field adjustment"""
|
"""Custom create method which supports field adjustment"""
|
||||||
|
|
||||||
initial_data = validated_data.copy()
|
initial_data = validated_data.copy()
|
||||||
|
|
||||||
# Remove any fields which do not exist on the model
|
# Remove any fields which do not exist on the model
|
||||||
@ -221,7 +218,6 @@ class InvenTreeModelSerializer(serializers.ModelSerializer):
|
|||||||
In addition to running validators on the serializer fields,
|
In addition to running validators on the serializer fields,
|
||||||
this class ensures that the underlying model is also validated.
|
this class ensures that the underlying model is also validated.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Run any native validation checks first (may raise a ValidationError)
|
# Run any native validation checks first (may raise a ValidationError)
|
||||||
data = super().run_validation(data)
|
data = super().run_validation(data)
|
||||||
|
|
||||||
@ -705,7 +701,6 @@ class RemoteImageMixin(metaclass=serializers.SerializerMetaclass):
|
|||||||
|
|
||||||
def skip_create_fields(self):
|
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 [
|
return [
|
||||||
'remote_image',
|
'remote_image',
|
||||||
]
|
]
|
||||||
@ -724,7 +719,6 @@ class RemoteImageMixin(metaclass=serializers.SerializerMetaclass):
|
|||||||
- Attempt to download the image and store it against this object instance
|
- Attempt to download the image and store it against this object instance
|
||||||
- Catches and re-throws any errors
|
- Catches and re-throws any errors
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not url:
|
if not url:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -31,7 +31,6 @@ class GenericOAuth2ApiConnectView(GenericOAuth2ApiLoginView):
|
|||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
"""Dispatch the connect request directly."""
|
"""Dispatch the connect request directly."""
|
||||||
|
|
||||||
# Override the request method be in connection mode
|
# Override the request method be in connection mode
|
||||||
request.GET = request.GET.copy()
|
request.GET = request.GET.copy()
|
||||||
request.GET['process'] = 'connect'
|
request.GET['process'] = 'connect'
|
||||||
|
@ -89,7 +89,6 @@ def check_daily_holdoff(task_name: str, n_days: int = 1) -> bool:
|
|||||||
Note that this function creates some *hidden* global settings (designated with the _ prefix),
|
Note that this function creates some *hidden* global settings (designated with the _ prefix),
|
||||||
which are used to keep a running track of when the particular task was was last run.
|
which are used to keep a running track of when the particular task was was last run.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from common.models import InvenTreeSetting
|
from common.models import InvenTreeSetting
|
||||||
from InvenTree.ready import isInTestMode
|
from InvenTree.ready import isInTestMode
|
||||||
|
|
||||||
@ -146,7 +145,6 @@ def check_daily_holdoff(task_name: str, n_days: int = 1) -> bool:
|
|||||||
|
|
||||||
def record_task_attempt(task_name: str):
|
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
|
from common.models import InvenTreeSetting
|
||||||
|
|
||||||
logger.info("Logging task attempt for '%s'", task_name)
|
logger.info("Logging task attempt for '%s'", task_name)
|
||||||
@ -156,7 +154,6 @@ def record_task_attempt(task_name: str):
|
|||||||
|
|
||||||
def record_task_success(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
|
from common.models import InvenTreeSetting
|
||||||
|
|
||||||
InvenTreeSetting.set_setting(f'_{task_name}_SUCCESS', datetime.now().isoformat(), None)
|
InvenTreeSetting.set_setting(f'_{task_name}_SUCCESS', datetime.now().isoformat(), None)
|
||||||
@ -168,7 +165,6 @@ def offload_task(taskname, *args, force_async=False, force_sync=False, **kwargs)
|
|||||||
If workers are not running or force_sync flag
|
If workers are not running or force_sync flag
|
||||||
is set then the task is ran synchronously.
|
is set then the task is ran synchronously.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import importlib
|
import importlib
|
||||||
|
|
||||||
@ -353,7 +349,6 @@ def delete_successful_tasks():
|
|||||||
@scheduled_task(ScheduledTask.DAILY)
|
@scheduled_task(ScheduledTask.DAILY)
|
||||||
def delete_failed_tasks():
|
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:
|
try:
|
||||||
from django_q.models import Failure
|
from django_q.models import Failure
|
||||||
|
|
||||||
@ -402,7 +397,6 @@ def delete_old_error_logs():
|
|||||||
@scheduled_task(ScheduledTask.DAILY)
|
@scheduled_task(ScheduledTask.DAILY)
|
||||||
def delete_old_notifications():
|
def delete_old_notifications():
|
||||||
"""Delete old notification logs"""
|
"""Delete old notification logs"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from common.models import (InvenTreeSetting, NotificationEntry,
|
from common.models import (InvenTreeSetting, NotificationEntry,
|
||||||
NotificationMessage)
|
NotificationMessage)
|
||||||
@ -503,7 +497,6 @@ def update_exchange_rates(force: bool = False):
|
|||||||
Arguments:
|
Arguments:
|
||||||
force: If True, force the update to run regardless of the last update time
|
force: If True, force the update to run regardless of the last update time
|
||||||
"""
|
"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from djmoney.contrib.exchange.models import Rate
|
from djmoney.contrib.exchange.models import Rate
|
||||||
|
|
||||||
@ -547,7 +540,6 @@ def update_exchange_rates(force: bool = False):
|
|||||||
@scheduled_task(ScheduledTask.DAILY)
|
@scheduled_task(ScheduledTask.DAILY)
|
||||||
def run_backup():
|
def run_backup():
|
||||||
"""Run the backup command."""
|
"""Run the backup command."""
|
||||||
|
|
||||||
from common.models import InvenTreeSetting
|
from common.models import InvenTreeSetting
|
||||||
|
|
||||||
if not InvenTreeSetting.get_setting('INVENTREE_BACKUP_ENABLE', False, cache=False):
|
if not InvenTreeSetting.get_setting('INVENTREE_BACKUP_ENABLE', False, cache=False):
|
||||||
@ -582,7 +574,6 @@ def check_for_migrations():
|
|||||||
|
|
||||||
If the setting auto_update is enabled we will start updating.
|
If the setting auto_update is enabled we will start updating.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from common.models import InvenTreeSetting
|
from common.models import InvenTreeSetting
|
||||||
from plugin import registry
|
from plugin import registry
|
||||||
|
|
||||||
|
@ -16,7 +16,6 @@ class InvenTreeTemplateLoader(CachedLoader):
|
|||||||
Any custom report or label templates will be forced to reload (without cache).
|
Any custom report or label templates will be forced to reload (without cache).
|
||||||
This ensures that generated PDF reports / labels are always up-to-date.
|
This ensures that generated PDF reports / labels are always up-to-date.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# List of template patterns to skip cache for
|
# List of template patterns to skip cache for
|
||||||
skip_cache_dirs = [
|
skip_cache_dirs = [
|
||||||
os.path.abspath(os.path.join(settings.MEDIA_ROOT, 'report')),
|
os.path.abspath(os.path.join(settings.MEDIA_ROOT, 'report')),
|
||||||
|
@ -267,7 +267,6 @@ class BulkDeleteTests(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
def test_errors(self):
|
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')
|
url = reverse('api-stock-test-result-list')
|
||||||
|
|
||||||
# DELETE without any of the required fields
|
# DELETE without any of the required fields
|
||||||
@ -318,7 +317,6 @@ class SearchTests(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
def test_empty(self):
|
def test_empty(self):
|
||||||
"""Test empty request"""
|
"""Test empty request"""
|
||||||
|
|
||||||
data = [
|
data = [
|
||||||
'',
|
'',
|
||||||
None,
|
None,
|
||||||
@ -331,7 +329,6 @@ class SearchTests(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
def test_results(self):
|
def test_results(self):
|
||||||
"""Test individual result types"""
|
"""Test individual result types"""
|
||||||
|
|
||||||
response = self.post(
|
response = self.post(
|
||||||
reverse('api-search'),
|
reverse('api-search'),
|
||||||
{
|
{
|
||||||
@ -374,7 +371,6 @@ class SearchTests(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
def test_permissions(self):
|
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
|
# First, remove all roles
|
||||||
for ruleset in self.group.rule_sets.all():
|
for ruleset in self.group.rule_sets.all():
|
||||||
ruleset.can_view = False
|
ruleset.can_view = False
|
||||||
|
@ -45,7 +45,6 @@ class ViewTests(InvenTreeTestCase):
|
|||||||
|
|
||||||
def test_settings_page(self):
|
def test_settings_page(self):
|
||||||
"""Test that the 'settings' page loads correctly"""
|
"""Test that the 'settings' page loads correctly"""
|
||||||
|
|
||||||
# Settings page loads
|
# Settings page loads
|
||||||
url = reverse('settings')
|
url = reverse('settings')
|
||||||
|
|
||||||
@ -122,7 +121,6 @@ class ViewTests(InvenTreeTestCase):
|
|||||||
|
|
||||||
def test_url_login(self):
|
def test_url_login(self):
|
||||||
"""Test logging in via arguments"""
|
"""Test logging in via arguments"""
|
||||||
|
|
||||||
# Log out
|
# Log out
|
||||||
self.client.logout()
|
self.client.logout()
|
||||||
response = self.client.get("/index/")
|
response = self.client.get("/index/")
|
||||||
|
@ -44,7 +44,6 @@ class ConversionTest(TestCase):
|
|||||||
|
|
||||||
def test_prefixes(self):
|
def test_prefixes(self):
|
||||||
"""Test inputs where prefixes are used"""
|
"""Test inputs where prefixes are used"""
|
||||||
|
|
||||||
tests = {
|
tests = {
|
||||||
"3": 3,
|
"3": 3,
|
||||||
"3m": 3,
|
"3m": 3,
|
||||||
@ -78,7 +77,6 @@ class ConversionTest(TestCase):
|
|||||||
|
|
||||||
def test_dimensionless_units(self):
|
def test_dimensionless_units(self):
|
||||||
"""Tests for 'dimensionless' unit quantities"""
|
"""Tests for 'dimensionless' unit quantities"""
|
||||||
|
|
||||||
# Test some dimensionless units
|
# Test some dimensionless units
|
||||||
tests = {
|
tests = {
|
||||||
'ea': 1,
|
'ea': 1,
|
||||||
@ -106,7 +104,6 @@ class ConversionTest(TestCase):
|
|||||||
|
|
||||||
def test_invalid_units(self):
|
def test_invalid_units(self):
|
||||||
"""Test conversion with bad units"""
|
"""Test conversion with bad units"""
|
||||||
|
|
||||||
tests = {
|
tests = {
|
||||||
'3': '10',
|
'3': '10',
|
||||||
'13': '-?-',
|
'13': '-?-',
|
||||||
@ -121,7 +118,6 @@ class ConversionTest(TestCase):
|
|||||||
|
|
||||||
def test_invalid_values(self):
|
def test_invalid_values(self):
|
||||||
"""Test conversion of invalid inputs"""
|
"""Test conversion of invalid inputs"""
|
||||||
|
|
||||||
inputs = [
|
inputs = [
|
||||||
'-x',
|
'-x',
|
||||||
'1/0',
|
'1/0',
|
||||||
@ -140,7 +136,6 @@ class ConversionTest(TestCase):
|
|||||||
|
|
||||||
def test_custom_units(self):
|
def test_custom_units(self):
|
||||||
"""Tests for custom unit conversion"""
|
"""Tests for custom unit conversion"""
|
||||||
|
|
||||||
# Start with an empty set of units
|
# Start with an empty set of units
|
||||||
CustomUnit.objects.all().delete()
|
CustomUnit.objects.all().delete()
|
||||||
InvenTree.conversion.reload_unit_registry()
|
InvenTree.conversion.reload_unit_registry()
|
||||||
@ -214,7 +209,6 @@ class FormatTest(TestCase):
|
|||||||
|
|
||||||
def test_parse(self):
|
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
|
# Extract data from a valid format string
|
||||||
fmt = "PO-{abc:02f}-{ref:04d}-{date}-???"
|
fmt = "PO-{abc:02f}-{ref:04d}-{date}-???"
|
||||||
|
|
||||||
@ -236,7 +230,6 @@ class FormatTest(TestCase):
|
|||||||
|
|
||||||
def test_create_regex(self):
|
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 = {
|
tests = {
|
||||||
"PO-123-{ref:04f}": r"^PO\-123\-(?P<ref>.+)$",
|
"PO-123-{ref:04f}": r"^PO\-123\-(?P<ref>.+)$",
|
||||||
"{PO}-???-{ref}-{date}-22": r"^(?P<PO>.+)\-...\-(?P<ref>.+)\-(?P<date>.+)\-22$",
|
"{PO}-???-{ref}-{date}-22": r"^(?P<PO>.+)\-...\-(?P<ref>.+)\-(?P<date>.+)\-22$",
|
||||||
@ -249,7 +242,6 @@ class FormatTest(TestCase):
|
|||||||
|
|
||||||
def test_validate_format(self):
|
def test_validate_format(self):
|
||||||
"""Test that string validation works as expected"""
|
"""Test that string validation works as expected"""
|
||||||
|
|
||||||
# These tests should pass
|
# These tests should pass
|
||||||
for value, pattern in {
|
for value, pattern in {
|
||||||
"ABC-hello-123": "???-{q}-###",
|
"ABC-hello-123": "???-{q}-###",
|
||||||
@ -270,7 +262,6 @@ class FormatTest(TestCase):
|
|||||||
|
|
||||||
def test_extract_value(self):
|
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
|
# Simple tests based on a straight-forward format string
|
||||||
fmt = "PO-###-{ref:04d}"
|
fmt = "PO-###-{ref:04d}"
|
||||||
|
|
||||||
@ -345,7 +336,6 @@ class TestHelpers(TestCase):
|
|||||||
|
|
||||||
def test_absolute_url(self):
|
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"
|
base = "https://demo.inventree.org:12345"
|
||||||
|
|
||||||
InvenTreeSetting.set_setting('INVENTREE_BASE_URL', base, change_user=None)
|
InvenTreeSetting.set_setting('INVENTREE_BASE_URL', base, change_user=None)
|
||||||
@ -418,9 +408,7 @@ class TestHelpers(TestCase):
|
|||||||
|
|
||||||
def test_logo_image(self):
|
def test_logo_image(self):
|
||||||
"""Test for retrieving logo image"""
|
"""Test for retrieving logo image"""
|
||||||
|
|
||||||
# By default, there is no custom logo provided
|
# By default, there is no custom logo provided
|
||||||
|
|
||||||
logo = helpers.getLogoImage()
|
logo = helpers.getLogoImage()
|
||||||
self.assertEqual(logo, '/static/img/inventree.png')
|
self.assertEqual(logo, '/static/img/inventree.png')
|
||||||
|
|
||||||
@ -429,7 +417,6 @@ class TestHelpers(TestCase):
|
|||||||
|
|
||||||
def test_download_image(self):
|
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
|
# Run check with a sequence of bad URLs
|
||||||
for url in [
|
for url in [
|
||||||
"blog",
|
"blog",
|
||||||
@ -489,7 +476,6 @@ class TestHelpers(TestCase):
|
|||||||
|
|
||||||
def test_model_mixin(self):
|
def test_model_mixin(self):
|
||||||
"""Test the getModelsWithMixin function"""
|
"""Test the getModelsWithMixin function"""
|
||||||
|
|
||||||
from InvenTree.models import InvenTreeBarcodeMixin
|
from InvenTree.models import InvenTreeBarcodeMixin
|
||||||
|
|
||||||
models = InvenTree.helpers_model.getModelsWithMixin(InvenTreeBarcodeMixin)
|
models = InvenTree.helpers_model.getModelsWithMixin(InvenTreeBarcodeMixin)
|
||||||
@ -829,7 +815,6 @@ class CurrencyTests(TestCase):
|
|||||||
|
|
||||||
def test_rates(self):
|
def test_rates(self):
|
||||||
"""Test exchange rate update."""
|
"""Test exchange rate update."""
|
||||||
|
|
||||||
# Initially, there will not be any exchange rate information
|
# Initially, there will not be any exchange rate information
|
||||||
rates = Rate.objects.all()
|
rates = Rate.objects.all()
|
||||||
|
|
||||||
@ -1083,7 +1068,6 @@ class TestOffloadTask(InvenTreeTestCase):
|
|||||||
|
|
||||||
Ref: https://github.com/inventree/InvenTree/pull/3273
|
Ref: https://github.com/inventree/InvenTree/pull/3273
|
||||||
"""
|
"""
|
||||||
|
|
||||||
offload_task(
|
offload_task(
|
||||||
'dummy_tasks.parts',
|
'dummy_tasks.parts',
|
||||||
part=Part.objects.get(pk=1),
|
part=Part.objects.get(pk=1),
|
||||||
@ -1106,7 +1090,6 @@ class TestOffloadTask(InvenTreeTestCase):
|
|||||||
|
|
||||||
def test_daily_holdoff(self):
|
def test_daily_holdoff(self):
|
||||||
"""Tests for daily task holdoff helper functions"""
|
"""Tests for daily task holdoff helper functions"""
|
||||||
|
|
||||||
import InvenTree.tasks
|
import InvenTree.tasks
|
||||||
|
|
||||||
with self.assertLogs(logger='inventree', level='INFO') as cm:
|
with self.assertLogs(logger='inventree', level='INFO') as cm:
|
||||||
@ -1162,7 +1145,6 @@ class BarcodeMixinTest(InvenTreeTestCase):
|
|||||||
|
|
||||||
def test_barcode_model_type(self):
|
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 part.models import Part
|
||||||
from stock.models import StockItem, StockLocation
|
from stock.models import StockItem, StockLocation
|
||||||
|
|
||||||
@ -1172,7 +1154,6 @@ class BarcodeMixinTest(InvenTreeTestCase):
|
|||||||
|
|
||||||
def test_barcode_hash(self):
|
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
|
# Test multiple values for the hashing function
|
||||||
# This is to ensure that the hash function is always "backwards compatible"
|
# This is to ensure that the hash function is always "backwards compatible"
|
||||||
hashing_tests = {
|
hashing_tests = {
|
||||||
@ -1208,7 +1189,6 @@ class MagicLoginTest(InvenTreeTestCase):
|
|||||||
|
|
||||||
def test_generation(self):
|
def test_generation(self):
|
||||||
"""Test that magic login tokens are generated correctly"""
|
"""Test that magic login tokens are generated correctly"""
|
||||||
|
|
||||||
# User does not exists
|
# User does not exists
|
||||||
resp = self.client.post(reverse('sesame-generate'), {'email': 1})
|
resp = self.client.post(reverse('sesame-generate'), {'email': 1})
|
||||||
self.assertEqual(resp.status_code, 200)
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
@ -40,7 +40,6 @@ def reload_translation_stats():
|
|||||||
|
|
||||||
def get_translation_percent(lang_code):
|
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:
|
if _translation_stats is None:
|
||||||
reload_translation_stats()
|
reload_translation_stats()
|
||||||
|
|
||||||
|
@ -143,7 +143,6 @@ class UserMixin:
|
|||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
"""Run setup for individual test methods"""
|
"""Run setup for individual test methods"""
|
||||||
|
|
||||||
if self.auto_login:
|
if self.auto_login:
|
||||||
self.client.login(username=self.username, password=self.password)
|
self.client.login(username=self.username, password=self.password)
|
||||||
|
|
||||||
@ -156,7 +155,6 @@ class UserMixin:
|
|||||||
assign_all: Set to True to assign *all* roles
|
assign_all: Set to True to assign *all* roles
|
||||||
group: The group to assign roles to (or leave None to use the group assigned to this class)
|
group: The group to assign roles to (or leave None to use the group assigned to this class)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if group is None:
|
if group is None:
|
||||||
group = cls.group
|
group = cls.group
|
||||||
|
|
||||||
@ -207,7 +205,6 @@ class ExchangeRateMixin:
|
|||||||
|
|
||||||
def generate_exchange_rates(self):
|
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 = {
|
rates = {
|
||||||
'AUD': 1.5,
|
'AUD': 1.5,
|
||||||
'CAD': 1.7,
|
'CAD': 1.7,
|
||||||
@ -271,7 +268,6 @@ class InvenTreeAPITestCase(ExchangeRateMixin, UserMixin, APITestCase):
|
|||||||
|
|
||||||
def checkResponse(self, url, method, expected_code, response):
|
def checkResponse(self, url, method, expected_code, response):
|
||||||
"""Debug output for an unexpected response"""
|
"""Debug output for an unexpected response"""
|
||||||
|
|
||||||
# No expected code, return
|
# No expected code, return
|
||||||
if expected_code is None:
|
if expected_code is None:
|
||||||
return
|
return
|
||||||
@ -318,7 +314,6 @@ class InvenTreeAPITestCase(ExchangeRateMixin, UserMixin, APITestCase):
|
|||||||
|
|
||||||
def post(self, url, data=None, expected_code=None, format='json'):
|
def post(self, url, data=None, expected_code=None, format='json'):
|
||||||
"""Issue a POST request."""
|
"""Issue a POST request."""
|
||||||
|
|
||||||
# Set default value - see B006
|
# Set default value - see B006
|
||||||
if data is None:
|
if data is None:
|
||||||
data = {}
|
data = {}
|
||||||
@ -331,7 +326,6 @@ class InvenTreeAPITestCase(ExchangeRateMixin, UserMixin, APITestCase):
|
|||||||
|
|
||||||
def delete(self, url, data=None, expected_code=None, format='json'):
|
def delete(self, url, data=None, expected_code=None, format='json'):
|
||||||
"""Issue a DELETE request."""
|
"""Issue a DELETE request."""
|
||||||
|
|
||||||
if data is None:
|
if data is None:
|
||||||
data = {}
|
data = {}
|
||||||
|
|
||||||
|
@ -17,7 +17,6 @@ import InvenTree.conversion
|
|||||||
|
|
||||||
def validate_physical_units(unit):
|
def validate_physical_units(unit):
|
||||||
"""Ensure that a given unit is a valid physical unit."""
|
"""Ensure that a given unit is a valid physical unit."""
|
||||||
|
|
||||||
unit = unit.strip()
|
unit = unit.strip()
|
||||||
|
|
||||||
# Ignore blank units
|
# Ignore blank units
|
||||||
@ -69,7 +68,6 @@ class AllowedURLValidator(validators.URLValidator):
|
|||||||
|
|
||||||
def validate_purchase_order_reference(value):
|
def validate_purchase_order_reference(value):
|
||||||
"""Validate the 'reference' field of a PurchaseOrder."""
|
"""Validate the 'reference' field of a PurchaseOrder."""
|
||||||
|
|
||||||
from order.models import PurchaseOrder
|
from order.models import PurchaseOrder
|
||||||
|
|
||||||
# If we get to here, run the "default" validation routine
|
# If we get to here, run the "default" validation routine
|
||||||
@ -78,7 +76,6 @@ def validate_purchase_order_reference(value):
|
|||||||
|
|
||||||
def validate_sales_order_reference(value):
|
def validate_sales_order_reference(value):
|
||||||
"""Validate the 'reference' field of a SalesOrder."""
|
"""Validate the 'reference' field of a SalesOrder."""
|
||||||
|
|
||||||
from order.models import SalesOrder
|
from order.models import SalesOrder
|
||||||
|
|
||||||
# If we get to here, run the "default" validation routine
|
# If we get to here, run the "default" validation routine
|
||||||
@ -140,7 +137,6 @@ def validate_part_name_format(value):
|
|||||||
|
|
||||||
Make sure that each template container has a field of Part Model
|
Make sure that each template container has a field of Part Model
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Make sure that the field_name exists in Part model
|
# Make sure that the field_name exists in Part model
|
||||||
from part.models import Part
|
from part.models import Part
|
||||||
|
|
||||||
|
@ -178,5 +178,4 @@ def inventreeTarget():
|
|||||||
|
|
||||||
def inventreePlatform():
|
def inventreePlatform():
|
||||||
"""Returns the platform for the instance."""
|
"""Returns the platform for the instance."""
|
||||||
|
|
||||||
return platform.platform(aliased=True)
|
return platform.platform(aliased=True)
|
||||||
|
@ -100,7 +100,6 @@ class BuildFilter(rest_filters.FilterSet):
|
|||||||
|
|
||||||
def filter_has_project_code(self, queryset, name, value):
|
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):
|
if str2bool(value):
|
||||||
return queryset.exclude(project_code=None)
|
return queryset.exclude(project_code=None)
|
||||||
else:
|
else:
|
||||||
@ -235,7 +234,6 @@ class BuildDetail(RetrieveUpdateDestroyAPI):
|
|||||||
|
|
||||||
def destroy(self, request, *args, **kwargs):
|
def destroy(self, request, *args, **kwargs):
|
||||||
"""Only allow deletion of a BuildOrder if the build status is CANCELLED"""
|
"""Only allow deletion of a BuildOrder if the build status is CANCELLED"""
|
||||||
|
|
||||||
build = self.get_object()
|
build = self.get_object()
|
||||||
|
|
||||||
if build.status != BuildStatus.CANCELLED:
|
if build.status != BuildStatus.CANCELLED:
|
||||||
@ -292,7 +290,6 @@ class BuildLineFilter(rest_filters.FilterSet):
|
|||||||
|
|
||||||
def filter_allocated(self, queryset, name, value):
|
def filter_allocated(self, queryset, name, value):
|
||||||
"""Filter by whether each BuildLine is fully allocated"""
|
"""Filter by whether each BuildLine is fully allocated"""
|
||||||
|
|
||||||
if str2bool(value):
|
if str2bool(value):
|
||||||
return queryset.filter(allocated__gte=F('quantity'))
|
return queryset.filter(allocated__gte=F('quantity'))
|
||||||
else:
|
else:
|
||||||
@ -309,7 +306,6 @@ class BuildLineFilter(rest_filters.FilterSet):
|
|||||||
- The quantity available for each BuildLine
|
- The quantity available for each BuildLine
|
||||||
- The quantity allocated for each BuildLine
|
- The quantity allocated for each BuildLine
|
||||||
"""
|
"""
|
||||||
|
|
||||||
flt = Q(quantity__lte=F('total_available_stock') + F('allocated'))
|
flt = Q(quantity__lte=F('total_available_stock') + F('allocated'))
|
||||||
|
|
||||||
if str2bool(value):
|
if str2bool(value):
|
||||||
|
@ -348,7 +348,6 @@ class Build(MPTTModel, InvenTree.models.InvenTreeBarcodeMixin, InvenTree.models.
|
|||||||
@property
|
@property
|
||||||
def tracked_line_items(self):
|
def tracked_line_items(self):
|
||||||
"""Returns the "trackable" BOM lines for this BuildOrder."""
|
"""Returns the "trackable" BOM lines for this BuildOrder."""
|
||||||
|
|
||||||
return self.build_lines.filter(bom_item__sub_part__trackable=True)
|
return self.build_lines.filter(bom_item__sub_part__trackable=True)
|
||||||
|
|
||||||
def has_tracked_line_items(self):
|
def has_tracked_line_items(self):
|
||||||
@ -358,7 +357,6 @@ class Build(MPTTModel, InvenTree.models.InvenTreeBarcodeMixin, InvenTree.models.
|
|||||||
@property
|
@property
|
||||||
def untracked_line_items(self):
|
def untracked_line_items(self):
|
||||||
"""Returns the "non trackable" BOM items for this BuildOrder."""
|
"""Returns the "non trackable" BOM items for this BuildOrder."""
|
||||||
|
|
||||||
return self.build_lines.filter(bom_item__sub_part__trackable=False)
|
return self.build_lines.filter(bom_item__sub_part__trackable=False)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -432,7 +430,6 @@ class Build(MPTTModel, InvenTree.models.InvenTreeBarcodeMixin, InvenTree.models.
|
|||||||
|
|
||||||
def is_partially_allocated(self):
|
def is_partially_allocated(self):
|
||||||
"""Test is this build order has any stock allocated against it"""
|
"""Test is this build order has any stock allocated against it"""
|
||||||
|
|
||||||
return self.allocated_stock.count() > 0
|
return self.allocated_stock.count() > 0
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -497,7 +494,6 @@ class Build(MPTTModel, InvenTree.models.InvenTreeBarcodeMixin, InvenTree.models.
|
|||||||
- Completed count must meet the required quantity
|
- Completed count must meet the required quantity
|
||||||
- Untracked parts must be allocated
|
- Untracked parts must be allocated
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if self.incomplete_count > 0:
|
if self.incomplete_count > 0:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@ -780,7 +776,6 @@ class Build(MPTTModel, InvenTree.models.InvenTreeBarcodeMixin, InvenTree.models.
|
|||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def trim_allocated_stock(self):
|
def trim_allocated_stock(self):
|
||||||
"""Called after save to reduce allocated stock if the build order is now overallocated."""
|
"""Called after save to reduce allocated stock if the build order is now overallocated."""
|
||||||
|
|
||||||
# Only need to worry about untracked stock here
|
# Only need to worry about untracked stock here
|
||||||
for build_line in self.untracked_line_items:
|
for build_line in self.untracked_line_items:
|
||||||
|
|
||||||
@ -817,7 +812,6 @@ class Build(MPTTModel, InvenTree.models.InvenTreeBarcodeMixin, InvenTree.models.
|
|||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def subtract_allocated_stock(self, user):
|
def subtract_allocated_stock(self, user):
|
||||||
"""Called when the Build is marked as "complete", this function removes the allocated untracked items from stock."""
|
"""Called when the Build is marked as "complete", this function removes the allocated untracked items from stock."""
|
||||||
|
|
||||||
# Find all BuildItem objects which point to this build
|
# Find all BuildItem objects which point to this build
|
||||||
items = self.allocated_stock.filter(
|
items = self.allocated_stock.filter(
|
||||||
build_line__bom_item__sub_part__trackable=False
|
build_line__bom_item__sub_part__trackable=False
|
||||||
@ -839,7 +833,6 @@ class Build(MPTTModel, InvenTree.models.InvenTreeBarcodeMixin, InvenTree.models.
|
|||||||
- Set the item status to "scrapped"
|
- Set the item status to "scrapped"
|
||||||
- Add a transaction entry to the stock item history
|
- Add a transaction entry to the stock item history
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not output:
|
if not output:
|
||||||
raise ValidationError(_("No build output specified"))
|
raise ValidationError(_("No build output specified"))
|
||||||
|
|
||||||
@ -1069,7 +1062,6 @@ class Build(MPTTModel, InvenTree.models.InvenTreeBarcodeMixin, InvenTree.models.
|
|||||||
|
|
||||||
def unallocated_lines(self, tracked=None):
|
def unallocated_lines(self, tracked=None):
|
||||||
"""Returns a list of BuildLine objects which have not been fully allocated."""
|
"""Returns a list of BuildLine objects which have not been fully allocated."""
|
||||||
|
|
||||||
lines = self.build_lines.all()
|
lines = self.build_lines.all()
|
||||||
|
|
||||||
if tracked is True:
|
if tracked is True:
|
||||||
@ -1096,7 +1088,6 @@ class Build(MPTTModel, InvenTree.models.InvenTreeBarcodeMixin, InvenTree.models.
|
|||||||
Returns:
|
Returns:
|
||||||
True if the BuildOrder has been fully allocated, otherwise False
|
True if the BuildOrder has been fully allocated, otherwise False
|
||||||
"""
|
"""
|
||||||
|
|
||||||
lines = self.unallocated_lines(tracked=tracked)
|
lines = self.unallocated_lines(tracked=tracked)
|
||||||
return len(lines) == 0
|
return len(lines) == 0
|
||||||
|
|
||||||
@ -1109,7 +1100,6 @@ class Build(MPTTModel, InvenTree.models.InvenTreeBarcodeMixin, InvenTree.models.
|
|||||||
To determine if the output has been fully allocated,
|
To determine if the output has been fully allocated,
|
||||||
we need to test all "trackable" BuildLine objects
|
we need to test all "trackable" BuildLine objects
|
||||||
"""
|
"""
|
||||||
|
|
||||||
for line in self.build_lines.filter(bom_item__sub_part__trackable=True):
|
for line in self.build_lines.filter(bom_item__sub_part__trackable=True):
|
||||||
# Grab all BuildItem objects which point to this output
|
# Grab all BuildItem objects which point to this output
|
||||||
allocations = BuildItem.objects.filter(
|
allocations = BuildItem.objects.filter(
|
||||||
@ -1134,7 +1124,6 @@ class Build(MPTTModel, InvenTree.models.InvenTreeBarcodeMixin, InvenTree.models.
|
|||||||
Returns:
|
Returns:
|
||||||
True if any BuildLine has been over-allocated.
|
True if any BuildLine has been over-allocated.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
for line in self.build_lines.all():
|
for line in self.build_lines.all():
|
||||||
if line.is_overallocated():
|
if line.is_overallocated():
|
||||||
return True
|
return True
|
||||||
@ -1159,7 +1148,6 @@ class Build(MPTTModel, InvenTree.models.InvenTreeBarcodeMixin, InvenTree.models.
|
|||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def create_build_line_items(self, prevent_duplicates=True):
|
def create_build_line_items(self, prevent_duplicates=True):
|
||||||
"""Create BuildLine objects for each BOM line in this BuildOrder."""
|
"""Create BuildLine objects for each BOM line in this BuildOrder."""
|
||||||
|
|
||||||
lines = []
|
lines = []
|
||||||
|
|
||||||
bom_items = self.part.get_bom_items()
|
bom_items = self.part.get_bom_items()
|
||||||
@ -1192,7 +1180,6 @@ class Build(MPTTModel, InvenTree.models.InvenTreeBarcodeMixin, InvenTree.models.
|
|||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def update_build_line_items(self):
|
def update_build_line_items(self):
|
||||||
"""Rebuild required quantity field for each BuildLine object"""
|
"""Rebuild required quantity field for each BuildLine object"""
|
||||||
|
|
||||||
lines_to_update = []
|
lines_to_update = []
|
||||||
|
|
||||||
for line in self.build_lines.all():
|
for line in self.build_lines.all():
|
||||||
@ -1296,7 +1283,6 @@ class BuildLine(models.Model):
|
|||||||
|
|
||||||
def allocated_quantity(self):
|
def allocated_quantity(self):
|
||||||
"""Calculate the total allocated quantity for this BuildLine"""
|
"""Calculate the total allocated quantity for this BuildLine"""
|
||||||
|
|
||||||
# Queryset containing all BuildItem objects allocated against this BuildLine
|
# Queryset containing all BuildItem objects allocated against this BuildLine
|
||||||
allocations = self.allocations.all()
|
allocations = self.allocations.all()
|
||||||
|
|
||||||
@ -1312,7 +1298,6 @@ class BuildLine(models.Model):
|
|||||||
|
|
||||||
def is_fully_allocated(self):
|
def is_fully_allocated(self):
|
||||||
"""Return True if this BuildLine is fully allocated"""
|
"""Return True if this BuildLine is fully allocated"""
|
||||||
|
|
||||||
if self.bom_item.consumable:
|
if self.bom_item.consumable:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -129,7 +129,6 @@ class BuildSerializer(InvenTreeModelSerializer):
|
|||||||
|
|
||||||
def validate_reference(self, reference):
|
def validate_reference(self, reference):
|
||||||
"""Custom validation for the Build reference field"""
|
"""Custom validation for the Build reference field"""
|
||||||
|
|
||||||
# Ensure the reference matches the required pattern
|
# Ensure the reference matches the required pattern
|
||||||
Build.validate_reference_field(reference)
|
Build.validate_reference_field(reference)
|
||||||
|
|
||||||
@ -209,7 +208,6 @@ class BuildOutputQuantitySerializer(BuildOutputSerializer):
|
|||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
"""Validate the serializer data"""
|
"""Validate the serializer data"""
|
||||||
|
|
||||||
data = super().validate(data)
|
data = super().validate(data)
|
||||||
|
|
||||||
output = data.get('output')
|
output = data.get('output')
|
||||||
@ -450,7 +448,6 @@ class BuildOutputScrapSerializer(serializers.Serializer):
|
|||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
"""Save the serializer to scrap the build outputs"""
|
"""Save the serializer to scrap the build outputs"""
|
||||||
|
|
||||||
build = self.context['build']
|
build = self.context['build']
|
||||||
request = self.context['request']
|
request = self.context['request']
|
||||||
data = self.validated_data
|
data = self.validated_data
|
||||||
@ -625,7 +622,6 @@ class BuildCompleteSerializer(serializers.Serializer):
|
|||||||
|
|
||||||
This is so we can determine (at run time) whether the build is ready to be completed.
|
This is so we can determine (at run time) whether the build is ready to be completed.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
build = self.context['build']
|
build = self.context['build']
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -1095,7 +1091,6 @@ class BuildLineSerializer(InvenTreeModelSerializer):
|
|||||||
- available: Total stock available for allocation against this build line
|
- available: Total stock available for allocation against this build line
|
||||||
- on_order: Total stock on order for this build line
|
- on_order: Total stock on order for this build line
|
||||||
"""
|
"""
|
||||||
|
|
||||||
queryset = queryset.select_related(
|
queryset = queryset.select_related(
|
||||||
'build', 'bom_item',
|
'build', 'bom_item',
|
||||||
)
|
)
|
||||||
|
@ -29,7 +29,6 @@ def update_build_order_lines(bom_item_pk: int):
|
|||||||
|
|
||||||
This task is triggered when a BomItem is created or updated.
|
This task is triggered when a BomItem is created or updated.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
logger.info("Updating build order lines for BomItem %s", bom_item_pk)
|
logger.info("Updating build order lines for BomItem %s", bom_item_pk)
|
||||||
|
|
||||||
bom_item = part_models.BomItem.objects.filter(pk=bom_item_pk).first()
|
bom_item = part_models.BomItem.objects.filter(pk=bom_item_pk).first()
|
||||||
@ -156,7 +155,6 @@ def check_build_stock(build: build.models.Build):
|
|||||||
|
|
||||||
def notify_overdue_build_order(bo: build.models.Build):
|
def notify_overdue_build_order(bo: build.models.Build):
|
||||||
"""Notify appropriate users that a Build has just become 'overdue'"""
|
"""Notify appropriate users that a Build has just become 'overdue'"""
|
||||||
|
|
||||||
targets = []
|
targets = []
|
||||||
|
|
||||||
if bo.issued_by:
|
if bo.issued_by:
|
||||||
@ -202,7 +200,6 @@ def check_overdue_build_orders():
|
|||||||
- Look at the 'target_date' of any outstanding BuildOrder objects
|
- Look at the 'target_date' of any outstanding BuildOrder objects
|
||||||
- If the 'target_date' expired *yesterday* then the order is just out of date
|
- If the 'target_date' expired *yesterday* then the order is just out of date
|
||||||
"""
|
"""
|
||||||
|
|
||||||
yesterday = datetime.now().date() - timedelta(days=1)
|
yesterday = datetime.now().date() - timedelta(days=1)
|
||||||
|
|
||||||
overdue_orders = build.models.Build.objects.filter(
|
overdue_orders = build.models.Build.objects.filter(
|
||||||
|
@ -279,7 +279,6 @@ class BuildTest(BuildAPITest):
|
|||||||
|
|
||||||
def test_delete(self):
|
def test_delete(self):
|
||||||
"""Test that we can delete a BuildOrder via the API"""
|
"""Test that we can delete a BuildOrder via the API"""
|
||||||
|
|
||||||
bo = Build.objects.get(pk=1)
|
bo = Build.objects.get(pk=1)
|
||||||
|
|
||||||
url = reverse('api-build-detail', kwargs={'pk': bo.pk})
|
url = reverse('api-build-detail', kwargs={'pk': bo.pk})
|
||||||
@ -684,9 +683,7 @@ class BuildAllocationTest(BuildAPITest):
|
|||||||
|
|
||||||
def test_invalid_bom_item(self):
|
def test_invalid_bom_item(self):
|
||||||
"""Test by passing an invalid BOM item."""
|
"""Test by passing an invalid BOM item."""
|
||||||
|
|
||||||
# Find the right (in this case, wrong) BuildLine instance
|
# Find the right (in this case, wrong) BuildLine instance
|
||||||
|
|
||||||
si = StockItem.objects.get(pk=11)
|
si = StockItem.objects.get(pk=11)
|
||||||
lines = self.build.build_lines.all()
|
lines = self.build.build_lines.all()
|
||||||
|
|
||||||
@ -718,7 +715,6 @@ class BuildAllocationTest(BuildAPITest):
|
|||||||
|
|
||||||
This should result in creation of a new BuildItem object
|
This should result in creation of a new BuildItem object
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Find the correct BuildLine
|
# Find the correct BuildLine
|
||||||
si = StockItem.objects.get(pk=2)
|
si = StockItem.objects.get(pk=2)
|
||||||
|
|
||||||
@ -758,7 +754,6 @@ class BuildAllocationTest(BuildAPITest):
|
|||||||
|
|
||||||
This should increment the quantity of the existing BuildItem object
|
This should increment the quantity of the existing BuildItem object
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Find the correct BuildLine
|
# Find the correct BuildLine
|
||||||
si = StockItem.objects.get(pk=2)
|
si = StockItem.objects.get(pk=2)
|
||||||
|
|
||||||
@ -875,7 +870,6 @@ class BuildOverallocationTest(BuildAPITest):
|
|||||||
|
|
||||||
def test_setup(self):
|
def test_setup(self):
|
||||||
"""Validate expected state after set-up."""
|
"""Validate expected state after set-up."""
|
||||||
|
|
||||||
self.assertEqual(self.build.incomplete_outputs.count(), 0)
|
self.assertEqual(self.build.incomplete_outputs.count(), 0)
|
||||||
self.assertEqual(self.build.complete_outputs.count(), 1)
|
self.assertEqual(self.build.complete_outputs.count(), 1)
|
||||||
self.assertEqual(self.build.completed, self.build.quantity)
|
self.assertEqual(self.build.completed, self.build.quantity)
|
||||||
@ -1040,7 +1034,6 @@ class BuildOutputScrapTest(BuildAPITest):
|
|||||||
|
|
||||||
def scrap(self, build_id, data, expected_code=None):
|
def scrap(self, build_id, data, expected_code=None):
|
||||||
"""Helper method to POST to the scrap API"""
|
"""Helper method to POST to the scrap API"""
|
||||||
|
|
||||||
url = reverse('api-build-output-scrap', kwargs={'pk': build_id})
|
url = reverse('api-build-output-scrap', kwargs={'pk': build_id})
|
||||||
|
|
||||||
response = self.post(url, data, expected_code=expected_code)
|
response = self.post(url, data, expected_code=expected_code)
|
||||||
@ -1049,7 +1042,6 @@ class BuildOutputScrapTest(BuildAPITest):
|
|||||||
|
|
||||||
def test_invalid_scraps(self):
|
def test_invalid_scraps(self):
|
||||||
"""Test that invalid scrap attempts are rejected"""
|
"""Test that invalid scrap attempts are rejected"""
|
||||||
|
|
||||||
# Test with missing required fields
|
# Test with missing required fields
|
||||||
response = self.scrap(1, {}, expected_code=400)
|
response = self.scrap(1, {}, expected_code=400)
|
||||||
|
|
||||||
@ -1113,7 +1105,6 @@ class BuildOutputScrapTest(BuildAPITest):
|
|||||||
|
|
||||||
def test_valid_scraps(self):
|
def test_valid_scraps(self):
|
||||||
"""Test that valid scrap attempts succeed"""
|
"""Test that valid scrap attempts succeed"""
|
||||||
|
|
||||||
# Create a build output
|
# Create a build output
|
||||||
build = Build.objects.get(pk=1)
|
build = Build.objects.get(pk=1)
|
||||||
|
|
||||||
|
@ -45,7 +45,6 @@ class BuildTestBase(TestCase):
|
|||||||
- 7 x output_2
|
- 7 x output_2
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
super().setUpTestData()
|
super().setUpTestData()
|
||||||
|
|
||||||
# Create a base "Part"
|
# Create a base "Part"
|
||||||
@ -145,7 +144,6 @@ class BuildTest(BuildTestBase):
|
|||||||
|
|
||||||
def test_ref_int(self):
|
def test_ref_int(self):
|
||||||
"""Test the "integer reference" field used for natural sorting"""
|
"""Test the "integer reference" field used for natural sorting"""
|
||||||
|
|
||||||
# Set build reference to new value
|
# Set build reference to new value
|
||||||
common.models.InvenTreeSetting.set_setting('BUILDORDER_REFERENCE_PATTERN', 'BO-{ref}-???', change_user=None)
|
common.models.InvenTreeSetting.set_setting('BUILDORDER_REFERENCE_PATTERN', 'BO-{ref}-???', change_user=None)
|
||||||
|
|
||||||
@ -174,9 +172,7 @@ class BuildTest(BuildTestBase):
|
|||||||
|
|
||||||
def test_ref_validation(self):
|
def test_ref_validation(self):
|
||||||
"""Test that the reference field validation works as expected"""
|
"""Test that the reference field validation works as expected"""
|
||||||
|
|
||||||
# Default reference pattern = 'BO-{ref:04d}
|
# Default reference pattern = 'BO-{ref:04d}
|
||||||
|
|
||||||
# These patterns should fail
|
# These patterns should fail
|
||||||
for ref in [
|
for ref in [
|
||||||
'BO-1234x',
|
'BO-1234x',
|
||||||
@ -223,7 +219,6 @@ class BuildTest(BuildTestBase):
|
|||||||
|
|
||||||
def test_next_ref(self):
|
def test_next_ref(self):
|
||||||
"""Test that the next reference is automatically generated"""
|
"""Test that the next reference is automatically generated"""
|
||||||
|
|
||||||
common.models.InvenTreeSetting.set_setting('BUILDORDER_REFERENCE_PATTERN', 'XYZ-{ref:06d}', change_user=None)
|
common.models.InvenTreeSetting.set_setting('BUILDORDER_REFERENCE_PATTERN', 'XYZ-{ref:06d}', change_user=None)
|
||||||
|
|
||||||
build = Build.objects.create(
|
build = Build.objects.create(
|
||||||
@ -250,7 +245,6 @@ class BuildTest(BuildTestBase):
|
|||||||
|
|
||||||
def test_init(self):
|
def test_init(self):
|
||||||
"""Perform some basic tests before we start the ball rolling"""
|
"""Perform some basic tests before we start the ball rolling"""
|
||||||
|
|
||||||
self.assertEqual(StockItem.objects.count(), 10)
|
self.assertEqual(StockItem.objects.count(), 10)
|
||||||
|
|
||||||
# Build is PENDING
|
# Build is PENDING
|
||||||
@ -272,7 +266,6 @@ class BuildTest(BuildTestBase):
|
|||||||
|
|
||||||
def test_build_item_clean(self):
|
def test_build_item_clean(self):
|
||||||
"""Ensure that dodgy BuildItem objects cannot be created"""
|
"""Ensure that dodgy BuildItem objects cannot be created"""
|
||||||
|
|
||||||
stock = StockItem.objects.create(part=self.assembly, quantity=99)
|
stock = StockItem.objects.create(part=self.assembly, quantity=99)
|
||||||
|
|
||||||
# Create a BuiltItem which points to an invalid StockItem
|
# Create a BuiltItem which points to an invalid StockItem
|
||||||
@ -299,7 +292,6 @@ class BuildTest(BuildTestBase):
|
|||||||
|
|
||||||
def test_duplicate_bom_line(self):
|
def test_duplicate_bom_line(self):
|
||||||
"""Try to add a duplicate BOM item - it should be allowed"""
|
"""Try to add a duplicate BOM item - it should be allowed"""
|
||||||
|
|
||||||
BomItem.objects.create(
|
BomItem.objects.create(
|
||||||
part=self.assembly,
|
part=self.assembly,
|
||||||
sub_part=self.sub_part_1,
|
sub_part=self.sub_part_1,
|
||||||
@ -313,7 +305,6 @@ class BuildTest(BuildTestBase):
|
|||||||
output: StockItem object (or None)
|
output: StockItem object (or None)
|
||||||
allocations: Map of {StockItem: quantity}
|
allocations: Map of {StockItem: quantity}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
items_to_create = []
|
items_to_create = []
|
||||||
|
|
||||||
for item, quantity in allocations.items():
|
for item, quantity in allocations.items():
|
||||||
@ -335,7 +326,6 @@ class BuildTest(BuildTestBase):
|
|||||||
|
|
||||||
def test_partial_allocation(self):
|
def test_partial_allocation(self):
|
||||||
"""Test partial allocation of stock"""
|
"""Test partial allocation of stock"""
|
||||||
|
|
||||||
# Fully allocate tracked stock against build output 1
|
# Fully allocate tracked stock against build output 1
|
||||||
self.allocate_stock(
|
self.allocate_stock(
|
||||||
self.output_1,
|
self.output_1,
|
||||||
@ -409,7 +399,6 @@ class BuildTest(BuildTestBase):
|
|||||||
|
|
||||||
def test_overallocation_and_trim(self):
|
def test_overallocation_and_trim(self):
|
||||||
"""Test overallocation of stock and trim function"""
|
"""Test overallocation of stock and trim function"""
|
||||||
|
|
||||||
# Fully allocate tracked stock (not eligible for trimming)
|
# Fully allocate tracked stock (not eligible for trimming)
|
||||||
self.allocate_stock(
|
self.allocate_stock(
|
||||||
self.output_1,
|
self.output_1,
|
||||||
@ -484,9 +473,7 @@ class BuildTest(BuildTestBase):
|
|||||||
|
|
||||||
def test_cancel(self):
|
def test_cancel(self):
|
||||||
"""Test cancellation of the build"""
|
"""Test cancellation of the build"""
|
||||||
|
|
||||||
# TODO
|
# TODO
|
||||||
|
|
||||||
"""
|
"""
|
||||||
self.allocate_stock(50, 50, 200, self.output_1)
|
self.allocate_stock(50, 50, 200, self.output_1)
|
||||||
self.build.cancel_build(None)
|
self.build.cancel_build(None)
|
||||||
@ -497,7 +484,6 @@ class BuildTest(BuildTestBase):
|
|||||||
|
|
||||||
def test_complete(self):
|
def test_complete(self):
|
||||||
"""Test completion of a build output"""
|
"""Test completion of a build output"""
|
||||||
|
|
||||||
self.stock_1_1.quantity = 1000
|
self.stock_1_1.quantity = 1000
|
||||||
self.stock_1_1.save()
|
self.stock_1_1.save()
|
||||||
|
|
||||||
@ -567,7 +553,6 @@ class BuildTest(BuildTestBase):
|
|||||||
|
|
||||||
def test_overdue_notification(self):
|
def test_overdue_notification(self):
|
||||||
"""Test sending of notifications when a build order is overdue."""
|
"""Test sending of notifications when a build order is overdue."""
|
||||||
|
|
||||||
self.build.target_date = datetime.now().date() - timedelta(days=1)
|
self.build.target_date = datetime.now().date() - timedelta(days=1)
|
||||||
self.build.save()
|
self.build.save()
|
||||||
|
|
||||||
@ -583,7 +568,6 @@ class BuildTest(BuildTestBase):
|
|||||||
|
|
||||||
def test_new_build_notification(self):
|
def test_new_build_notification(self):
|
||||||
"""Test that a notification is sent when a new build is created"""
|
"""Test that a notification is sent when a new build is created"""
|
||||||
|
|
||||||
Build.objects.create(
|
Build.objects.create(
|
||||||
reference='BO-9999',
|
reference='BO-9999',
|
||||||
title='Some new build',
|
title='Some new build',
|
||||||
@ -609,7 +593,6 @@ class BuildTest(BuildTestBase):
|
|||||||
|
|
||||||
def test_metadata(self):
|
def test_metadata(self):
|
||||||
"""Unit tests for the metadata field."""
|
"""Unit tests for the metadata field."""
|
||||||
|
|
||||||
# Make sure a BuildItem exists before trying to run this test
|
# Make sure a BuildItem exists before trying to run this test
|
||||||
b = BuildItem(stock_item=self.stock_1_2, build_line=self.line_1, install_into=self.output_1, quantity=10)
|
b = BuildItem(stock_item=self.stock_1_2, build_line=self.line_1, install_into=self.output_1, quantity=10)
|
||||||
b.save()
|
b.save()
|
||||||
@ -664,7 +647,6 @@ class AutoAllocationTests(BuildTestBase):
|
|||||||
|
|
||||||
A "fully auto" allocation should allocate *all* of these stock items to the build
|
A "fully auto" allocation should allocate *all* of these stock items to the build
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# No build item allocations have been made against the build
|
# No build item allocations have been made against the build
|
||||||
self.assertEqual(self.build.allocated_stock.count(), 0)
|
self.assertEqual(self.build.allocated_stock.count(), 0)
|
||||||
|
|
||||||
@ -717,7 +699,6 @@ class AutoAllocationTests(BuildTestBase):
|
|||||||
|
|
||||||
def test_fully_auto(self):
|
def test_fully_auto(self):
|
||||||
"""We should be able to auto-allocate against a build in a single go"""
|
"""We should be able to auto-allocate against a build in a single go"""
|
||||||
|
|
||||||
self.build.auto_allocate_stock(
|
self.build.auto_allocate_stock(
|
||||||
interchangeable=True,
|
interchangeable=True,
|
||||||
substitutes=True,
|
substitutes=True,
|
||||||
|
@ -111,7 +111,6 @@ class TestReferencePatternMigration(MigratorTestCase):
|
|||||||
|
|
||||||
def prepare(self):
|
def prepare(self):
|
||||||
"""Create some initial data prior to migration"""
|
"""Create some initial data prior to migration"""
|
||||||
|
|
||||||
Setting = self.old_state.apps.get_model('common', 'inventreesetting')
|
Setting = self.old_state.apps.get_model('common', 'inventreesetting')
|
||||||
|
|
||||||
# Create a custom existing prefix so we can confirm the operation is working
|
# Create a custom existing prefix so we can confirm the operation is working
|
||||||
@ -141,7 +140,6 @@ class TestReferencePatternMigration(MigratorTestCase):
|
|||||||
|
|
||||||
def test_reference_migration(self):
|
def test_reference_migration(self):
|
||||||
"""Test that the reference fields have been correctly updated"""
|
"""Test that the reference fields have been correctly updated"""
|
||||||
|
|
||||||
Build = self.new_state.apps.get_model('build', 'build')
|
Build = self.new_state.apps.get_model('build', 'build')
|
||||||
|
|
||||||
for build in Build.objects.all():
|
for build in Build.objects.all():
|
||||||
@ -170,7 +168,6 @@ class TestBuildLineCreation(MigratorTestCase):
|
|||||||
|
|
||||||
def prepare(self):
|
def prepare(self):
|
||||||
"""Create data to work with"""
|
"""Create data to work with"""
|
||||||
|
|
||||||
# Model references
|
# Model references
|
||||||
Part = self.old_state.apps.get_model('part', 'part')
|
Part = self.old_state.apps.get_model('part', 'part')
|
||||||
BomItem = self.old_state.apps.get_model('part', 'bomitem')
|
BomItem = self.old_state.apps.get_model('part', 'bomitem')
|
||||||
@ -235,7 +232,6 @@ class TestBuildLineCreation(MigratorTestCase):
|
|||||||
|
|
||||||
def test_build_line_creation(self):
|
def test_build_line_creation(self):
|
||||||
"""Test that the BuildLine objects have been created correctly"""
|
"""Test that the BuildLine objects have been created correctly"""
|
||||||
|
|
||||||
Build = self.new_state.apps.get_model('build', 'build')
|
Build = self.new_state.apps.get_model('build', 'build')
|
||||||
BomItem = self.new_state.apps.get_model('part', 'bomitem')
|
BomItem = self.new_state.apps.get_model('part', 'bomitem')
|
||||||
BuildLine = self.new_state.apps.get_model('build', 'buildline')
|
BuildLine = self.new_state.apps.get_model('build', 'buildline')
|
||||||
|
@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
def generate_next_build_reference():
|
def generate_next_build_reference():
|
||||||
"""Generate the next available BuildOrder reference"""
|
"""Generate the next available BuildOrder reference"""
|
||||||
|
|
||||||
from build.models import Build
|
from build.models import Build
|
||||||
|
|
||||||
return Build.generate_reference()
|
return Build.generate_reference()
|
||||||
@ -11,7 +10,6 @@ def generate_next_build_reference():
|
|||||||
|
|
||||||
def validate_build_order_reference_pattern(pattern):
|
def validate_build_order_reference_pattern(pattern):
|
||||||
"""Validate the BuildOrder reference 'pattern' setting"""
|
"""Validate the BuildOrder reference 'pattern' setting"""
|
||||||
|
|
||||||
from build.models import Build
|
from build.models import Build
|
||||||
|
|
||||||
Build.validate_reference_pattern(pattern)
|
Build.validate_reference_pattern(pattern)
|
||||||
@ -19,7 +17,6 @@ def validate_build_order_reference_pattern(pattern):
|
|||||||
|
|
||||||
def validate_build_order_reference(value):
|
def validate_build_order_reference(value):
|
||||||
"""Validate that the BuildOrder reference field matches the required pattern."""
|
"""Validate that the BuildOrder reference field matches the required pattern."""
|
||||||
|
|
||||||
from build.models import Build
|
from build.models import Build
|
||||||
|
|
||||||
# If we get to here, run the "default" validation routine
|
# If we get to here, run the "default" validation routine
|
||||||
|
@ -113,7 +113,6 @@ class CurrencyExchangeView(APIView):
|
|||||||
|
|
||||||
def get(self, request, format=None):
|
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
|
# Extract a list of all available rates
|
||||||
try:
|
try:
|
||||||
rates = Rate.objects.all()
|
rates = Rate.objects.all()
|
||||||
@ -157,7 +156,6 @@ class CurrencyRefreshView(APIView):
|
|||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
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
|
from InvenTree.tasks import update_exchange_rates
|
||||||
|
|
||||||
update_exchange_rates(force=True)
|
update_exchange_rates(force=True)
|
||||||
@ -194,7 +192,6 @@ class GlobalSettingsList(SettingsList):
|
|||||||
|
|
||||||
def list(self, request, *args, **kwargs):
|
def list(self, request, *args, **kwargs):
|
||||||
"""Ensure all global settings are created"""
|
"""Ensure all global settings are created"""
|
||||||
|
|
||||||
common.models.InvenTreeSetting.build_default_values()
|
common.models.InvenTreeSetting.build_default_values()
|
||||||
return super().list(request, *args, **kwargs)
|
return super().list(request, *args, **kwargs)
|
||||||
|
|
||||||
@ -253,7 +250,6 @@ class UserSettingsList(SettingsList):
|
|||||||
|
|
||||||
def list(self, request, *args, **kwargs):
|
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)
|
common.models.InvenTreeUserSetting.build_default_values(user=request.user)
|
||||||
return super().list(request, *args, **kwargs)
|
return super().list(request, *args, **kwargs)
|
||||||
|
|
||||||
@ -385,7 +381,6 @@ class NotificationList(NotificationMessageMixin, BulkDeleteMixin, ListAPI):
|
|||||||
|
|
||||||
def filter_delete_queryset(self, queryset, request):
|
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)
|
queryset = queryset.filter(user=request.user)
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
@ -84,7 +84,6 @@ class BaseURLValidator(URLValidator):
|
|||||||
|
|
||||||
def __init__(self, schemes=None, **kwargs):
|
def __init__(self, schemes=None, **kwargs):
|
||||||
"""Custom init routine"""
|
"""Custom init routine"""
|
||||||
|
|
||||||
super().__init__(schemes, **kwargs)
|
super().__init__(schemes, **kwargs)
|
||||||
|
|
||||||
# Override default host_re value - allow optional tld regex
|
# Override default host_re value - allow optional tld regex
|
||||||
@ -204,7 +203,6 @@ class BaseInvenTreeSetting(models.Model):
|
|||||||
|
|
||||||
If a particular setting is not present, create it with the default value
|
If a particular setting is not present, create it with the default value
|
||||||
"""
|
"""
|
||||||
|
|
||||||
cache_key = f"BUILD_DEFAULT_VALUES:{str(cls.__name__)}"
|
cache_key = f"BUILD_DEFAULT_VALUES:{str(cls.__name__)}"
|
||||||
|
|
||||||
if InvenTree.helpers.str2bool(cache.get(cache_key, False)):
|
if InvenTree.helpers.str2bool(cache.get(cache_key, False)):
|
||||||
@ -255,7 +253,6 @@ class BaseInvenTreeSetting(models.Model):
|
|||||||
|
|
||||||
def save_to_cache(self):
|
def save_to_cache(self):
|
||||||
"""Save this setting object to cache"""
|
"""Save this setting object to cache"""
|
||||||
|
|
||||||
ckey = self.cache_key
|
ckey = self.cache_key
|
||||||
|
|
||||||
# skip saving to cache if no pk is set
|
# skip saving to cache if no pk is set
|
||||||
@ -283,7 +280,6 @@ class BaseInvenTreeSetting(models.Model):
|
|||||||
- The unique KEY string
|
- The unique KEY string
|
||||||
- Any key:value kwargs associated with the particular setting type (e.g. user-id)
|
- Any key:value kwargs associated with the particular setting type (e.g. user-id)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
key = f"{str(cls.__name__)}:{setting_key}"
|
key = f"{str(cls.__name__)}:{setting_key}"
|
||||||
|
|
||||||
for k, v in kwargs.items():
|
for k, v in kwargs.items():
|
||||||
@ -992,7 +988,6 @@ def validate_email_domains(setting):
|
|||||||
|
|
||||||
def currency_exchange_plugins():
|
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:
|
try:
|
||||||
from plugin import registry
|
from plugin import registry
|
||||||
plugs = registry.with_mixin('currencyexchange', active=True)
|
plugs = registry.with_mixin('currencyexchange', active=True)
|
||||||
@ -1006,7 +1001,6 @@ def currency_exchange_plugins():
|
|||||||
|
|
||||||
def update_exchange_rates(setting):
|
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():
|
if InvenTree.ready.isImportingData():
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -1018,7 +1012,6 @@ def update_exchange_rates(setting):
|
|||||||
|
|
||||||
def reload_plugin_registry(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
|
from plugin import registry
|
||||||
|
|
||||||
logger.info("Reloading plugin registry due to change in setting '%s'", setting.key)
|
logger.info("Reloading plugin registry due to change in setting '%s'", setting.key)
|
||||||
@ -2891,7 +2884,6 @@ class NewsFeedEntry(models.Model):
|
|||||||
|
|
||||||
def rename_notes_image(instance, filename):
|
def rename_notes_image(instance, filename):
|
||||||
"""Function for renaming uploading image file. Will store in the 'notes' directory."""
|
"""Function for renaming uploading image file. Will store in the 'notes' directory."""
|
||||||
|
|
||||||
fname = os.path.basename(filename)
|
fname = os.path.basename(filename)
|
||||||
return os.path.join('notes', fname)
|
return os.path.join('notes', fname)
|
||||||
|
|
||||||
@ -2936,7 +2928,6 @@ class CustomUnit(models.Model):
|
|||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
"""Validate that the provided custom unit is indeed valid"""
|
"""Validate that the provided custom unit is indeed valid"""
|
||||||
|
|
||||||
super().clean()
|
super().clean()
|
||||||
|
|
||||||
from InvenTree.conversion import get_unit_registry
|
from InvenTree.conversion import get_unit_registry
|
||||||
@ -2994,7 +2985,6 @@ class CustomUnit(models.Model):
|
|||||||
@receiver(post_delete, sender=CustomUnit, dispatch_uid='custom_unit_deleted')
|
@receiver(post_delete, sender=CustomUnit, dispatch_uid='custom_unit_deleted')
|
||||||
def after_custom_unit_updated(sender, instance, **kwargs):
|
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
|
# Force reload of the unit registry
|
||||||
from InvenTree.conversion import reload_unit_registry
|
from InvenTree.conversion import reload_unit_registry
|
||||||
reload_unit_registry()
|
reload_unit_registry()
|
||||||
|
@ -242,7 +242,6 @@ class UIMessageNotification(SingleNotificationMethod):
|
|||||||
|
|
||||||
def get_targets(self):
|
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]
|
return [target for target in self.targets if target.is_active]
|
||||||
|
|
||||||
def send(self, target):
|
def send(self, target):
|
||||||
|
@ -192,7 +192,6 @@ class NotificationMessageSerializer(InvenTreeModelSerializer):
|
|||||||
|
|
||||||
def get_target(self, obj):
|
def get_target(self, obj):
|
||||||
"""Function to resolve generic object reference to target."""
|
"""Function to resolve generic object reference to target."""
|
||||||
|
|
||||||
target = get_objectreference(obj, 'target_content_type', 'target_object_id')
|
target = get_objectreference(obj, 'target_content_type', 'target_object_id')
|
||||||
|
|
||||||
if target and 'link' not in target:
|
if target and 'link' not in target:
|
||||||
|
@ -12,7 +12,6 @@ logger = logging.getLogger('inventree')
|
|||||||
|
|
||||||
def currency_code_default():
|
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
|
from common.models import InvenTreeSetting
|
||||||
|
|
||||||
cached_value = cache.get('currency_code_default', '')
|
cached_value = cache.get('currency_code_default', '')
|
||||||
|
@ -268,7 +268,6 @@ class SettingsTest(InvenTreeTestCase):
|
|||||||
|
|
||||||
def test_global_setting_caching(self):
|
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'
|
key = 'PART_NAME_FORMAT'
|
||||||
|
|
||||||
cache_key = InvenTreeSetting.create_cache_key(key)
|
cache_key = InvenTreeSetting.create_cache_key(key)
|
||||||
@ -290,7 +289,6 @@ class SettingsTest(InvenTreeTestCase):
|
|||||||
|
|
||||||
def test_user_setting_caching(self):
|
def test_user_setting_caching(self):
|
||||||
"""Test caching operation for the user settings class"""
|
"""Test caching operation for the user settings class"""
|
||||||
|
|
||||||
cache.clear()
|
cache.clear()
|
||||||
|
|
||||||
# Generate a number of new users
|
# Generate a number of new users
|
||||||
@ -610,7 +608,6 @@ class NotificationUserSettingsApiTest(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
def test_setting(self):
|
def test_setting(self):
|
||||||
"""Test the string name for NotificationUserSetting."""
|
"""Test the string name for NotificationUserSetting."""
|
||||||
|
|
||||||
NotificationUserSetting.set_setting('NOTIFICATION_METHOD_MAIL', True, change_user=self.user, user=self.user)
|
NotificationUserSetting.set_setting('NOTIFICATION_METHOD_MAIL', True, change_user=self.user, user=self.user)
|
||||||
test_setting = NotificationUserSetting.get_setting_object('NOTIFICATION_METHOD_MAIL', user=self.user)
|
test_setting = NotificationUserSetting.get_setting_object('NOTIFICATION_METHOD_MAIL', user=self.user)
|
||||||
self.assertEqual(str(test_setting), 'NOTIFICATION_METHOD_MAIL (for testuser): True')
|
self.assertEqual(str(test_setting), 'NOTIFICATION_METHOD_MAIL (for testuser): True')
|
||||||
@ -823,7 +820,6 @@ class NotificationTest(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
def test_api_list(self):
|
def test_api_list(self):
|
||||||
"""Test list URL."""
|
"""Test list URL."""
|
||||||
|
|
||||||
url = reverse('api-notifications-list')
|
url = reverse('api-notifications-list')
|
||||||
|
|
||||||
self.get(url, expected_code=200)
|
self.get(url, expected_code=200)
|
||||||
@ -843,7 +839,6 @@ class NotificationTest(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
def test_bulk_delete(self):
|
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
|
from error_report.models import Error
|
||||||
|
|
||||||
# Create some notification messages by throwing errors
|
# Create some notification messages by throwing errors
|
||||||
@ -1019,7 +1014,6 @@ class CurrencyAPITests(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
def test_exchange_endpoint(self):
|
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)
|
response = self.get(reverse('api-currency-exchange'), expected_code=200)
|
||||||
|
|
||||||
self.assertIn('base_currency', response.data)
|
self.assertIn('base_currency', response.data)
|
||||||
@ -1027,7 +1021,6 @@ class CurrencyAPITests(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
def test_refresh_endpoint(self):
|
def test_refresh_endpoint(self):
|
||||||
"""Call the 'refresh currencies' endpoint"""
|
"""Call the 'refresh currencies' endpoint"""
|
||||||
|
|
||||||
from djmoney.contrib.exchange.models import Rate
|
from djmoney.contrib.exchange.models import Rate
|
||||||
|
|
||||||
# Delete any existing exchange rate data
|
# Delete any existing exchange rate data
|
||||||
@ -1053,7 +1046,6 @@ class NotesImageTest(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
def test_invalid_files(self):
|
def test_invalid_files(self):
|
||||||
"""Test that invalid files are rejected."""
|
"""Test that invalid files are rejected."""
|
||||||
|
|
||||||
n = NotesImage.objects.count()
|
n = NotesImage.objects.count()
|
||||||
|
|
||||||
# Test upload of a simple text file
|
# Test upload of a simple text file
|
||||||
@ -1085,7 +1077,6 @@ class NotesImageTest(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
def test_valid_image(self):
|
def test_valid_image(self):
|
||||||
"""Test upload of a valid image file"""
|
"""Test upload of a valid image file"""
|
||||||
|
|
||||||
n = NotesImage.objects.count()
|
n = NotesImage.objects.count()
|
||||||
|
|
||||||
# Construct a simple image file
|
# Construct a simple image file
|
||||||
@ -1132,13 +1123,11 @@ class ProjectCodesTest(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
def test_list(self):
|
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)
|
response = self.get(self.url, expected_code=200)
|
||||||
self.assertEqual(len(response.data), ProjectCode.objects.count())
|
self.assertEqual(len(response.data), ProjectCode.objects.count())
|
||||||
|
|
||||||
def test_delete(self):
|
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()
|
n = ProjectCode.objects.count()
|
||||||
|
|
||||||
# Get the first project code
|
# Get the first project code
|
||||||
@ -1155,7 +1144,6 @@ class ProjectCodesTest(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
def test_duplicate_code(self):
|
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
|
# Create a new project code
|
||||||
response = self.post(
|
response = self.post(
|
||||||
self.url,
|
self.url,
|
||||||
@ -1170,7 +1158,6 @@ class ProjectCodesTest(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
def test_write_access(self):
|
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
|
# By default user has staff access, can create a new project code
|
||||||
response = self.post(
|
response = self.post(
|
||||||
self.url,
|
self.url,
|
||||||
@ -1240,13 +1227,11 @@ class CustomUnitAPITest(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
def test_list(self):
|
def test_list(self):
|
||||||
"""Test API list functionality"""
|
"""Test API list functionality"""
|
||||||
|
|
||||||
response = self.get(self.url, expected_code=200)
|
response = self.get(self.url, expected_code=200)
|
||||||
self.assertEqual(len(response.data), CustomUnit.objects.count())
|
self.assertEqual(len(response.data), CustomUnit.objects.count())
|
||||||
|
|
||||||
def test_edit(self):
|
def test_edit(self):
|
||||||
"""Test edit permissions for CustomUnit model"""
|
"""Test edit permissions for CustomUnit model"""
|
||||||
|
|
||||||
unit = CustomUnit.objects.first()
|
unit = CustomUnit.objects.first()
|
||||||
|
|
||||||
# Try to edit without permission
|
# Try to edit without permission
|
||||||
@ -1278,7 +1263,6 @@ class CustomUnitAPITest(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
def test_validation(self):
|
def test_validation(self):
|
||||||
"""Test that validation works as expected"""
|
"""Test that validation works as expected"""
|
||||||
|
|
||||||
unit = CustomUnit.objects.first()
|
unit = CustomUnit.objects.first()
|
||||||
|
|
||||||
self.user.is_staff = True
|
self.user.is_staff = True
|
||||||
|
@ -382,7 +382,6 @@ class SupplierPartList(ListCreateDestroyAPIView):
|
|||||||
|
|
||||||
def get_serializer(self, *args, **kwargs):
|
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?
|
# Do we wish to include extra detail?
|
||||||
try:
|
try:
|
||||||
params = self.request.query_params
|
params = self.request.query_params
|
||||||
@ -489,7 +488,6 @@ class SupplierPriceBreakList(ListCreateAPI):
|
|||||||
|
|
||||||
def get_serializer(self, *args, **kwargs):
|
def get_serializer(self, *args, **kwargs):
|
||||||
"""Return serializer instance for this endpoint"""
|
"""Return serializer instance for this endpoint"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
params = self.request.query_params
|
params = self.request.query_params
|
||||||
|
|
||||||
|
@ -160,7 +160,6 @@ class Company(InvenTreeNotesMixin, MetadataMixin, models.Model):
|
|||||||
|
|
||||||
This property exists for backwards compatibility
|
This property exists for backwards compatibility
|
||||||
"""
|
"""
|
||||||
|
|
||||||
addr = self.primary_address
|
addr = self.primary_address
|
||||||
|
|
||||||
return str(addr) if addr is not None else None
|
return str(addr) if addr is not None else None
|
||||||
@ -287,7 +286,6 @@ class Address(models.Model):
|
|||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
"""Custom init function"""
|
"""Custom init function"""
|
||||||
|
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
@ -312,7 +310,6 @@ class Address(models.Model):
|
|||||||
|
|
||||||
- If this address is marked as "primary", ensure that all other addresses for this company are marked as non-primary
|
- If this address is marked as "primary", ensure that all other addresses for this company are marked as non-primary
|
||||||
"""
|
"""
|
||||||
|
|
||||||
others = list(Address.objects.filter(company=self.company).exclude(pk=self.pk).all())
|
others = list(Address.objects.filter(company=self.company).exclude(pk=self.pk).all())
|
||||||
|
|
||||||
# If this is the *only* address for this company, make it the primary one
|
# If this is the *only* address for this company, make it the primary one
|
||||||
@ -755,7 +752,6 @@ class SupplierPart(MetadataMixin, InvenTreeBarcodeMixin, common.models.MetaMixin
|
|||||||
|
|
||||||
def base_quantity(self, quantity=1) -> Decimal:
|
def base_quantity(self, quantity=1) -> Decimal:
|
||||||
"""Calculate the base unit quantiy for a given quantity."""
|
"""Calculate the base unit quantiy for a given quantity."""
|
||||||
|
|
||||||
q = Decimal(quantity) * Decimal(self.pack_quantity_native)
|
q = Decimal(quantity) * Decimal(self.pack_quantity_native)
|
||||||
q = round(q, 10).normalize()
|
q = round(q, 10).normalize()
|
||||||
|
|
||||||
@ -780,7 +776,6 @@ class SupplierPart(MetadataMixin, InvenTreeBarcodeMixin, common.models.MetaMixin
|
|||||||
|
|
||||||
def update_available_quantity(self, quantity):
|
def update_available_quantity(self, quantity):
|
||||||
"""Update the available quantity for this SupplierPart"""
|
"""Update the available quantity for this SupplierPart"""
|
||||||
|
|
||||||
self.available = quantity
|
self.available = quantity
|
||||||
self.availability_updated = datetime.now()
|
self.availability_updated = datetime.now()
|
||||||
self.save()
|
self.save()
|
||||||
@ -918,7 +913,6 @@ class SupplierPriceBreak(common.models.PriceBreak):
|
|||||||
@receiver(post_save, sender=SupplierPriceBreak, dispatch_uid='post_save_supplier_price_break')
|
@receiver(post_save, sender=SupplierPriceBreak, dispatch_uid='post_save_supplier_price_break')
|
||||||
def after_save_supplier_price(sender, instance, created, **kwargs):
|
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 InvenTree.ready.canAppAccessDatabase() and not InvenTree.ready.isImportingData():
|
||||||
|
|
||||||
if instance.part and instance.part.part:
|
if instance.part and instance.part.part:
|
||||||
@ -928,7 +922,6 @@ def after_save_supplier_price(sender, instance, created, **kwargs):
|
|||||||
@receiver(post_delete, sender=SupplierPriceBreak, dispatch_uid='post_delete_supplier_price_break')
|
@receiver(post_delete, sender=SupplierPriceBreak, dispatch_uid='post_delete_supplier_price_break')
|
||||||
def after_delete_supplier_price(sender, instance, **kwargs):
|
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 InvenTree.ready.canAppAccessDatabase() and not InvenTree.ready.isImportingData():
|
||||||
|
|
||||||
if instance.part and instance.part.part:
|
if instance.part and instance.part.part:
|
||||||
|
@ -342,7 +342,6 @@ class SupplierPartSerializer(InvenTreeTagModelSerializer):
|
|||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
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
|
# Check if 'available' quantity was supplied
|
||||||
self.has_available_quantity = 'available' in kwargs.get('data', {})
|
self.has_available_quantity = 'available' in kwargs.get('data', {})
|
||||||
|
|
||||||
@ -402,7 +401,6 @@ class SupplierPartSerializer(InvenTreeTagModelSerializer):
|
|||||||
Fields:
|
Fields:
|
||||||
in_stock: Current stock quantity for each SupplierPart
|
in_stock: Current stock quantity for each SupplierPart
|
||||||
"""
|
"""
|
||||||
|
|
||||||
queryset = queryset.annotate(
|
queryset = queryset.annotate(
|
||||||
in_stock=part.filters.annotate_total_stock()
|
in_stock=part.filters.annotate_total_stock()
|
||||||
)
|
)
|
||||||
@ -411,7 +409,6 @@ class SupplierPartSerializer(InvenTreeTagModelSerializer):
|
|||||||
|
|
||||||
def update(self, supplier_part, data):
|
def update(self, supplier_part, data):
|
||||||
"""Custom update functionality for the serializer"""
|
"""Custom update functionality for the serializer"""
|
||||||
|
|
||||||
available = data.pop('available', None)
|
available = data.pop('available', None)
|
||||||
|
|
||||||
response = super().update(supplier_part, data)
|
response = super().update(supplier_part, data)
|
||||||
@ -423,7 +420,6 @@ class SupplierPartSerializer(InvenTreeTagModelSerializer):
|
|||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
"""Extract manufacturer data and process ManufacturerPart."""
|
"""Extract manufacturer data and process ManufacturerPart."""
|
||||||
|
|
||||||
# Extract 'available' quantity from the serializer
|
# Extract 'available' quantity from the serializer
|
||||||
available = validated_data.pop('available', None)
|
available = validated_data.pop('available', None)
|
||||||
|
|
||||||
@ -468,7 +464,6 @@ class SupplierPriceBreakSerializer(InvenTreeModelSerializer):
|
|||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
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)
|
supplier_detail = kwargs.pop('supplier_detail', False)
|
||||||
part_detail = kwargs.pop('part_detail', False)
|
part_detail = kwargs.pop('part_detail', False)
|
||||||
|
|
||||||
|
@ -20,7 +20,6 @@ class CompanyTest(InvenTreeAPITestCase):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
"""Perform initialization for the unit test class"""
|
"""Perform initialization for the unit test class"""
|
||||||
|
|
||||||
super().setUpTestData()
|
super().setUpTestData()
|
||||||
|
|
||||||
# Create some company objects to work with
|
# Create some company objects to work with
|
||||||
@ -148,7 +147,6 @@ class ContactTest(InvenTreeAPITestCase):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
"""Perform init for this test class"""
|
"""Perform init for this test class"""
|
||||||
|
|
||||||
super().setUpTestData()
|
super().setUpTestData()
|
||||||
|
|
||||||
# Create some companies
|
# Create some companies
|
||||||
@ -178,7 +176,6 @@ class ContactTest(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
def test_list(self):
|
def test_list(self):
|
||||||
"""Test company list API endpoint"""
|
"""Test company list API endpoint"""
|
||||||
|
|
||||||
# List all results
|
# List all results
|
||||||
response = self.get(self.url, {}, expected_code=200)
|
response = self.get(self.url, {}, expected_code=200)
|
||||||
|
|
||||||
@ -202,7 +199,6 @@ class ContactTest(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
def test_create(self):
|
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()
|
n = Contact.objects.count()
|
||||||
|
|
||||||
company = Company.objects.first()
|
company = Company.objects.first()
|
||||||
@ -232,7 +228,6 @@ class ContactTest(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
def test_edit(self):
|
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
|
# Get the first contact
|
||||||
contact = Contact.objects.first()
|
contact = Contact.objects.first()
|
||||||
# Use this contact in the tests
|
# Use this contact in the tests
|
||||||
@ -268,7 +263,6 @@ class ContactTest(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
def test_delete(self):
|
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
|
# Get the last contact
|
||||||
contact = Contact.objects.first()
|
contact = Contact.objects.first()
|
||||||
url = reverse('api-contact-detail', kwargs={'pk': contact.pk})
|
url = reverse('api-contact-detail', kwargs={'pk': contact.pk})
|
||||||
@ -292,7 +286,6 @@ class AddressTest(InvenTreeAPITestCase):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
"""Perform initialization for this test class"""
|
"""Perform initialization for this test class"""
|
||||||
|
|
||||||
super().setUpTestData()
|
super().setUpTestData()
|
||||||
cls.num_companies = 3
|
cls.num_companies = 3
|
||||||
cls.num_addr = 3
|
cls.num_addr = 3
|
||||||
@ -323,14 +316,12 @@ class AddressTest(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
def test_list(self):
|
def test_list(self):
|
||||||
"""Test listing all addresses without filtering"""
|
"""Test listing all addresses without filtering"""
|
||||||
|
|
||||||
response = self.get(self.url, expected_code=200)
|
response = self.get(self.url, expected_code=200)
|
||||||
|
|
||||||
self.assertEqual(len(response.data), self.num_companies * self.num_addr)
|
self.assertEqual(len(response.data), self.num_companies * self.num_addr)
|
||||||
|
|
||||||
def test_filter_list(self):
|
def test_filter_list(self):
|
||||||
"""Test listing addresses filtered on company"""
|
"""Test listing addresses filtered on company"""
|
||||||
|
|
||||||
company = Company.objects.first()
|
company = Company.objects.first()
|
||||||
|
|
||||||
response = self.get(self.url, {'company': company.pk}, expected_code=200)
|
response = self.get(self.url, {'company': company.pk}, expected_code=200)
|
||||||
@ -339,7 +330,6 @@ class AddressTest(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
def test_create(self):
|
def test_create(self):
|
||||||
"""Test creating a new address"""
|
"""Test creating a new address"""
|
||||||
|
|
||||||
company = Company.objects.first()
|
company = Company.objects.first()
|
||||||
|
|
||||||
self.post(self.url,
|
self.post(self.url,
|
||||||
@ -360,7 +350,6 @@ class AddressTest(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
def test_get(self):
|
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()
|
addr = Address.objects.first()
|
||||||
|
|
||||||
url = reverse('api-address-detail', kwargs={'pk': addr.pk})
|
url = reverse('api-address-detail', kwargs={'pk': addr.pk})
|
||||||
@ -373,7 +362,6 @@ class AddressTest(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
def test_edit(self):
|
def test_edit(self):
|
||||||
"""Test editing an object"""
|
"""Test editing an object"""
|
||||||
|
|
||||||
addr = Address.objects.first()
|
addr = Address.objects.first()
|
||||||
|
|
||||||
url = reverse('api-address-detail', kwargs={'pk': addr.pk})
|
url = reverse('api-address-detail', kwargs={'pk': addr.pk})
|
||||||
@ -402,7 +390,6 @@ class AddressTest(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
def test_delete(self):
|
def test_delete(self):
|
||||||
"""Test deleting an object"""
|
"""Test deleting an object"""
|
||||||
|
|
||||||
addr = Address.objects.first()
|
addr = Address.objects.first()
|
||||||
|
|
||||||
url = reverse('api-address-detail', kwargs={'pk': addr.pk})
|
url = reverse('api-address-detail', kwargs={'pk': addr.pk})
|
||||||
@ -567,7 +554,6 @@ class SupplierPartTest(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
def test_available(self):
|
def test_available(self):
|
||||||
"""Tests for updating the 'available' field"""
|
"""Tests for updating the 'available' field"""
|
||||||
|
|
||||||
url = reverse('api-supplier-part-list')
|
url = reverse('api-supplier-part-list')
|
||||||
|
|
||||||
# Should fail when sending an invalid 'available' field
|
# Should fail when sending an invalid 'available' field
|
||||||
@ -651,7 +637,6 @@ class CompanyMetadataAPITest(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
def metatester(self, apikey, model):
|
def metatester(self, apikey, model):
|
||||||
"""Generic tester"""
|
"""Generic tester"""
|
||||||
|
|
||||||
modeldata = model.objects.first()
|
modeldata = model.objects.first()
|
||||||
|
|
||||||
# Useless test unless a model object is found
|
# Useless test unless a model object is found
|
||||||
@ -680,7 +665,6 @@ class CompanyMetadataAPITest(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
def test_metadata(self):
|
def test_metadata(self):
|
||||||
"""Test all endpoints"""
|
"""Test all endpoints"""
|
||||||
|
|
||||||
for apikey, model in {
|
for apikey, model in {
|
||||||
'api-manufacturer-part-metadata': ManufacturerPart,
|
'api-manufacturer-part-metadata': ManufacturerPart,
|
||||||
'api-supplier-part-metadata': SupplierPart,
|
'api-supplier-part-metadata': SupplierPart,
|
||||||
|
@ -293,7 +293,6 @@ class TestAddressMigration(MigratorTestCase):
|
|||||||
|
|
||||||
def prepare(self):
|
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 = self.old_state.apps.get_model('company', 'company')
|
||||||
|
|
||||||
Company.objects.create(name='Company 1', address=self.short_l1)
|
Company.objects.create(name='Company 1', address=self.short_l1)
|
||||||
@ -301,7 +300,6 @@ class TestAddressMigration(MigratorTestCase):
|
|||||||
|
|
||||||
def test_address_migration(self):
|
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')
|
Address = self.new_state.apps.get_model('company', 'address')
|
||||||
Company = self.new_state.apps.get_model('company', 'company')
|
Company = self.new_state.apps.get_model('company', 'company')
|
||||||
|
|
||||||
@ -329,7 +327,6 @@ class TestSupplierPartQuantity(MigratorTestCase):
|
|||||||
|
|
||||||
def prepare(self):
|
def prepare(self):
|
||||||
"""Prepare a number of SupplierPart objects"""
|
"""Prepare a number of SupplierPart objects"""
|
||||||
|
|
||||||
Part = self.old_state.apps.get_model('part', 'part')
|
Part = self.old_state.apps.get_model('part', 'part')
|
||||||
Company = self.old_state.apps.get_model('company', 'company')
|
Company = self.old_state.apps.get_model('company', 'company')
|
||||||
SupplierPart = self.old_state.apps.get_model('company', 'supplierpart')
|
SupplierPart = self.old_state.apps.get_model('company', 'supplierpart')
|
||||||
@ -356,7 +353,6 @@ class TestSupplierPartQuantity(MigratorTestCase):
|
|||||||
|
|
||||||
def test_supplier_part_quantity(self):
|
def test_supplier_part_quantity(self):
|
||||||
"""Test that the supplier part quantity is correctly migrated."""
|
"""Test that the supplier part quantity is correctly migrated."""
|
||||||
|
|
||||||
SupplierPart = self.new_state.apps.get_model('company', 'supplierpart')
|
SupplierPart = self.new_state.apps.get_model('company', 'supplierpart')
|
||||||
|
|
||||||
for i, sp in enumerate(SupplierPart.objects.all()):
|
for i, sp in enumerate(SupplierPart.objects.all()):
|
||||||
|
@ -14,7 +14,6 @@ class SupplierPartPackUnitsTests(InvenTreeTestCase):
|
|||||||
|
|
||||||
def test_pack_quantity_dimensionless(self):
|
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)
|
# Create a part without units (dimensionless)
|
||||||
part = Part.objects.create(name='Test Part', description='Test part description', component=True)
|
part = Part.objects.create(name='Test Part', description='Test part description', component=True)
|
||||||
|
|
||||||
@ -59,7 +58,6 @@ class SupplierPartPackUnitsTests(InvenTreeTestCase):
|
|||||||
|
|
||||||
def test_pack_quantity(self):
|
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'
|
# Create a part with units 'm'
|
||||||
part = Part.objects.create(name='Test Part', description='Test part description', component=True, units='m')
|
part = Part.objects.create(name='Test Part', description='Test part description', component=True, units='m')
|
||||||
|
|
||||||
|
@ -29,7 +29,6 @@ class CompanySimpleTest(TestCase):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
"""Perform initialization for the tests in this class"""
|
"""Perform initialization for the tests in this class"""
|
||||||
|
|
||||||
super().setUpTestData()
|
super().setUpTestData()
|
||||||
|
|
||||||
Company.objects.create(name='ABC Co.',
|
Company.objects.create(name='ABC Co.',
|
||||||
@ -194,7 +193,6 @@ class AddressTest(TestCase):
|
|||||||
|
|
||||||
def test_primary_constraint(self):
|
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=True)
|
||||||
Address.objects.create(company=self.c, primary=False)
|
Address.objects.create(company=self.c, primary=False)
|
||||||
|
|
||||||
@ -211,7 +209,6 @@ class AddressTest(TestCase):
|
|||||||
|
|
||||||
def test_first_address_is_primary(self):
|
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)
|
addr = Address.objects.create(company=self.c)
|
||||||
self.assertTrue(addr.primary)
|
self.assertTrue(addr.primary)
|
||||||
|
|
||||||
@ -255,7 +252,6 @@ class ManufacturerPartSimpleTest(TestCase):
|
|||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
"""Initialization for the unit tests in this class"""
|
"""Initialization for the unit tests in this class"""
|
||||||
|
|
||||||
# Create a manufacturer part
|
# Create a manufacturer part
|
||||||
self.part = Part.objects.get(pk=1)
|
self.part = Part.objects.get(pk=1)
|
||||||
manufacturer = Company.objects.get(pk=1)
|
manufacturer = Company.objects.get(pk=1)
|
||||||
|
@ -21,7 +21,6 @@ class CompanyIndex(InvenTreeRoleMixin, ListView):
|
|||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
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)
|
ctx = super().get_context_data(**kwargs)
|
||||||
|
|
||||||
# Provide custom context data to the template,
|
# Provide custom context data to the template,
|
||||||
|
@ -27,7 +27,6 @@ class StatusView(APIView):
|
|||||||
|
|
||||||
def get_status_model(self, *args, **kwargs):
|
def get_status_model(self, *args, **kwargs):
|
||||||
"""Return the StatusCode moedl based on extra parameters passed to the view"""
|
"""Return the StatusCode moedl based on extra parameters passed to the view"""
|
||||||
|
|
||||||
status_model = self.kwargs.get(self.MODEL_REF, None)
|
status_model = self.kwargs.get(self.MODEL_REF, None)
|
||||||
|
|
||||||
if status_model is None:
|
if status_model is None:
|
||||||
@ -37,7 +36,6 @@ class StatusView(APIView):
|
|||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
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()
|
status_class = self.get_status_model()
|
||||||
|
|
||||||
if not inspect.isclass(status_class):
|
if not inspect.isclass(status_class):
|
||||||
|
@ -163,7 +163,6 @@ class StatusCode(BaseEnum):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def template_context(cls):
|
def template_context(cls):
|
||||||
"""Return a dict representation containing all required information for templates."""
|
"""Return a dict representation containing all required information for templates."""
|
||||||
|
|
||||||
ret = {x.name: x.value for x in cls.values()}
|
ret = {x.name: x.value for x in cls.values()}
|
||||||
ret['list'] = cls.list()
|
ret['list'] = cls.list()
|
||||||
|
|
||||||
|
@ -41,7 +41,6 @@ class LabelFilterMixin:
|
|||||||
|
|
||||||
def get_items(self):
|
def get_items(self):
|
||||||
"""Return a list of database objects from query parameter"""
|
"""Return a list of database objects from query parameter"""
|
||||||
|
|
||||||
ids = []
|
ids = []
|
||||||
|
|
||||||
# Construct a list of possible query parameter value options
|
# Construct a list of possible query parameter value options
|
||||||
@ -73,7 +72,6 @@ class LabelListView(LabelFilterMixin, ListAPI):
|
|||||||
As each 'label' instance may optionally define its own filters,
|
As each 'label' instance may optionally define its own filters,
|
||||||
the resulting queryset is the 'union' of the two.
|
the resulting queryset is the 'union' of the two.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
queryset = super().filter_queryset(queryset)
|
queryset = super().filter_queryset(queryset)
|
||||||
|
|
||||||
items = self.get_items()
|
items = self.get_items()
|
||||||
|
@ -399,7 +399,6 @@ class BuildLineLabel(LabelTemplate):
|
|||||||
|
|
||||||
def get_context_data(self, request):
|
def get_context_data(self, request):
|
||||||
"""Generate context data for each provided BuildLine object."""
|
"""Generate context data for each provided BuildLine object."""
|
||||||
|
|
||||||
build_line = self.object_to_print
|
build_line = self.object_to_print
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -13,7 +13,6 @@ class LabelSerializerBase(InvenTreeModelSerializer):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def label_fields():
|
def label_fields():
|
||||||
"""Generic serializer fields for a label template"""
|
"""Generic serializer fields for a label template"""
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'pk',
|
'pk',
|
||||||
'name',
|
'name',
|
||||||
|
@ -11,6 +11,5 @@ from label.models import LabelOutput
|
|||||||
@scheduled_task(ScheduledTask.DAILY)
|
@scheduled_task(ScheduledTask.DAILY)
|
||||||
def cleanup_old_label_outputs():
|
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
|
# Remove any label outputs which are older than 30 days
|
||||||
LabelOutput.objects.filter(created__lte=timezone.now() - timedelta(days=5)).delete()
|
LabelOutput.objects.filter(created__lte=timezone.now() - timedelta(days=5)).delete()
|
||||||
|
@ -94,7 +94,6 @@ class LabelTest(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
def test_print_part_label(self):
|
def test_print_part_label(self):
|
||||||
"""Actually 'print' a label, and ensure that the correct information is contained."""
|
"""Actually 'print' a label, and ensure that the correct information is contained."""
|
||||||
|
|
||||||
label_data = """
|
label_data = """
|
||||||
{% load barcode %}
|
{% load barcode %}
|
||||||
{% load report %}
|
{% load report %}
|
||||||
|
@ -173,7 +173,6 @@ class PurchaseOrderLineItemResource(PriceResourceMixin, InvenTreeResource):
|
|||||||
|
|
||||||
def dehydrate_purchase_price(self, line):
|
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:
|
if line.purchase_price:
|
||||||
return line.purchase_price.amount
|
return line.purchase_price.amount
|
||||||
else:
|
else:
|
||||||
|
@ -92,7 +92,6 @@ class OrderFilter(rest_filters.FilterSet):
|
|||||||
|
|
||||||
def filter_status(self, queryset, name, value):
|
def filter_status(self, queryset, name, value):
|
||||||
"""Filter by integer status code"""
|
"""Filter by integer status code"""
|
||||||
|
|
||||||
return queryset.filter(status=value)
|
return queryset.filter(status=value)
|
||||||
|
|
||||||
# Exact match for reference
|
# Exact match for reference
|
||||||
@ -106,7 +105,6 @@ class OrderFilter(rest_filters.FilterSet):
|
|||||||
|
|
||||||
def filter_assigned_to_me(self, queryset, name, value):
|
def filter_assigned_to_me(self, queryset, name, value):
|
||||||
"""Filter by orders which are assigned to the current user."""
|
"""Filter by orders which are assigned to the current user."""
|
||||||
|
|
||||||
# Work out who "me" is!
|
# Work out who "me" is!
|
||||||
owners = Owner.get_owners_matching_user(self.request.user)
|
owners = Owner.get_owners_matching_user(self.request.user)
|
||||||
|
|
||||||
@ -122,7 +120,6 @@ class OrderFilter(rest_filters.FilterSet):
|
|||||||
|
|
||||||
Note that the overdue_filter() classmethod must be defined for the model
|
Note that the overdue_filter() classmethod must be defined for the model
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if str2bool(value):
|
if str2bool(value):
|
||||||
return queryset.filter(self.Meta.model.overdue_filter())
|
return queryset.filter(self.Meta.model.overdue_filter())
|
||||||
else:
|
else:
|
||||||
@ -132,7 +129,6 @@ class OrderFilter(rest_filters.FilterSet):
|
|||||||
|
|
||||||
def filter_outstanding(self, queryset, name, value):
|
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):
|
if str2bool(value):
|
||||||
return queryset.filter(status__in=self.Meta.model.get_status_class().OPEN)
|
return queryset.filter(status__in=self.Meta.model.get_status_class().OPEN)
|
||||||
else:
|
else:
|
||||||
@ -147,7 +143,6 @@ class OrderFilter(rest_filters.FilterSet):
|
|||||||
|
|
||||||
def filter_has_project_code(self, queryset, name, value):
|
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):
|
if str2bool(value):
|
||||||
return queryset.exclude(project_code=None)
|
return queryset.exclude(project_code=None)
|
||||||
else:
|
else:
|
||||||
@ -227,7 +222,6 @@ class PurchaseOrderList(PurchaseOrderMixin, APIDownloadMixin, ListCreateAPI):
|
|||||||
|
|
||||||
def create(self, request, *args, **kwargs):
|
def create(self, request, *args, **kwargs):
|
||||||
"""Save user information on create."""
|
"""Save user information on create."""
|
||||||
|
|
||||||
data = self.clean_data(request.data)
|
data = self.clean_data(request.data)
|
||||||
|
|
||||||
duplicate_order = data.pop('duplicate_order', None)
|
duplicate_order = data.pop('duplicate_order', None)
|
||||||
@ -275,7 +269,6 @@ class PurchaseOrderList(PurchaseOrderMixin, APIDownloadMixin, ListCreateAPI):
|
|||||||
|
|
||||||
def download_queryset(self, queryset, export_format):
|
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)
|
dataset = PurchaseOrderResource().export(queryset=queryset)
|
||||||
|
|
||||||
filedata = dataset.export(export_format)
|
filedata = dataset.export(export_format)
|
||||||
@ -432,7 +425,6 @@ class PurchaseOrderLineItemFilter(LineItemFilter):
|
|||||||
|
|
||||||
def filter_pending(self, queryset, name, value):
|
def filter_pending(self, queryset, name, value):
|
||||||
"""Filter by "pending" status (order status = pending)"""
|
"""Filter by "pending" status (order status = pending)"""
|
||||||
|
|
||||||
if str2bool(value):
|
if str2bool(value):
|
||||||
return queryset.filter(order__status__in=PurchaseOrderStatusGroups.OPEN)
|
return queryset.filter(order__status__in=PurchaseOrderStatusGroups.OPEN)
|
||||||
else:
|
else:
|
||||||
@ -511,7 +503,6 @@ class PurchaseOrderLineItemList(PurchaseOrderLineItemMixin, APIDownloadMixin, Li
|
|||||||
|
|
||||||
def download_queryset(self, queryset, export_format):
|
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)
|
dataset = PurchaseOrderLineItemResource().export(queryset=queryset)
|
||||||
|
|
||||||
filedata = dataset.export(export_format)
|
filedata = dataset.export(export_format)
|
||||||
@ -562,7 +553,6 @@ class PurchaseOrderExtraLineList(GeneralExtraLineList, ListCreateAPI):
|
|||||||
|
|
||||||
def download_queryset(self, queryset, export_format):
|
def download_queryset(self, queryset, export_format):
|
||||||
"""Download this queryset as a file"""
|
"""Download this queryset as a file"""
|
||||||
|
|
||||||
dataset = PurchaseOrderExtraLineResource().export(queryset=queryset)
|
dataset = PurchaseOrderExtraLineResource().export(queryset=queryset)
|
||||||
filedata = dataset.export(export_format)
|
filedata = dataset.export(export_format)
|
||||||
filename = f"InvenTree_ExtraPurchaseOrderLines.{export_format}"
|
filename = f"InvenTree_ExtraPurchaseOrderLines.{export_format}"
|
||||||
@ -662,7 +652,6 @@ class SalesOrderList(SalesOrderMixin, APIDownloadMixin, ListCreateAPI):
|
|||||||
|
|
||||||
def download_queryset(self, queryset, export_format):
|
def download_queryset(self, queryset, export_format):
|
||||||
"""Download this queryset as a file"""
|
"""Download this queryset as a file"""
|
||||||
|
|
||||||
dataset = SalesOrderResource().export(queryset=queryset)
|
dataset = SalesOrderResource().export(queryset=queryset)
|
||||||
|
|
||||||
filedata = dataset.export(export_format)
|
filedata = dataset.export(export_format)
|
||||||
@ -812,7 +801,6 @@ class SalesOrderLineItemList(SalesOrderLineItemMixin, APIDownloadMixin, ListCrea
|
|||||||
|
|
||||||
def download_queryset(self, queryset, export_format):
|
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)
|
dataset = SalesOrderLineItemResource().export(queryset=queryset)
|
||||||
filedata = dataset.export(export_format)
|
filedata = dataset.export(export_format)
|
||||||
|
|
||||||
@ -849,7 +837,6 @@ class SalesOrderExtraLineList(GeneralExtraLineList, ListCreateAPI):
|
|||||||
|
|
||||||
def download_queryset(self, queryset, export_format):
|
def download_queryset(self, queryset, export_format):
|
||||||
"""Download this queryset as a file"""
|
"""Download this queryset as a file"""
|
||||||
|
|
||||||
dataset = SalesOrderExtraLineResource().export(queryset=queryset)
|
dataset = SalesOrderExtraLineResource().export(queryset=queryset)
|
||||||
filedata = dataset.export(export_format)
|
filedata = dataset.export(export_format)
|
||||||
filename = f"InvenTree_ExtraSalesOrderLines.{export_format}"
|
filename = f"InvenTree_ExtraSalesOrderLines.{export_format}"
|
||||||
@ -1151,7 +1138,6 @@ class ReturnOrderList(ReturnOrderMixin, APIDownloadMixin, ListCreateAPI):
|
|||||||
|
|
||||||
def download_queryset(self, queryset, export_format):
|
def download_queryset(self, queryset, export_format):
|
||||||
"""Download this queryset as a file"""
|
"""Download this queryset as a file"""
|
||||||
|
|
||||||
dataset = ReturnOrderResource().export(queryset=queryset)
|
dataset = ReturnOrderResource().export(queryset=queryset)
|
||||||
filedata = dataset.export(export_format)
|
filedata = dataset.export(export_format)
|
||||||
filename = f"InvenTree_ReturnOrders.{export_format}"
|
filename = f"InvenTree_ReturnOrders.{export_format}"
|
||||||
@ -1252,7 +1238,6 @@ class ReturnOrderLineItemFilter(LineItemFilter):
|
|||||||
|
|
||||||
def filter_received(self, queryset, name, value):
|
def filter_received(self, queryset, name, value):
|
||||||
"""Filter by 'received' field"""
|
"""Filter by 'received' field"""
|
||||||
|
|
||||||
if str2bool(value):
|
if str2bool(value):
|
||||||
return queryset.exclude(received_date=None)
|
return queryset.exclude(received_date=None)
|
||||||
else:
|
else:
|
||||||
@ -1267,7 +1252,6 @@ class ReturnOrderLineItemMixin:
|
|||||||
|
|
||||||
def get_serializer(self, *args, **kwargs):
|
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:
|
try:
|
||||||
params = self.request.query_params
|
params = self.request.query_params
|
||||||
|
|
||||||
@ -1283,7 +1267,6 @@ class ReturnOrderLineItemMixin:
|
|||||||
|
|
||||||
def get_queryset(self, *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 = super().get_queryset(*args, **kwargs)
|
||||||
|
|
||||||
queryset = queryset.prefetch_related(
|
queryset = queryset.prefetch_related(
|
||||||
@ -1302,7 +1285,6 @@ class ReturnOrderLineItemList(ReturnOrderLineItemMixin, APIDownloadMixin, ListCr
|
|||||||
|
|
||||||
def download_queryset(self, queryset, export_format):
|
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")
|
raise NotImplementedError("download_queryset not yet implemented for this endpoint")
|
||||||
|
|
||||||
filter_backends = SEARCH_ORDER_FILTER
|
filter_backends = SEARCH_ORDER_FILTER
|
||||||
@ -1334,7 +1316,6 @@ class ReturnOrderExtraLineList(GeneralExtraLineList, ListCreateAPI):
|
|||||||
|
|
||||||
def download_queryset(self, queryset, export_format):
|
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")
|
raise NotImplementedError("download_queryset not yet implemented")
|
||||||
|
|
||||||
|
|
||||||
@ -1389,7 +1370,6 @@ class OrderCalendarExport(ICalFeed):
|
|||||||
https://stackoverflow.com/questions/152248/can-i-use-http-basic-authentication-with-django
|
https://stackoverflow.com/questions/152248/can-i-use-http-basic-authentication-with-django
|
||||||
https://www.djangosnippets.org/snippets/243/
|
https://www.djangosnippets.org/snippets/243/
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import base64
|
import base64
|
||||||
|
|
||||||
if request.user.is_authenticated:
|
if request.user.is_authenticated:
|
||||||
@ -1435,7 +1415,6 @@ class OrderCalendarExport(ICalFeed):
|
|||||||
|
|
||||||
def title(self, obj):
|
def title(self, obj):
|
||||||
"""Return calendar title."""
|
"""Return calendar title."""
|
||||||
|
|
||||||
if obj["ordertype"] == 'purchase-order':
|
if obj["ordertype"] == 'purchase-order':
|
||||||
ordertype_title = _('Purchase Order')
|
ordertype_title = _('Purchase Order')
|
||||||
elif obj["ordertype"] == 'sales-order':
|
elif obj["ordertype"] == 'sales-order':
|
||||||
@ -1514,7 +1493,6 @@ class OrderCalendarExport(ICalFeed):
|
|||||||
|
|
||||||
def item_link(self, item):
|
def item_link(self, item):
|
||||||
"""Set the item link."""
|
"""Set the item link."""
|
||||||
|
|
||||||
return construct_absolute_url(item.get_absolute_url())
|
return construct_absolute_url(item.get_absolute_url())
|
||||||
|
|
||||||
|
|
||||||
|
@ -62,7 +62,6 @@ class TotalPriceMixin(models.Model):
|
|||||||
|
|
||||||
def save(self, *args, **kwargs):
|
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
|
# Recalculate total_price for this order
|
||||||
self.update_total_price(commit=False)
|
self.update_total_price(commit=False)
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
@ -90,7 +89,6 @@ class TotalPriceMixin(models.Model):
|
|||||||
- Otherwise, return the currency associated with the company
|
- Otherwise, return the currency associated with the company
|
||||||
- Finally, return the default currency code
|
- Finally, return the default currency code
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if self.order_currency:
|
if self.order_currency:
|
||||||
return self.order_currency
|
return self.order_currency
|
||||||
|
|
||||||
@ -102,7 +100,6 @@ class TotalPriceMixin(models.Model):
|
|||||||
|
|
||||||
def update_total_price(self, commit=True):
|
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)
|
self.total_price = self.calculate_total_price(target_currency=self.currency)
|
||||||
|
|
||||||
if commit:
|
if commit:
|
||||||
@ -200,7 +197,6 @@ class Order(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, Reference
|
|||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
"""Custom clean method for the generic order class"""
|
"""Custom clean method for the generic order class"""
|
||||||
|
|
||||||
super().clean()
|
super().clean()
|
||||||
|
|
||||||
# Check that the referenced 'contact' matches the correct 'company'
|
# Check that the referenced 'contact' matches the correct 'company'
|
||||||
@ -216,7 +212,6 @@ class Order(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, Reference
|
|||||||
|
|
||||||
It requires any subclasses to implement the get_status_class() class method
|
It requires any subclasses to implement the get_status_class() class method
|
||||||
"""
|
"""
|
||||||
|
|
||||||
today = datetime.now().date()
|
today = datetime.now().date()
|
||||||
return Q(status__in=cls.get_status_class().OPEN) & ~Q(target_date=None) & Q(target_date__lt=today)
|
return Q(status__in=cls.get_status_class().OPEN) & ~Q(target_date=None) & Q(target_date__lt=today)
|
||||||
|
|
||||||
@ -226,7 +221,6 @@ class Order(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, Reference
|
|||||||
|
|
||||||
Makes use of the overdue_filter() method to avoid code duplication
|
Makes use of the overdue_filter() method to avoid code duplication
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return self.__class__.objects.filter(pk=self.pk).filter(self.__class__.overdue_filter()).exists()
|
return self.__class__.objects.filter(pk=self.pk).filter(self.__class__.overdue_filter()).exists()
|
||||||
|
|
||||||
description = models.CharField(max_length=250, blank=True, verbose_name=_('Description'), help_text=_('Order description (optional)'))
|
description = models.CharField(max_length=250, blank=True, verbose_name=_('Description'), help_text=_('Order description (optional)'))
|
||||||
@ -284,7 +278,6 @@ class Order(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, Reference
|
|||||||
@classmethod
|
@classmethod
|
||||||
def get_status_class(cls):
|
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__}")
|
raise NotImplementedError(f"get_status_class() not implemented for {__class__}")
|
||||||
|
|
||||||
|
|
||||||
@ -315,7 +308,6 @@ class PurchaseOrder(TotalPriceMixin, Order):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def api_defaults(cls, request):
|
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 = {
|
defaults = {
|
||||||
'reference': order.validators.generate_next_purchase_order_reference(),
|
'reference': order.validators.generate_next_purchase_order_reference(),
|
||||||
}
|
}
|
||||||
@ -362,7 +354,6 @@ class PurchaseOrder(TotalPriceMixin, Order):
|
|||||||
|
|
||||||
def __str__(self):
|
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')}"
|
return f"{self.reference} - {self.supplier.name if self.supplier else _('deleted')}"
|
||||||
|
|
||||||
reference = models.CharField(
|
reference = models.CharField(
|
||||||
@ -768,7 +759,6 @@ class SalesOrder(TotalPriceMixin, Order):
|
|||||||
|
|
||||||
def __str__(self):
|
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')}"
|
return f"{self.reference} - {self.customer.name if self.customer else _('deleted')}"
|
||||||
|
|
||||||
reference = models.CharField(
|
reference = models.CharField(
|
||||||
@ -895,7 +885,6 @@ class SalesOrder(TotalPriceMixin, Order):
|
|||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def issue_order(self):
|
def issue_order(self):
|
||||||
"""Change this order from 'PENDING' to 'IN_PROGRESS'"""
|
"""Change this order from 'PENDING' to 'IN_PROGRESS'"""
|
||||||
|
|
||||||
if self.status == SalesOrderStatus.PENDING:
|
if self.status == SalesOrderStatus.PENDING:
|
||||||
self.status = SalesOrderStatus.IN_PROGRESS.value
|
self.status = SalesOrderStatus.IN_PROGRESS.value
|
||||||
self.issue_date = datetime.now().date()
|
self.issue_date = datetime.now().date()
|
||||||
@ -1069,7 +1058,6 @@ class OrderLineItem(MetadataMixin, models.Model):
|
|||||||
|
|
||||||
Calls save method on the linked order
|
Calls save method on the linked order
|
||||||
"""
|
"""
|
||||||
|
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
self.order.save()
|
self.order.save()
|
||||||
|
|
||||||
@ -1078,7 +1066,6 @@ class OrderLineItem(MetadataMixin, models.Model):
|
|||||||
|
|
||||||
Calls save method on the linked order
|
Calls save method on the linked order
|
||||||
"""
|
"""
|
||||||
|
|
||||||
super().delete(*args, **kwargs)
|
super().delete(*args, **kwargs)
|
||||||
self.order.save()
|
self.order.save()
|
||||||
|
|
||||||
@ -1093,7 +1080,6 @@ class OrderLineItem(MetadataMixin, models.Model):
|
|||||||
@property
|
@property
|
||||||
def total_line_price(self):
|
def total_line_price(self):
|
||||||
"""Return the total price for this line item"""
|
"""Return the total price for this line item"""
|
||||||
|
|
||||||
if self.price:
|
if self.price:
|
||||||
return self.quantity * self.price
|
return self.quantity * self.price
|
||||||
|
|
||||||
@ -1295,7 +1281,6 @@ class SalesOrderLineItem(OrderLineItem):
|
|||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
"""Perform extra validation steps for this SalesOrderLineItem instance"""
|
"""Perform extra validation steps for this SalesOrderLineItem instance"""
|
||||||
|
|
||||||
super().clean()
|
super().clean()
|
||||||
|
|
||||||
if self.part:
|
if self.part:
|
||||||
@ -1729,7 +1714,6 @@ class ReturnOrder(TotalPriceMixin, Order):
|
|||||||
|
|
||||||
def __str__(self):
|
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')}"
|
return f"{self.reference} - {self.customer.name if self.customer else _('no customer')}"
|
||||||
|
|
||||||
reference = models.CharField(
|
reference = models.CharField(
|
||||||
@ -1810,7 +1794,6 @@ class ReturnOrder(TotalPriceMixin, Order):
|
|||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def complete_order(self):
|
def complete_order(self):
|
||||||
"""Complete this ReturnOrder (if not already completed)"""
|
"""Complete this ReturnOrder (if not already completed)"""
|
||||||
|
|
||||||
if self.status == ReturnOrderStatus.IN_PROGRESS:
|
if self.status == ReturnOrderStatus.IN_PROGRESS:
|
||||||
self.status = ReturnOrderStatus.COMPLETE.value
|
self.status = ReturnOrderStatus.COMPLETE.value
|
||||||
self.complete_date = datetime.now().date()
|
self.complete_date = datetime.now().date()
|
||||||
@ -1825,7 +1808,6 @@ class ReturnOrder(TotalPriceMixin, Order):
|
|||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def issue_order(self):
|
def issue_order(self):
|
||||||
"""Issue this ReturnOrder (if currently pending)"""
|
"""Issue this ReturnOrder (if currently pending)"""
|
||||||
|
|
||||||
if self.status == ReturnOrderStatus.PENDING:
|
if self.status == ReturnOrderStatus.PENDING:
|
||||||
self.status = ReturnOrderStatus.IN_PROGRESS.value
|
self.status = ReturnOrderStatus.IN_PROGRESS.value
|
||||||
self.issue_date = datetime.now().date()
|
self.issue_date = datetime.now().date()
|
||||||
@ -1842,7 +1824,6 @@ class ReturnOrder(TotalPriceMixin, Order):
|
|||||||
- Adds a tracking entry to the StockItem
|
- Adds a tracking entry to the StockItem
|
||||||
- Removes the 'customer' reference from the StockItem
|
- Removes the 'customer' reference from the StockItem
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Prevent an item from being "received" multiple times
|
# Prevent an item from being "received" multiple times
|
||||||
if line.received_date is not None:
|
if line.received_date is not None:
|
||||||
logger.warning("receive_line_item called with item already returned")
|
logger.warning("receive_line_item called with item already returned")
|
||||||
@ -1908,7 +1889,6 @@ class ReturnOrderLineItem(OrderLineItem):
|
|||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
"""Perform extra validation steps for the ReturnOrderLineItem model"""
|
"""Perform extra validation steps for the ReturnOrderLineItem model"""
|
||||||
|
|
||||||
super().clean()
|
super().clean()
|
||||||
|
|
||||||
if self.item and not self.item.serialized:
|
if self.item and not self.item.serialized:
|
||||||
@ -1977,7 +1957,6 @@ class ReturnOrderAttachment(InvenTreeAttachment):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def get_api_url():
|
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')
|
return reverse('api-return-order-attachment-list')
|
||||||
|
|
||||||
def getSubdir(self):
|
def getSubdir(self):
|
||||||
|
@ -86,14 +86,12 @@ class AbstractOrderSerializer(serializers.Serializer):
|
|||||||
|
|
||||||
def validate_reference(self, reference):
|
def validate_reference(self, reference):
|
||||||
"""Custom validation for the reference field"""
|
"""Custom validation for the reference field"""
|
||||||
|
|
||||||
self.Meta.model.validate_reference_field(reference)
|
self.Meta.model.validate_reference_field(reference)
|
||||||
return reference
|
return reference
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def annotate_queryset(queryset):
|
def annotate_queryset(queryset):
|
||||||
"""Add extra information to the queryset"""
|
"""Add extra information to the queryset"""
|
||||||
|
|
||||||
queryset = queryset.annotate(
|
queryset = queryset.annotate(
|
||||||
line_items=SubqueryCount('lines')
|
line_items=SubqueryCount('lines')
|
||||||
)
|
)
|
||||||
@ -103,7 +101,6 @@ class AbstractOrderSerializer(serializers.Serializer):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def order_fields(extra_fields):
|
def order_fields(extra_fields):
|
||||||
"""Construct a set of fields for this serializer"""
|
"""Construct a set of fields for this serializer"""
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'pk',
|
'pk',
|
||||||
'creation_date',
|
'creation_date',
|
||||||
@ -272,7 +269,6 @@ class PurchaseOrderCompleteSerializer(serializers.Serializer):
|
|||||||
|
|
||||||
def validate_accept_incomplete(self, value):
|
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']
|
order = self.context['order']
|
||||||
|
|
||||||
if not value and not order.is_complete:
|
if not value and not order.is_complete:
|
||||||
@ -910,7 +906,6 @@ class SalesOrderLineItemSerializer(InvenTreeModelSerializer):
|
|||||||
- "overdue" status (boolean field)
|
- "overdue" status (boolean field)
|
||||||
- "available_quantity"
|
- "available_quantity"
|
||||||
"""
|
"""
|
||||||
|
|
||||||
queryset = queryset.annotate(
|
queryset = queryset.annotate(
|
||||||
overdue=Case(
|
overdue=Case(
|
||||||
When(
|
When(
|
||||||
@ -1160,7 +1155,6 @@ class SalesOrderCompleteSerializer(serializers.Serializer):
|
|||||||
|
|
||||||
def validate_accept_incomplete(self, value):
|
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']
|
order = self.context['order']
|
||||||
|
|
||||||
if not value and not order.is_completed():
|
if not value and not order.is_completed():
|
||||||
@ -1170,7 +1164,6 @@ class SalesOrderCompleteSerializer(serializers.Serializer):
|
|||||||
|
|
||||||
def get_context_data(self):
|
def get_context_data(self):
|
||||||
"""Custom context data for this serializer"""
|
"""Custom context data for this serializer"""
|
||||||
|
|
||||||
order = self.context['order']
|
order = self.context['order']
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -1498,7 +1491,6 @@ class ReturnOrderSerializer(AbstractOrderSerializer, TotalPriceMixin, InvenTreeM
|
|||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
"""Initialization routine for the serializer"""
|
"""Initialization routine for the serializer"""
|
||||||
|
|
||||||
customer_detail = kwargs.pop('customer_detail', False)
|
customer_detail = kwargs.pop('customer_detail', False)
|
||||||
|
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
@ -1509,7 +1501,6 @@ class ReturnOrderSerializer(AbstractOrderSerializer, TotalPriceMixin, InvenTreeM
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def annotate_queryset(queryset):
|
def annotate_queryset(queryset):
|
||||||
"""Custom annotation for the serializer queryset"""
|
"""Custom annotation for the serializer queryset"""
|
||||||
|
|
||||||
queryset = AbstractOrderSerializer.annotate_queryset(queryset)
|
queryset = AbstractOrderSerializer.annotate_queryset(queryset)
|
||||||
|
|
||||||
queryset = queryset.annotate(
|
queryset = queryset.annotate(
|
||||||
@ -1585,7 +1576,6 @@ class ReturnOrderLineItemReceiveSerializer(serializers.Serializer):
|
|||||||
|
|
||||||
def validate_line_item(self, item):
|
def validate_line_item(self, item):
|
||||||
"""Validation for a single line item"""
|
"""Validation for a single line item"""
|
||||||
|
|
||||||
if item.order != self.context['order']:
|
if item.order != self.context['order']:
|
||||||
raise ValidationError(_("Line item does not match return order"))
|
raise ValidationError(_("Line item does not match return order"))
|
||||||
|
|
||||||
@ -1619,7 +1609,6 @@ class ReturnOrderReceiveSerializer(serializers.Serializer):
|
|||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
"""Perform data validation for this serializer"""
|
"""Perform data validation for this serializer"""
|
||||||
|
|
||||||
order = self.context['order']
|
order = self.context['order']
|
||||||
if order.status != ReturnOrderStatus.IN_PROGRESS:
|
if order.status != ReturnOrderStatus.IN_PROGRESS:
|
||||||
raise ValidationError(_("Items can only be received against orders which are in progress"))
|
raise ValidationError(_("Items can only be received against orders which are in progress"))
|
||||||
@ -1636,7 +1625,6 @@ class ReturnOrderReceiveSerializer(serializers.Serializer):
|
|||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def save(self):
|
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']
|
order = self.context['order']
|
||||||
request = self.context['request']
|
request = self.context['request']
|
||||||
|
|
||||||
@ -1682,7 +1670,6 @@ class ReturnOrderLineItemSerializer(InvenTreeModelSerializer):
|
|||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
"""Initialization routine for the serializer"""
|
"""Initialization routine for the serializer"""
|
||||||
|
|
||||||
order_detail = kwargs.pop('order_detail', False)
|
order_detail = kwargs.pop('order_detail', False)
|
||||||
item_detail = kwargs.pop('item_detail', False)
|
item_detail = kwargs.pop('item_detail', False)
|
||||||
part_detail = kwargs.pop('part_detail', False)
|
part_detail = kwargs.pop('part_detail', False)
|
||||||
|
@ -15,7 +15,6 @@ from plugin.events import trigger_event
|
|||||||
|
|
||||||
def notify_overdue_purchase_order(po: order.models.PurchaseOrder):
|
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 = []
|
targets = []
|
||||||
|
|
||||||
if po.created_by:
|
if po.created_by:
|
||||||
@ -64,7 +63,6 @@ def check_overdue_purchase_orders():
|
|||||||
- Look at the 'target_date' of any outstanding PurchaseOrder objects
|
- Look at the 'target_date' of any outstanding PurchaseOrder objects
|
||||||
- If the 'target_date' expired *yesterday* then the order is just out of date
|
- If the 'target_date' expired *yesterday* then the order is just out of date
|
||||||
"""
|
"""
|
||||||
|
|
||||||
yesterday = datetime.now().date() - timedelta(days=1)
|
yesterday = datetime.now().date() - timedelta(days=1)
|
||||||
|
|
||||||
overdue_orders = order.models.PurchaseOrder.objects.filter(
|
overdue_orders = order.models.PurchaseOrder.objects.filter(
|
||||||
@ -78,7 +76,6 @@ def check_overdue_purchase_orders():
|
|||||||
|
|
||||||
def notify_overdue_sales_order(so: order.models.SalesOrder):
|
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 = []
|
targets = []
|
||||||
|
|
||||||
if so.created_by:
|
if so.created_by:
|
||||||
@ -127,7 +124,6 @@ def check_overdue_sales_orders():
|
|||||||
- Look at the 'target_date' of any outstanding SalesOrder objects
|
- Look at the 'target_date' of any outstanding SalesOrder objects
|
||||||
- If the 'target_date' expired *yesterday* then the order is just out of date
|
- If the 'target_date' expired *yesterday* then the order is just out of date
|
||||||
"""
|
"""
|
||||||
|
|
||||||
yesterday = datetime.now().date() - timedelta(days=1)
|
yesterday = datetime.now().date() - timedelta(days=1)
|
||||||
|
|
||||||
overdue_orders = order.models.SalesOrder.objects.filter(
|
overdue_orders = order.models.SalesOrder.objects.filter(
|
||||||
|
@ -62,7 +62,6 @@ class PurchaseOrderTest(OrderTest):
|
|||||||
|
|
||||||
def test_options(self):
|
def test_options(self):
|
||||||
"""Test the PurchaseOrder OPTIONS endpoint."""
|
"""Test the PurchaseOrder OPTIONS endpoint."""
|
||||||
|
|
||||||
self.assignRole('purchase_order.add')
|
self.assignRole('purchase_order.add')
|
||||||
|
|
||||||
response = self.options(self.LIST_URL, expected_code=200)
|
response = self.options(self.LIST_URL, expected_code=200)
|
||||||
@ -144,7 +143,6 @@ class PurchaseOrderTest(OrderTest):
|
|||||||
|
|
||||||
def test_total_price(self):
|
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
|
# Ensure we have exchange rate data
|
||||||
self.generate_exchange_rates()
|
self.generate_exchange_rates()
|
||||||
|
|
||||||
@ -360,7 +358,6 @@ class PurchaseOrderTest(OrderTest):
|
|||||||
|
|
||||||
def test_po_duplicate(self):
|
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')
|
self.assignRole('purchase_order.add')
|
||||||
|
|
||||||
po = models.PurchaseOrder.objects.get(pk=1)
|
po = models.PurchaseOrder.objects.get(pk=1)
|
||||||
@ -511,7 +508,6 @@ class PurchaseOrderTest(OrderTest):
|
|||||||
|
|
||||||
def test_po_calendar(self):
|
def test_po_calendar(self):
|
||||||
"""Test the calendar export endpoint"""
|
"""Test the calendar export endpoint"""
|
||||||
|
|
||||||
# Create required purchase orders
|
# Create required purchase orders
|
||||||
self.assignRole('purchase_order.add')
|
self.assignRole('purchase_order.add')
|
||||||
|
|
||||||
@ -1120,7 +1116,6 @@ class SalesOrderTest(OrderTest):
|
|||||||
|
|
||||||
def test_total_price(self):
|
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
|
# Ensure we have exchange rate data
|
||||||
self.generate_exchange_rates()
|
self.generate_exchange_rates()
|
||||||
|
|
||||||
@ -1359,7 +1354,6 @@ class SalesOrderTest(OrderTest):
|
|||||||
|
|
||||||
def test_so_calendar(self):
|
def test_so_calendar(self):
|
||||||
"""Test the calendar export endpoint"""
|
"""Test the calendar export endpoint"""
|
||||||
|
|
||||||
# Create required sales orders
|
# Create required sales orders
|
||||||
self.assignRole('sales_order.add')
|
self.assignRole('sales_order.add')
|
||||||
|
|
||||||
@ -1420,7 +1414,6 @@ class SalesOrderTest(OrderTest):
|
|||||||
|
|
||||||
def test_export(self):
|
def test_export(self):
|
||||||
"""Test we can export the SalesOrder list"""
|
"""Test we can export the SalesOrder list"""
|
||||||
|
|
||||||
n = models.SalesOrder.objects.count()
|
n = models.SalesOrder.objects.count()
|
||||||
|
|
||||||
# Check there are some sales orders
|
# Check there are some sales orders
|
||||||
@ -1940,7 +1933,6 @@ class ReturnOrderTests(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
def test_options(self):
|
def test_options(self):
|
||||||
"""Test the OPTIONS endpoint"""
|
"""Test the OPTIONS endpoint"""
|
||||||
|
|
||||||
self.assignRole('return_order.add')
|
self.assignRole('return_order.add')
|
||||||
data = self.options(reverse('api-return-order-list'), expected_code=200).data
|
data = self.options(reverse('api-return-order-list'), expected_code=200).data
|
||||||
|
|
||||||
@ -1958,7 +1950,6 @@ class ReturnOrderTests(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
def test_list(self):
|
def test_list(self):
|
||||||
"""Tests for the list endpoint"""
|
"""Tests for the list endpoint"""
|
||||||
|
|
||||||
url = reverse('api-return-order-list')
|
url = reverse('api-return-order-list')
|
||||||
|
|
||||||
response = self.get(url, expected_code=200)
|
response = self.get(url, expected_code=200)
|
||||||
@ -2024,7 +2015,6 @@ class ReturnOrderTests(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
def test_create(self):
|
def test_create(self):
|
||||||
"""Test creation of ReturnOrder via the API"""
|
"""Test creation of ReturnOrder via the API"""
|
||||||
|
|
||||||
url = reverse('api-return-order-list')
|
url = reverse('api-return-order-list')
|
||||||
|
|
||||||
# Do not have required permissions yet
|
# Do not have required permissions yet
|
||||||
@ -2055,7 +2045,6 @@ class ReturnOrderTests(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
def test_update(self):
|
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})
|
url = reverse('api-return-order-detail', kwargs={'pk': 1})
|
||||||
|
|
||||||
# Test detail endpoint
|
# Test detail endpoint
|
||||||
@ -2087,7 +2076,6 @@ class ReturnOrderTests(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
def test_ro_issue(self):
|
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)
|
order = models.ReturnOrder.objects.get(pk=1)
|
||||||
self.assertEqual(order.status, ReturnOrderStatus.PENDING)
|
self.assertEqual(order.status, ReturnOrderStatus.PENDING)
|
||||||
self.assertIsNone(order.issue_date)
|
self.assertIsNone(order.issue_date)
|
||||||
@ -2106,7 +2094,6 @@ class ReturnOrderTests(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
def test_receive(self):
|
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)
|
customer = Company.objects.get(pk=4)
|
||||||
|
|
||||||
# Create an order
|
# Create an order
|
||||||
@ -2209,7 +2196,6 @@ class ReturnOrderTests(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
def test_ro_calendar(self):
|
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
|
# Full test is in test_po_calendar. Since these use the same backend, test only
|
||||||
# that the endpoint is available
|
# that the endpoint is available
|
||||||
url = reverse('api-po-so-calendar', kwargs={'ordertype': 'return-order'})
|
url = reverse('api-po-so-calendar', kwargs={'ordertype': 'return-order'})
|
||||||
@ -2243,7 +2229,6 @@ class OrderMetadataAPITest(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
def metatester(self, apikey, model):
|
def metatester(self, apikey, model):
|
||||||
"""Generic tester"""
|
"""Generic tester"""
|
||||||
|
|
||||||
modeldata = model.objects.first()
|
modeldata = model.objects.first()
|
||||||
|
|
||||||
# Useless test unless a model object is found
|
# Useless test unless a model object is found
|
||||||
@ -2272,7 +2257,6 @@ class OrderMetadataAPITest(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
def test_metadata(self):
|
def test_metadata(self):
|
||||||
"""Test all endpoints"""
|
"""Test all endpoints"""
|
||||||
|
|
||||||
for apikey, model in {
|
for apikey, model in {
|
||||||
'api-po-metadata': models.PurchaseOrder,
|
'api-po-metadata': models.PurchaseOrder,
|
||||||
'api-po-line-metadata': models.PurchaseOrderLineItem,
|
'api-po-line-metadata': models.PurchaseOrderLineItem,
|
||||||
|
@ -72,7 +72,6 @@ class SalesOrderTest(TestCase):
|
|||||||
|
|
||||||
def test_so_reference(self):
|
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
|
# Test that a good reference is created when we have no existing orders
|
||||||
SalesOrder.objects.all().delete()
|
SalesOrder.objects.all().delete()
|
||||||
|
|
||||||
@ -80,7 +79,6 @@ class SalesOrderTest(TestCase):
|
|||||||
|
|
||||||
def test_rebuild_reference(self):
|
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.assertEqual(self.order.reference_int, 1234)
|
||||||
|
|
||||||
self.order.reference = '999'
|
self.order.reference = '999'
|
||||||
@ -121,7 +119,6 @@ class SalesOrderTest(TestCase):
|
|||||||
|
|
||||||
def test_add_duplicate_line_item(self):
|
def test_add_duplicate_line_item(self):
|
||||||
"""Adding a duplicate line item to a SalesOrder is accepted"""
|
"""Adding a duplicate line item to a SalesOrder is accepted"""
|
||||||
|
|
||||||
for ii in range(1, 5):
|
for ii in range(1, 5):
|
||||||
SalesOrderLineItem.objects.create(order=self.order, part=self.part, quantity=ii)
|
SalesOrderLineItem.objects.create(order=self.order, part=self.part, quantity=ii)
|
||||||
|
|
||||||
@ -283,14 +280,12 @@ class SalesOrderTest(TestCase):
|
|||||||
|
|
||||||
def test_shipment_delivery(self):
|
def test_shipment_delivery(self):
|
||||||
"""Test the shipment delivery settings"""
|
"""Test the shipment delivery settings"""
|
||||||
|
|
||||||
# Shipment delivery date should be empty before setting date
|
# Shipment delivery date should be empty before setting date
|
||||||
self.assertIsNone(self.shipment.delivery_date)
|
self.assertIsNone(self.shipment.delivery_date)
|
||||||
self.assertFalse(self.shipment.is_delivered())
|
self.assertFalse(self.shipment.is_delivered())
|
||||||
|
|
||||||
def test_overdue_notification(self):
|
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.created_by = get_user_model().objects.get(pk=3)
|
||||||
self.order.responsible = Owner.create(obj=Group.objects.get(pk=2))
|
self.order.responsible = Owner.create(obj=Group.objects.get(pk=2))
|
||||||
self.order.target_date = datetime.now().date() - timedelta(days=1)
|
self.order.target_date = datetime.now().date() - timedelta(days=1)
|
||||||
@ -311,7 +306,6 @@ class SalesOrderTest(TestCase):
|
|||||||
- The responsible user should receive a notification
|
- The responsible user should receive a notification
|
||||||
- The creating user should *not* receive a notification
|
- The creating user should *not* receive a notification
|
||||||
"""
|
"""
|
||||||
|
|
||||||
SalesOrder.objects.create(
|
SalesOrder.objects.create(
|
||||||
customer=self.customer,
|
customer=self.customer,
|
||||||
reference='1234567',
|
reference='1234567',
|
||||||
|
@ -39,7 +39,6 @@ class OrderTest(TestCase):
|
|||||||
|
|
||||||
def test_basics(self):
|
def test_basics(self):
|
||||||
"""Basic tests e.g. repr functions etc."""
|
"""Basic tests e.g. repr functions etc."""
|
||||||
|
|
||||||
for pk in range(1, 8):
|
for pk in range(1, 8):
|
||||||
|
|
||||||
order = PurchaseOrder.objects.get(pk=pk)
|
order = PurchaseOrder.objects.get(pk=pk)
|
||||||
@ -53,7 +52,6 @@ class OrderTest(TestCase):
|
|||||||
|
|
||||||
def test_rebuild_reference(self):
|
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 = PurchaseOrder.objects.get(pk=1)
|
||||||
order.save()
|
order.save()
|
||||||
self.assertEqual(order.reference_int, 1)
|
self.assertEqual(order.reference_int, 1)
|
||||||
@ -219,7 +217,6 @@ class OrderTest(TestCase):
|
|||||||
|
|
||||||
def test_receive_pack_size(self):
|
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)
|
prt = Part.objects.get(pk=1)
|
||||||
sup = Company.objects.get(pk=1)
|
sup = Company.objects.get(pk=1)
|
||||||
|
|
||||||
@ -366,7 +363,6 @@ class OrderTest(TestCase):
|
|||||||
- The responsible user(s) should receive a notification
|
- The responsible user(s) should receive a notification
|
||||||
- The creating user should *not* receive a notification
|
- The creating user should *not* receive a notification
|
||||||
"""
|
"""
|
||||||
|
|
||||||
po = PurchaseOrder.objects.create(
|
po = PurchaseOrder.objects.create(
|
||||||
supplier=Company.objects.get(pk=1),
|
supplier=Company.objects.get(pk=1),
|
||||||
reference='XYZABC',
|
reference='XYZABC',
|
||||||
|
@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
def generate_next_sales_order_reference():
|
def generate_next_sales_order_reference():
|
||||||
"""Generate the next available SalesOrder reference"""
|
"""Generate the next available SalesOrder reference"""
|
||||||
|
|
||||||
from order.models import SalesOrder
|
from order.models import SalesOrder
|
||||||
|
|
||||||
return SalesOrder.generate_reference()
|
return SalesOrder.generate_reference()
|
||||||
@ -11,7 +10,6 @@ def generate_next_sales_order_reference():
|
|||||||
|
|
||||||
def generate_next_purchase_order_reference():
|
def generate_next_purchase_order_reference():
|
||||||
"""Generate the next available PurchasesOrder reference"""
|
"""Generate the next available PurchasesOrder reference"""
|
||||||
|
|
||||||
from order.models import PurchaseOrder
|
from order.models import PurchaseOrder
|
||||||
|
|
||||||
return PurchaseOrder.generate_reference()
|
return PurchaseOrder.generate_reference()
|
||||||
@ -19,7 +17,6 @@ def generate_next_purchase_order_reference():
|
|||||||
|
|
||||||
def generate_next_return_order_reference():
|
def generate_next_return_order_reference():
|
||||||
"""Generate the next available ReturnOrder reference"""
|
"""Generate the next available ReturnOrder reference"""
|
||||||
|
|
||||||
from order.models import ReturnOrder
|
from order.models import ReturnOrder
|
||||||
|
|
||||||
return ReturnOrder.generate_reference()
|
return ReturnOrder.generate_reference()
|
||||||
@ -27,7 +24,6 @@ def generate_next_return_order_reference():
|
|||||||
|
|
||||||
def validate_sales_order_reference_pattern(pattern):
|
def validate_sales_order_reference_pattern(pattern):
|
||||||
"""Validate the SalesOrder reference 'pattern' setting"""
|
"""Validate the SalesOrder reference 'pattern' setting"""
|
||||||
|
|
||||||
from order.models import SalesOrder
|
from order.models import SalesOrder
|
||||||
|
|
||||||
SalesOrder.validate_reference_pattern(pattern)
|
SalesOrder.validate_reference_pattern(pattern)
|
||||||
@ -35,7 +31,6 @@ def validate_sales_order_reference_pattern(pattern):
|
|||||||
|
|
||||||
def validate_purchase_order_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
|
from order.models import PurchaseOrder
|
||||||
|
|
||||||
PurchaseOrder.validate_reference_pattern(pattern)
|
PurchaseOrder.validate_reference_pattern(pattern)
|
||||||
@ -43,7 +38,6 @@ def validate_purchase_order_reference_pattern(pattern):
|
|||||||
|
|
||||||
def validate_return_order_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
|
from order.models import ReturnOrder
|
||||||
|
|
||||||
ReturnOrder.validate_reference_pattern(pattern)
|
ReturnOrder.validate_reference_pattern(pattern)
|
||||||
@ -51,7 +45,6 @@ def validate_return_order_reference_pattern(pattern):
|
|||||||
|
|
||||||
def validate_sales_order_reference(value):
|
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
|
from order.models import SalesOrder
|
||||||
|
|
||||||
SalesOrder.validate_reference_field(value)
|
SalesOrder.validate_reference_field(value)
|
||||||
@ -59,7 +52,6 @@ def validate_sales_order_reference(value):
|
|||||||
|
|
||||||
def validate_purchase_order_reference(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
|
from order.models import PurchaseOrder
|
||||||
|
|
||||||
PurchaseOrder.validate_reference_field(value)
|
PurchaseOrder.validate_reference_field(value)
|
||||||
@ -67,7 +59,6 @@ def validate_purchase_order_reference(value):
|
|||||||
|
|
||||||
def validate_return_order_reference(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
|
from order.models import ReturnOrder
|
||||||
|
|
||||||
ReturnOrder.validate_reference_field(value)
|
ReturnOrder.validate_reference_field(value)
|
||||||
|
@ -68,7 +68,6 @@ class PartResource(InvenTreeResource):
|
|||||||
|
|
||||||
def dehydrate_min_cost(self, part):
|
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
|
min_cost = part.pricing.overall_min if part.pricing else None
|
||||||
|
|
||||||
if min_cost is not None:
|
if min_cost is not None:
|
||||||
@ -76,7 +75,6 @@ class PartResource(InvenTreeResource):
|
|||||||
|
|
||||||
def dehydrate_max_cost(self, part):
|
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
|
max_cost = part.pricing.overall_max if part.pricing else None
|
||||||
|
|
||||||
if max_cost is not None:
|
if max_cost is not None:
|
||||||
@ -97,7 +95,6 @@ class PartResource(InvenTreeResource):
|
|||||||
|
|
||||||
def after_import(self, dataset, result, using_transactions, dry_run, **kwargs):
|
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)
|
super().after_import(dataset, result, using_transactions, dry_run, **kwargs)
|
||||||
|
|
||||||
# Rebuild the Part tree(s)
|
# Rebuild the Part tree(s)
|
||||||
@ -203,7 +200,6 @@ class PartCategoryResource(InvenTreeResource):
|
|||||||
|
|
||||||
def after_import(self, dataset, result, using_transactions, dry_run, **kwargs):
|
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)
|
super().after_import(dataset, result, using_transactions, dry_run, **kwargs)
|
||||||
|
|
||||||
# Rebuild the PartCategory tree(s)
|
# Rebuild the PartCategory tree(s)
|
||||||
@ -284,7 +280,6 @@ class BomItemResource(InvenTreeResource):
|
|||||||
|
|
||||||
def dehydrate_min_cost(self, item):
|
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
|
min_price = item.sub_part.pricing.overall_min if item.sub_part.pricing else None
|
||||||
|
|
||||||
if min_price is not None:
|
if min_price is not None:
|
||||||
@ -292,7 +287,6 @@ class BomItemResource(InvenTreeResource):
|
|||||||
|
|
||||||
def dehydrate_max_cost(self, item):
|
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
|
max_price = item.sub_part.pricing.overall_max if item.sub_part.pricing else None
|
||||||
|
|
||||||
if max_price is not None:
|
if max_price is not None:
|
||||||
@ -307,7 +301,6 @@ class BomItemResource(InvenTreeResource):
|
|||||||
|
|
||||||
def before_export(self, queryset, *args, **kwargs):
|
def before_export(self, queryset, *args, **kwargs):
|
||||||
"""Perform before exporting data"""
|
"""Perform before exporting data"""
|
||||||
|
|
||||||
self.is_importing = kwargs.get('importing', False)
|
self.is_importing = kwargs.get('importing', False)
|
||||||
self.include_pricing = kwargs.pop('include_pricing', False)
|
self.include_pricing = kwargs.pop('include_pricing', False)
|
||||||
|
|
||||||
|
@ -50,7 +50,6 @@ class CategoryMixin:
|
|||||||
|
|
||||||
def get_queryset(self, *args, **kwargs):
|
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 = super().get_queryset(*args, **kwargs)
|
||||||
queryset = part_serializers.CategorySerializer.annotate_queryset(queryset)
|
queryset = part_serializers.CategorySerializer.annotate_queryset(queryset)
|
||||||
return queryset
|
return queryset
|
||||||
@ -77,7 +76,6 @@ class CategoryList(CategoryMixin, APIDownloadMixin, ListCreateAPI):
|
|||||||
|
|
||||||
def download_queryset(self, queryset, export_format):
|
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)
|
dataset = PartCategoryResource().export(queryset=queryset)
|
||||||
filedata = dataset.export(export_format)
|
filedata = dataset.export(export_format)
|
||||||
filename = f"InvenTree_Categories.{export_format}"
|
filename = f"InvenTree_Categories.{export_format}"
|
||||||
@ -192,7 +190,6 @@ class CategoryDetail(CategoryMixin, CustomRetrieveUpdateDestroyAPI):
|
|||||||
|
|
||||||
def get_serializer(self, *args, **kwargs):
|
def get_serializer(self, *args, **kwargs):
|
||||||
"""Add additional context based on query parameters"""
|
"""Add additional context based on query parameters"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
params = self.request.query_params
|
params = self.request.query_params
|
||||||
|
|
||||||
@ -466,7 +463,6 @@ class PartScheduling(RetrieveAPI):
|
|||||||
|
|
||||||
def retrieve(self, request, *args, **kwargs):
|
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()
|
part = self.get_object()
|
||||||
|
|
||||||
schedule = []
|
schedule = []
|
||||||
@ -674,7 +670,6 @@ class PartRequirements(RetrieveAPI):
|
|||||||
|
|
||||||
def retrieve(self, request, *args, **kwargs):
|
def retrieve(self, request, *args, **kwargs):
|
||||||
"""Construct a response detailing Part requirements"""
|
"""Construct a response detailing Part requirements"""
|
||||||
|
|
||||||
part = self.get_object()
|
part = self.get_object()
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
@ -700,13 +695,11 @@ class PartPricingDetail(RetrieveUpdateAPI):
|
|||||||
|
|
||||||
def get_object(self):
|
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()
|
part = super().get_object()
|
||||||
return part.pricing
|
return part.pricing
|
||||||
|
|
||||||
def _get_serializer(self, *args, **kwargs):
|
def _get_serializer(self, *args, **kwargs):
|
||||||
"""Return a part pricing serializer object"""
|
"""Return a part pricing serializer object"""
|
||||||
|
|
||||||
part = self.get_object()
|
part = self.get_object()
|
||||||
kwargs['instance'] = part.pricing
|
kwargs['instance'] = part.pricing
|
||||||
|
|
||||||
@ -825,7 +818,6 @@ class PartFilter(rest_filters.FilterSet):
|
|||||||
|
|
||||||
def filter_has_units(self, queryset, name, value):
|
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):
|
if str2bool(value):
|
||||||
return queryset.exclude(units='')
|
return queryset.exclude(units='')
|
||||||
else:
|
else:
|
||||||
@ -836,7 +828,6 @@ class PartFilter(rest_filters.FilterSet):
|
|||||||
|
|
||||||
def filter_has_ipn(self, queryset, name, value):
|
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):
|
if str2bool(value):
|
||||||
return queryset.exclude(IPN='')
|
return queryset.exclude(IPN='')
|
||||||
else:
|
else:
|
||||||
@ -860,7 +851,6 @@ class PartFilter(rest_filters.FilterSet):
|
|||||||
|
|
||||||
def filter_low_stock(self, queryset, name, value):
|
def filter_low_stock(self, queryset, name, value):
|
||||||
"""Filter by "low stock" status."""
|
"""Filter by "low stock" status."""
|
||||||
|
|
||||||
if str2bool(value):
|
if str2bool(value):
|
||||||
# Ignore any parts which do not have a specified 'minimum_stock' level
|
# Ignore any parts which do not have a specified 'minimum_stock' level
|
||||||
# Filter items which have an 'in_stock' level lower than 'minimum_stock'
|
# Filter items which have an 'in_stock' level lower than 'minimum_stock'
|
||||||
@ -874,7 +864,6 @@ class PartFilter(rest_filters.FilterSet):
|
|||||||
|
|
||||||
def filter_has_stock(self, queryset, name, value):
|
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):
|
if str2bool(value):
|
||||||
return queryset.filter(Q(in_stock__gt=0))
|
return queryset.filter(Q(in_stock__gt=0))
|
||||||
else:
|
else:
|
||||||
@ -885,7 +874,6 @@ class PartFilter(rest_filters.FilterSet):
|
|||||||
|
|
||||||
def filter_unallocated_stock(self, queryset, name, value):
|
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):
|
if str2bool(value):
|
||||||
return queryset.filter(Q(unallocated_stock__gt=0))
|
return queryset.filter(Q(unallocated_stock__gt=0))
|
||||||
else:
|
else:
|
||||||
@ -905,7 +893,6 @@ class PartFilter(rest_filters.FilterSet):
|
|||||||
|
|
||||||
def filter_exclude_tree(self, queryset, name, part):
|
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)
|
children = part.get_descendants(include_self=True)
|
||||||
|
|
||||||
return queryset.exclude(id__in=children)
|
return queryset.exclude(id__in=children)
|
||||||
@ -914,7 +901,6 @@ class PartFilter(rest_filters.FilterSet):
|
|||||||
|
|
||||||
def filter_ancestor(self, queryset, name, part):
|
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)
|
descendants = part.get_descendants(include_self=False)
|
||||||
return queryset.filter(id__in=descendants)
|
return queryset.filter(id__in=descendants)
|
||||||
|
|
||||||
@ -922,14 +908,12 @@ class PartFilter(rest_filters.FilterSet):
|
|||||||
|
|
||||||
def filter_variant_of(self, queryset, name, part):
|
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())
|
return queryset.filter(id__in=part.get_children())
|
||||||
|
|
||||||
in_bom_for = rest_filters.ModelChoiceFilter(label='In BOM Of', queryset=Part.objects.all(), method='filter_in_bom')
|
in_bom_for = rest_filters.ModelChoiceFilter(label='In BOM Of', queryset=Part.objects.all(), method='filter_in_bom')
|
||||||
|
|
||||||
def filter_in_bom(self, queryset, name, part):
|
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()
|
bom_parts = part.get_parts_in_bom()
|
||||||
return queryset.filter(id__in=[p.pk for p in bom_parts])
|
return queryset.filter(id__in=[p.pk for p in bom_parts])
|
||||||
|
|
||||||
@ -937,7 +921,6 @@ class PartFilter(rest_filters.FilterSet):
|
|||||||
|
|
||||||
def filter_has_pricing(self, queryset, name, value):
|
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_a = Q(pricing_data=None)
|
||||||
q_b = Q(pricing_data__overall_min=None, pricing_data__overall_max=None)
|
q_b = Q(pricing_data__overall_min=None, pricing_data__overall_max=None)
|
||||||
|
|
||||||
@ -950,7 +933,6 @@ class PartFilter(rest_filters.FilterSet):
|
|||||||
|
|
||||||
def filter_has_stocktake(self, queryset, name, value):
|
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):
|
if str2bool(value):
|
||||||
return queryset.exclude(last_stocktake=None)
|
return queryset.exclude(last_stocktake=None)
|
||||||
else:
|
else:
|
||||||
@ -960,7 +942,6 @@ class PartFilter(rest_filters.FilterSet):
|
|||||||
|
|
||||||
def filter_stock_to_build(self, queryset, name, value):
|
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):
|
if str2bool(value):
|
||||||
# Return parts which are required for a build order, but have not yet been allocated
|
# Return parts which are required for a build order, but have not yet been allocated
|
||||||
return queryset.filter(required_for_build_orders__gt=F('allocated_to_build_orders'))
|
return queryset.filter(required_for_build_orders__gt=F('allocated_to_build_orders'))
|
||||||
@ -972,7 +953,6 @@ class PartFilter(rest_filters.FilterSet):
|
|||||||
|
|
||||||
def filter_depleted_stock(self, queryset, name, value):
|
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):
|
if str2bool(value):
|
||||||
return queryset.filter(Q(in_stock=0) & ~Q(stock_item_count=0))
|
return queryset.filter(Q(in_stock=0) & ~Q(stock_item_count=0))
|
||||||
else:
|
else:
|
||||||
@ -1234,7 +1214,6 @@ class PartList(PartMixin, APIDownloadMixin, ListCreateAPI):
|
|||||||
- Only parts which have a matching parameter are returned
|
- Only parts which have a matching parameter are returned
|
||||||
- Queryset is ordered based on parameter value
|
- Queryset is ordered based on parameter value
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Extract "ordering" parameter from query args
|
# Extract "ordering" parameter from query args
|
||||||
ordering = self.request.query_params.get('ordering', None)
|
ordering = self.request.query_params.get('ordering', None)
|
||||||
|
|
||||||
@ -1379,7 +1358,6 @@ class PartParameterTemplateFilter(rest_filters.FilterSet):
|
|||||||
|
|
||||||
def filter_has_choices(self, queryset, name, value):
|
def filter_has_choices(self, queryset, name, value):
|
||||||
"""Filter queryset to include only PartParameterTemplates with choices."""
|
"""Filter queryset to include only PartParameterTemplates with choices."""
|
||||||
|
|
||||||
if str2bool(value):
|
if str2bool(value):
|
||||||
return queryset.exclude(Q(choices=None) | Q(choices=''))
|
return queryset.exclude(Q(choices=None) | Q(choices=''))
|
||||||
else:
|
else:
|
||||||
@ -1392,7 +1370,6 @@ class PartParameterTemplateFilter(rest_filters.FilterSet):
|
|||||||
|
|
||||||
def filter_has_units(self, queryset, name, value):
|
def filter_has_units(self, queryset, name, value):
|
||||||
"""Filter queryset to include only PartParameterTemplates with units."""
|
"""Filter queryset to include only PartParameterTemplates with units."""
|
||||||
|
|
||||||
if str2bool(value):
|
if str2bool(value):
|
||||||
return queryset.exclude(Q(units=None) | Q(units=''))
|
return queryset.exclude(Q(units=None) | Q(units=''))
|
||||||
else:
|
else:
|
||||||
@ -1488,7 +1465,6 @@ class PartParameterAPIMixin:
|
|||||||
- part_detail
|
- part_detail
|
||||||
- template_detail
|
- template_detail
|
||||||
"""
|
"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
kwargs['part_detail'] = str2bool(self.request.GET.get('part_detail', False))
|
kwargs['part_detail'] = str2bool(self.request.GET.get('part_detail', False))
|
||||||
kwargs['template_detail'] = str2bool(self.request.GET.get('template_detail', True))
|
kwargs['template_detail'] = str2bool(self.request.GET.get('template_detail', True))
|
||||||
@ -1515,7 +1491,6 @@ class PartParameterFilter(rest_filters.FilterSet):
|
|||||||
|
|
||||||
If 'include_variants' query parameter is provided, filter against variant parts also
|
If 'include_variants' query parameter is provided, filter against variant parts also
|
||||||
"""
|
"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
include_variants = str2bool(self.request.GET.get('include_variants', False))
|
include_variants = str2bool(self.request.GET.get('include_variants', False))
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
@ -1679,7 +1654,6 @@ class BomFilter(rest_filters.FilterSet):
|
|||||||
|
|
||||||
def filter_available_stock(self, queryset, name, value):
|
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):
|
if str2bool(value):
|
||||||
return queryset.filter(available_stock__gt=0)
|
return queryset.filter(available_stock__gt=0)
|
||||||
else:
|
else:
|
||||||
@ -1689,7 +1663,6 @@ class BomFilter(rest_filters.FilterSet):
|
|||||||
|
|
||||||
def filter_on_order(self, queryset, name, value):
|
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):
|
if str2bool(value):
|
||||||
return queryset.filter(on_order__gt=0)
|
return queryset.filter(on_order__gt=0)
|
||||||
else:
|
else:
|
||||||
@ -1699,7 +1672,6 @@ class BomFilter(rest_filters.FilterSet):
|
|||||||
|
|
||||||
def filter_has_pricing(self, queryset, name, value):
|
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_a = Q(sub_part__pricing_data=None)
|
||||||
q_b = Q(sub_part__pricing_data__overall_min=None, sub_part__pricing_data__overall_max=None)
|
q_b = Q(sub_part__pricing_data__overall_min=None, sub_part__pricing_data__overall_max=None)
|
||||||
|
|
||||||
@ -1722,7 +1694,6 @@ class BomMixin:
|
|||||||
- part_detail
|
- part_detail
|
||||||
- sub_part_detail
|
- sub_part_detail
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Do we wish to include extra detail?
|
# Do we wish to include extra detail?
|
||||||
try:
|
try:
|
||||||
kwargs['part_detail'] = str2bool(self.request.GET.get('part_detail', None))
|
kwargs['part_detail'] = str2bool(self.request.GET.get('part_detail', None))
|
||||||
@ -1760,7 +1731,6 @@ class BomList(BomMixin, ListCreateDestroyAPIView):
|
|||||||
|
|
||||||
def list(self, request, *args, **kwargs):
|
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())
|
queryset = self.filter_queryset(self.get_queryset())
|
||||||
|
|
||||||
page = self.paginate_queryset(queryset)
|
page = self.paginate_queryset(queryset)
|
||||||
|
@ -49,7 +49,6 @@ class PartConfig(AppConfig):
|
|||||||
|
|
||||||
Prevents issues with state machine if the server is restarted mid-update
|
Prevents issues with state machine if the server is restarted mid-update
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from .models import PartPricing
|
from .models import PartPricing
|
||||||
|
|
||||||
if isImportingData():
|
if isImportingData():
|
||||||
|
@ -63,7 +63,6 @@ def ExportBom(part: Part, fmt='csv', cascade: bool = False, max_levels: int = No
|
|||||||
Returns:
|
Returns:
|
||||||
StreamingHttpResponse: Response that can be passed to the endpoint
|
StreamingHttpResponse: Response that can be passed to the endpoint
|
||||||
"""
|
"""
|
||||||
|
|
||||||
parameter_data = str2bool(kwargs.get('parameter_data', False))
|
parameter_data = str2bool(kwargs.get('parameter_data', False))
|
||||||
stock_data = str2bool(kwargs.get('stock_data', False))
|
stock_data = str2bool(kwargs.get('stock_data', False))
|
||||||
supplier_data = str2bool(kwargs.get('supplier_data', False))
|
supplier_data = str2bool(kwargs.get('supplier_data', False))
|
||||||
|
@ -43,7 +43,6 @@ def annotate_on_order_quantity(reference: str = ''):
|
|||||||
|
|
||||||
Note that in addition to the 'quantity' on order, we must also take into account 'pack_quantity'.
|
Note that in addition to the 'quantity' on order, we must also take into account 'pack_quantity'.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Filter only 'active' purhase orders
|
# Filter only 'active' purhase orders
|
||||||
# Filter only line with outstanding quantity
|
# Filter only line with outstanding quantity
|
||||||
order_filter = Q(
|
order_filter = Q(
|
||||||
@ -85,7 +84,6 @@ def annotate_total_stock(reference: str = ''):
|
|||||||
reference: The relationship reference of the part from the current model e.g. 'part'
|
reference: The relationship reference of the part from the current model e.g. 'part'
|
||||||
stock_filter: Q object which defines how to filter the stock items
|
stock_filter: Q object which defines how to filter the stock items
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Stock filter only returns 'in stock' items
|
# Stock filter only returns 'in stock' items
|
||||||
stock_filter = stock.models.StockItem.IN_STOCK_FILTER
|
stock_filter = stock.models.StockItem.IN_STOCK_FILTER
|
||||||
|
|
||||||
@ -107,7 +105,6 @@ def annotate_build_order_requirements(reference: str = ''):
|
|||||||
- We are interested in the 'quantity' of each BuildLine item
|
- We are interested in the 'quantity' of each BuildLine item
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Active build orders only
|
# Active build orders only
|
||||||
build_filter = Q(build__status__in=BuildStatusGroups.ACTIVE_CODES)
|
build_filter = Q(build__status__in=BuildStatusGroups.ACTIVE_CODES)
|
||||||
|
|
||||||
@ -132,7 +129,6 @@ def annotate_build_order_allocations(reference: str = ''):
|
|||||||
reference: The relationship reference of the part from the current model
|
reference: The relationship reference of the part from the current model
|
||||||
build_filter: Q object which defines how to filter the allocation items
|
build_filter: Q object which defines how to filter the allocation items
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Build filter only returns 'active' build orders
|
# Build filter only returns 'active' build orders
|
||||||
build_filter = Q(build_line__build__status__in=BuildStatusGroups.ACTIVE_CODES)
|
build_filter = Q(build_line__build__status__in=BuildStatusGroups.ACTIVE_CODES)
|
||||||
|
|
||||||
@ -157,7 +153,6 @@ def annotate_sales_order_allocations(reference: str = ''):
|
|||||||
reference: The relationship reference of the part from the current model
|
reference: The relationship reference of the part from the current model
|
||||||
order_filter: Q object which defines how to filter the allocation items
|
order_filter: Q object which defines how to filter the allocation items
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Order filter only returns incomplete shipments for open orders
|
# Order filter only returns incomplete shipments for open orders
|
||||||
order_filter = Q(
|
order_filter = Q(
|
||||||
line__order__status__in=SalesOrderStatusGroups.OPEN,
|
line__order__status__in=SalesOrderStatusGroups.OPEN,
|
||||||
@ -183,7 +178,6 @@ def variant_stock_query(reference: str = '', filter: Q = stock.models.StockItem.
|
|||||||
reference: The relationship reference of the part from the current model
|
reference: The relationship reference of the part from the current model
|
||||||
filter: Q object which defines how to filter the returned StockItem instances
|
filter: Q object which defines how to filter the returned StockItem instances
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return stock.models.StockItem.objects.filter(
|
return stock.models.StockItem.objects.filter(
|
||||||
part__tree_id=OuterRef(f'{reference}tree_id'),
|
part__tree_id=OuterRef(f'{reference}tree_id'),
|
||||||
part__lft__gt=OuterRef(f'{reference}lft'),
|
part__lft__gt=OuterRef(f'{reference}lft'),
|
||||||
@ -198,7 +192,6 @@ def annotate_variant_quantity(subquery: Q, reference: str = 'quantity'):
|
|||||||
subquery: A 'variant_stock_query' Q object
|
subquery: A 'variant_stock_query' Q object
|
||||||
reference: The relationship reference of the variant stock items from the current queryset
|
reference: The relationship reference of the variant stock items from the current queryset
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return Coalesce(
|
return Coalesce(
|
||||||
Subquery(
|
Subquery(
|
||||||
subquery.annotate(
|
subquery.annotate(
|
||||||
@ -216,7 +209,6 @@ def annotate_category_parts():
|
|||||||
- Includes parts in subcategories also
|
- Includes parts in subcategories also
|
||||||
- Requires subquery to perform annotation
|
- Requires subquery to perform annotation
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Construct a subquery to provide all parts in this category and any subcategories:
|
# Construct a subquery to provide all parts in this category and any subcategories:
|
||||||
subquery = part.models.Part.objects.exclude(category=None).filter(
|
subquery = part.models.Part.objects.exclude(category=None).filter(
|
||||||
category__tree_id=OuterRef('tree_id'),
|
category__tree_id=OuterRef('tree_id'),
|
||||||
@ -250,9 +242,7 @@ def filter_by_parameter(queryset, template_id: int, value: str, func: str = ''):
|
|||||||
Returns:
|
Returns:
|
||||||
A queryset of Part objects filtered by the given parameter
|
A queryset of Part objects filtered by the given parameter
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# TODO
|
# TODO
|
||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
@ -268,7 +258,6 @@ def order_by_parameter(queryset, template_id: int, ascending=True):
|
|||||||
Returns:
|
Returns:
|
||||||
A queryset of Part objects ordered by the given parameter
|
A queryset of Part objects ordered by the given parameter
|
||||||
"""
|
"""
|
||||||
|
|
||||||
template_filter = part.models.PartParameter.objects.filter(
|
template_filter = part.models.PartParameter.objects.filter(
|
||||||
template__id=template_id,
|
template__id=template_id,
|
||||||
part_id=OuterRef('id'),
|
part_id=OuterRef('id'),
|
||||||
|
@ -453,7 +453,6 @@ class Part(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, MPTTModel)
|
|||||||
If the part image has been updated, then check if the "old" (previous) image is still used by another part.
|
If the part image has been updated, then check if the "old" (previous) image is still used by another part.
|
||||||
If not, it is considered "orphaned" and will be deleted.
|
If not, it is considered "orphaned" and will be deleted.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if self.pk:
|
if self.pk:
|
||||||
try:
|
try:
|
||||||
previous = Part.objects.get(pk=self.pk)
|
previous = Part.objects.get(pk=self.pk)
|
||||||
@ -556,7 +555,6 @@ class Part(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, MPTTModel)
|
|||||||
|
|
||||||
This function is exposed to any Validation plugins, and thus can be customized.
|
This function is exposed to any Validation plugins, and thus can be customized.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from plugin.registry import registry
|
from plugin.registry import registry
|
||||||
|
|
||||||
for plugin in registry.with_mixin('validation'):
|
for plugin in registry.with_mixin('validation'):
|
||||||
@ -579,7 +577,6 @@ class Part(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, MPTTModel)
|
|||||||
- Validation is handled by custom plugins
|
- Validation is handled by custom plugins
|
||||||
- By default, no validation checks are performed
|
- By default, no validation checks are performed
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from plugin.registry import registry
|
from plugin.registry import registry
|
||||||
|
|
||||||
for plugin in registry.with_mixin('validation'):
|
for plugin in registry.with_mixin('validation'):
|
||||||
@ -626,7 +623,6 @@ class Part(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, MPTTModel)
|
|||||||
Raises:
|
Raises:
|
||||||
ValidationError if serial number is invalid and raise_error = True
|
ValidationError if serial number is invalid and raise_error = True
|
||||||
"""
|
"""
|
||||||
|
|
||||||
serial = str(serial).strip()
|
serial = str(serial).strip()
|
||||||
|
|
||||||
# First, throw the serial number against each of the loaded validation plugins
|
# First, throw the serial number against each of the loaded validation plugins
|
||||||
@ -682,7 +678,6 @@ class Part(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, MPTTModel)
|
|||||||
|
|
||||||
def find_conflicting_serial_numbers(self, serials: list):
|
def find_conflicting_serial_numbers(self, serials: list):
|
||||||
"""For a provided list of serials, return a list of those which are conflicting."""
|
"""For a provided list of serials, return a list of those which are conflicting."""
|
||||||
|
|
||||||
conflicts = []
|
conflicts = []
|
||||||
|
|
||||||
for serial in serials:
|
for serial in serials:
|
||||||
@ -704,7 +699,6 @@ class Part(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, MPTTModel)
|
|||||||
Returns:
|
Returns:
|
||||||
The latest serial number specified for this part, or None
|
The latest serial number specified for this part, or None
|
||||||
"""
|
"""
|
||||||
|
|
||||||
stock = StockModels.StockItem.objects.all().exclude(serial=None).exclude(serial='')
|
stock = StockModels.StockItem.objects.all().exclude(serial=None).exclude(serial='')
|
||||||
|
|
||||||
# Generate a query for any stock items for this part variant tree with non-empty serial numbers
|
# Generate a query for any stock items for this part variant tree with non-empty serial numbers
|
||||||
@ -1237,7 +1231,6 @@ class Part(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, MPTTModel)
|
|||||||
@property
|
@property
|
||||||
def can_build(self):
|
def can_build(self):
|
||||||
"""Return the number of units that can be build with available stock."""
|
"""Return the number of units that can be build with available stock."""
|
||||||
|
|
||||||
import part.filters
|
import part.filters
|
||||||
|
|
||||||
# If this part does NOT have a BOM, result is simply the currently available stock
|
# If this part does NOT have a BOM, result is simply the currently available stock
|
||||||
@ -1436,7 +1429,6 @@ class Part(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, MPTTModel)
|
|||||||
|
|
||||||
def allocation_count(self, **kwargs):
|
def allocation_count(self, **kwargs):
|
||||||
"""Return the total quantity of stock allocated for this part, against both build orders and sales orders."""
|
"""Return the total quantity of stock allocated for this part, against both build orders and sales orders."""
|
||||||
|
|
||||||
if self.id is None:
|
if self.id is None:
|
||||||
# If this instance has not been saved, foreign-key lookups will fail
|
# If this instance has not been saved, foreign-key lookups will fail
|
||||||
return 0
|
return 0
|
||||||
@ -1562,7 +1554,6 @@ class Part(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, MPTTModel)
|
|||||||
|
|
||||||
So we construct a query for each case, and combine them...
|
So we construct a query for each case, and combine them...
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Cache all *parent* parts
|
# Cache all *parent* parts
|
||||||
try:
|
try:
|
||||||
parents = self.get_ancestors(include_self=False)
|
parents = self.get_ancestors(include_self=False)
|
||||||
@ -1599,7 +1590,6 @@ class Part(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, MPTTModel)
|
|||||||
|
|
||||||
Includes consideration of inherited BOMs
|
Includes consideration of inherited BOMs
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Grab a queryset of all BomItem objects which "require" this part
|
# Grab a queryset of all BomItem objects which "require" this part
|
||||||
bom_items = BomItem.objects.filter(
|
bom_items = BomItem.objects.filter(
|
||||||
self.get_used_in_bom_item_filter(
|
self.get_used_in_bom_item_filter(
|
||||||
@ -1738,7 +1728,6 @@ class Part(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, MPTTModel)
|
|||||||
|
|
||||||
def update_pricing(self):
|
def update_pricing(self):
|
||||||
"""Recalculate cached pricing for this Part instance"""
|
"""Recalculate cached pricing for this Part instance"""
|
||||||
|
|
||||||
self.pricing.update_pricing()
|
self.pricing.update_pricing()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -1748,7 +1737,6 @@ class Part(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, MPTTModel)
|
|||||||
If there is no PartPricing database entry defined for this Part,
|
If there is no PartPricing database entry defined for this Part,
|
||||||
it will first be created, and then returned.
|
it will first be created, and then returned.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
pricing = PartPricing.objects.get(part=self)
|
pricing = PartPricing.objects.get(part=self)
|
||||||
except PartPricing.DoesNotExist:
|
except PartPricing.DoesNotExist:
|
||||||
@ -1768,7 +1756,6 @@ class Part(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, MPTTModel)
|
|||||||
create: Whether or not a new PartPricing object should be created if it does not already exist
|
create: Whether or not a new PartPricing object should be created if it does not already exist
|
||||||
test: Whether or not the pricing update is allowed during unit tests
|
test: Whether or not the pricing update is allowed during unit tests
|
||||||
"""
|
"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.refresh_from_db()
|
self.refresh_from_db()
|
||||||
except Part.DoesNotExist:
|
except Part.DoesNotExist:
|
||||||
@ -2102,7 +2089,6 @@ class Part(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, MPTTModel)
|
|||||||
|
|
||||||
def getTestTemplateMap(self, **kwargs):
|
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 = {}
|
templates = {}
|
||||||
|
|
||||||
for template in self.getTestTemplates(**kwargs):
|
for template in self.getTestTemplates(**kwargs):
|
||||||
@ -2160,7 +2146,6 @@ class Part(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, MPTTModel)
|
|||||||
Note that some supplier parts may have a different pack_quantity attribute,
|
Note that some supplier parts may have a different pack_quantity attribute,
|
||||||
and this needs to be taken into account!
|
and this needs to be taken into account!
|
||||||
"""
|
"""
|
||||||
|
|
||||||
quantity = 0
|
quantity = 0
|
||||||
|
|
||||||
# Iterate through all supplier parts
|
# Iterate through all supplier parts
|
||||||
@ -2213,7 +2198,6 @@ class Part(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, MPTTModel)
|
|||||||
@property
|
@property
|
||||||
def latest_stocktake(self):
|
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()
|
return self.stocktakes.order_by('-pk').first()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -2356,7 +2340,6 @@ class PartPricing(common.models.MetaMixin):
|
|||||||
@property
|
@property
|
||||||
def is_valid(self):
|
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
|
return self.updated is not None
|
||||||
|
|
||||||
def convert(self, money):
|
def convert(self, money):
|
||||||
@ -2364,7 +2347,6 @@ class PartPricing(common.models.MetaMixin):
|
|||||||
|
|
||||||
If a MissingRate error is raised, ignore it and return None
|
If a MissingRate error is raised, ignore it and return None
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if money is None:
|
if money is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@ -2380,7 +2362,6 @@ class PartPricing(common.models.MetaMixin):
|
|||||||
|
|
||||||
def schedule_for_update(self, counter: int = 0, test: bool = False):
|
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
|
import InvenTree.ready
|
||||||
|
|
||||||
# If we are running within CI, only schedule the update if the test flag is set
|
# If we are running within CI, only schedule the update if the test flag is set
|
||||||
@ -2446,7 +2427,6 @@ class PartPricing(common.models.MetaMixin):
|
|||||||
|
|
||||||
def update_pricing(self, counter: int = 0, cascade: bool = True):
|
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 importing data, skip pricing update
|
||||||
if InvenTree.ready.isImportingData():
|
if InvenTree.ready.isImportingData():
|
||||||
return
|
return
|
||||||
@ -2485,7 +2465,6 @@ class PartPricing(common.models.MetaMixin):
|
|||||||
|
|
||||||
def update_assemblies(self, counter: int = 0):
|
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
|
# If the linked Part is used in any assemblies, schedule a pricing update for those assemblies
|
||||||
used_in_parts = self.part.get_used_in()
|
used_in_parts = self.part.get_used_in()
|
||||||
|
|
||||||
@ -2494,7 +2473,6 @@ class PartPricing(common.models.MetaMixin):
|
|||||||
|
|
||||||
def update_templates(self, counter: int = 0):
|
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)
|
templates = self.part.get_ancestors(include_self=False)
|
||||||
|
|
||||||
for p in templates:
|
for p in templates:
|
||||||
@ -2502,7 +2480,6 @@ class PartPricing(common.models.MetaMixin):
|
|||||||
|
|
||||||
def save(self, *args, **kwargs):
|
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
|
# Update the currency which was used to perform the calculation
|
||||||
self.currency = currency_code_default()
|
self.currency = currency_code_default()
|
||||||
|
|
||||||
@ -2524,7 +2501,6 @@ class PartPricing(common.models.MetaMixin):
|
|||||||
|
|
||||||
Note: The cumulative costs are calculated based on the specified default currency
|
Note: The cumulative costs are calculated based on the specified default currency
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not self.part.assembly:
|
if not self.part.assembly:
|
||||||
# Not an assembly - no BOM pricing
|
# Not an assembly - no BOM pricing
|
||||||
self.bom_cost_min = None
|
self.bom_cost_min = None
|
||||||
@ -2603,7 +2579,6 @@ class PartPricing(common.models.MetaMixin):
|
|||||||
|
|
||||||
Purchase history only takes into account "completed" purchase orders.
|
Purchase history only takes into account "completed" purchase orders.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Find all line items for completed orders which reference this part
|
# Find all line items for completed orders which reference this part
|
||||||
line_items = OrderModels.PurchaseOrderLineItem.objects.filter(
|
line_items = OrderModels.PurchaseOrderLineItem.objects.filter(
|
||||||
order__status=PurchaseOrderStatus.COMPLETE.value,
|
order__status=PurchaseOrderStatus.COMPLETE.value,
|
||||||
@ -2670,7 +2645,6 @@ class PartPricing(common.models.MetaMixin):
|
|||||||
|
|
||||||
def update_internal_cost(self, save=True):
|
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
|
min_int_cost = None
|
||||||
max_int_cost = None
|
max_int_cost = None
|
||||||
|
|
||||||
@ -2704,7 +2678,6 @@ class PartPricing(common.models.MetaMixin):
|
|||||||
- The limits are simply the lower and upper bounds of available SupplierPriceBreaks
|
- The limits are simply the lower and upper bounds of available SupplierPriceBreaks
|
||||||
- We do not take "quantity" into account here
|
- We do not take "quantity" into account here
|
||||||
"""
|
"""
|
||||||
|
|
||||||
min_sup_cost = None
|
min_sup_cost = None
|
||||||
max_sup_cost = None
|
max_sup_cost = None
|
||||||
|
|
||||||
@ -2745,7 +2718,6 @@ class PartPricing(common.models.MetaMixin):
|
|||||||
|
|
||||||
Here we track the min/max costs of any variant parts.
|
Here we track the min/max costs of any variant parts.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
variant_min = None
|
variant_min = None
|
||||||
variant_max = None
|
variant_max = None
|
||||||
|
|
||||||
@ -2785,7 +2757,6 @@ class PartPricing(common.models.MetaMixin):
|
|||||||
|
|
||||||
Here we simply take the minimum / maximum values of the other calculated fields.
|
Here we simply take the minimum / maximum values of the other calculated fields.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
overall_min = None
|
overall_min = None
|
||||||
overall_max = None
|
overall_max = None
|
||||||
|
|
||||||
@ -2851,7 +2822,6 @@ class PartPricing(common.models.MetaMixin):
|
|||||||
|
|
||||||
def update_sale_cost(self, save=True):
|
def update_sale_cost(self, save=True):
|
||||||
"""Recalculate sale cost data"""
|
"""Recalculate sale cost data"""
|
||||||
|
|
||||||
# Iterate through the sell price breaks
|
# Iterate through the sell price breaks
|
||||||
min_sell_price = None
|
min_sell_price = None
|
||||||
max_sell_price = None
|
max_sell_price = None
|
||||||
@ -3091,7 +3061,6 @@ class PartStocktake(models.Model):
|
|||||||
@receiver(post_save, sender=PartStocktake, dispatch_uid='post_save_stocktake')
|
@receiver(post_save, sender=PartStocktake, dispatch_uid='post_save_stocktake')
|
||||||
def update_last_stocktake(sender, instance, created, **kwargs):
|
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
|
# When a new PartStocktake instance is create, update the last_stocktake date for the Part
|
||||||
if created:
|
if created:
|
||||||
try:
|
try:
|
||||||
@ -3104,7 +3073,6 @@ def update_last_stocktake(sender, instance, created, **kwargs):
|
|||||||
|
|
||||||
def save_stocktake_report(instance, filename):
|
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)
|
filename = os.path.basename(filename)
|
||||||
return os.path.join('stocktake', 'report', filename)
|
return os.path.join('stocktake', 'report', filename)
|
||||||
|
|
||||||
@ -3397,7 +3365,6 @@ class PartParameterTemplate(MetadataMixin, models.Model):
|
|||||||
- A 'checkbox' field cannot have 'choices' set
|
- A 'checkbox' field cannot have 'choices' set
|
||||||
- A 'checkbox' field cannot have 'units' set
|
- A 'checkbox' field cannot have 'units' set
|
||||||
"""
|
"""
|
||||||
|
|
||||||
super().clean()
|
super().clean()
|
||||||
|
|
||||||
# Check that checkbox parameters do not have units or choices
|
# Check that checkbox parameters do not have units or choices
|
||||||
@ -3450,7 +3417,6 @@ class PartParameterTemplate(MetadataMixin, models.Model):
|
|||||||
|
|
||||||
def get_choices(self):
|
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:
|
if not self.choices:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
@ -3496,7 +3462,6 @@ class PartParameterTemplate(MetadataMixin, models.Model):
|
|||||||
@receiver(post_save, sender=PartParameterTemplate, dispatch_uid='post_save_part_parameter_template')
|
@receiver(post_save, sender=PartParameterTemplate, dispatch_uid='post_save_part_parameter_template')
|
||||||
def post_save_part_parameter_template(sender, instance, created, **kwargs):
|
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
|
import part.tasks as part_tasks
|
||||||
|
|
||||||
if InvenTree.ready.canAppAccessDatabase() and not InvenTree.ready.isImportingData():
|
if InvenTree.ready.canAppAccessDatabase() and not InvenTree.ready.isImportingData():
|
||||||
@ -3540,7 +3505,6 @@ class PartParameter(MetadataMixin, models.Model):
|
|||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
"""Custom save method for the PartParameter model."""
|
"""Custom save method for the PartParameter model."""
|
||||||
|
|
||||||
# Validate the PartParameter before saving
|
# Validate the PartParameter before saving
|
||||||
self.calculate_numeric_value()
|
self.calculate_numeric_value()
|
||||||
|
|
||||||
@ -3553,7 +3517,6 @@ class PartParameter(MetadataMixin, models.Model):
|
|||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
"""Validate the PartParameter before saving to the database."""
|
"""Validate the PartParameter before saving to the database."""
|
||||||
|
|
||||||
super().clean()
|
super().clean()
|
||||||
|
|
||||||
# Validate the parameter data against the template units
|
# Validate the parameter data against the template units
|
||||||
@ -3597,7 +3560,6 @@ class PartParameter(MetadataMixin, models.Model):
|
|||||||
- If a 'units' field is provided, then the data will be converted to the base SI unit.
|
- If a 'units' field is provided, then the data will be converted to the base SI unit.
|
||||||
- Otherwise, we'll try to do a simple float cast
|
- Otherwise, we'll try to do a simple float cast
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if self.template.units:
|
if self.template.units:
|
||||||
try:
|
try:
|
||||||
self.data_numeric = InvenTree.conversion.convert_physical_value(self.data, self.template.units)
|
self.data_numeric = InvenTree.conversion.convert_physical_value(self.data, self.template.units)
|
||||||
@ -3775,7 +3737,6 @@ class BomItem(DataImportMixin, MetadataMixin, models.Model):
|
|||||||
|
|
||||||
def get_assemblies(self):
|
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]
|
assemblies = [self.part]
|
||||||
|
|
||||||
if self.inherited:
|
if self.inherited:
|
||||||
@ -4087,7 +4048,6 @@ class BomItem(DataImportMixin, MetadataMixin, models.Model):
|
|||||||
@receiver(post_save, sender=BomItem, dispatch_uid='update_bom_build_lines')
|
@receiver(post_save, sender=BomItem, dispatch_uid='update_bom_build_lines')
|
||||||
def update_bom_build_lines(sender, instance, created, **kwargs):
|
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():
|
if InvenTree.ready.canAppAccessDatabase() and not InvenTree.ready.isImportingData():
|
||||||
import build.tasks
|
import build.tasks
|
||||||
InvenTree.tasks.offload_task(
|
InvenTree.tasks.offload_task(
|
||||||
@ -4101,7 +4061,6 @@ def update_bom_build_lines(sender, instance, created, **kwargs):
|
|||||||
@receiver(post_save, sender=PartInternalPriceBreak, dispatch_uid='post_save_internal_price_break')
|
@receiver(post_save, sender=PartInternalPriceBreak, dispatch_uid='post_save_internal_price_break')
|
||||||
def update_pricing_after_edit(sender, instance, created, **kwargs):
|
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
|
# Update part pricing *unless* we are importing data
|
||||||
if InvenTree.ready.canAppAccessDatabase() and not InvenTree.ready.isImportingData():
|
if InvenTree.ready.canAppAccessDatabase() and not InvenTree.ready.isImportingData():
|
||||||
instance.part.schedule_pricing_update(create=True)
|
instance.part.schedule_pricing_update(create=True)
|
||||||
@ -4112,7 +4071,6 @@ def update_pricing_after_edit(sender, instance, created, **kwargs):
|
|||||||
@receiver(post_delete, sender=PartInternalPriceBreak, dispatch_uid='post_delete_internal_price_break')
|
@receiver(post_delete, sender=PartInternalPriceBreak, dispatch_uid='post_delete_internal_price_break')
|
||||||
def update_pricing_after_delete(sender, instance, **kwargs):
|
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
|
# Update part pricing *unless* we are importing data
|
||||||
if InvenTree.ready.canAppAccessDatabase() and not InvenTree.ready.isImportingData():
|
if InvenTree.ready.canAppAccessDatabase() and not InvenTree.ready.isImportingData():
|
||||||
instance.part.schedule_pricing_update(create=False)
|
instance.part.schedule_pricing_update(create=False)
|
||||||
@ -4203,7 +4161,6 @@ class PartRelated(MetadataMixin, models.Model):
|
|||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
"""Overwrite clean method to check that relation is unique."""
|
"""Overwrite clean method to check that relation is unique."""
|
||||||
|
|
||||||
super().clean()
|
super().clean()
|
||||||
|
|
||||||
if self.part_1 == self.part_2:
|
if self.part_1 == self.part_2:
|
||||||
|
@ -64,7 +64,6 @@ class CategorySerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
|||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
"""Optionally add or remove extra fields"""
|
"""Optionally add or remove extra fields"""
|
||||||
|
|
||||||
path_detail = kwargs.pop('path_detail', False)
|
path_detail = kwargs.pop('path_detail', False)
|
||||||
|
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
@ -79,7 +78,6 @@ class CategorySerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def annotate_queryset(queryset):
|
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!)
|
# Annotate the number of 'parts' which exist in each category (including subcategories!)
|
||||||
queryset = queryset.annotate(
|
queryset = queryset.annotate(
|
||||||
part_count=part.filters.annotate_category_parts()
|
part_count=part.filters.annotate_category_parts()
|
||||||
@ -274,7 +272,6 @@ class PartBriefSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
|||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
"""Custom initialization routine for the PartBrief serializer"""
|
"""Custom initialization routine for the PartBrief serializer"""
|
||||||
|
|
||||||
pricing = kwargs.pop('pricing', True)
|
pricing = kwargs.pop('pricing', True)
|
||||||
|
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
@ -311,7 +308,6 @@ class PartParameterSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
|||||||
|
|
||||||
Allows us to optionally include or exclude particular information
|
Allows us to optionally include or exclude particular information
|
||||||
"""
|
"""
|
||||||
|
|
||||||
template_detail = kwargs.pop('template_detail', True)
|
template_detail = kwargs.pop('template_detail', True)
|
||||||
part_detail = kwargs.pop('part_detail', False)
|
part_detail = kwargs.pop('part_detail', False)
|
||||||
|
|
||||||
@ -360,7 +356,6 @@ class PartSetCategorySerializer(serializers.Serializer):
|
|||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def save(self):
|
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
|
data = self.validated_data
|
||||||
parts = data['parts']
|
parts = data['parts']
|
||||||
category = data['category']
|
category = data['category']
|
||||||
@ -453,7 +448,6 @@ class InitialSupplierSerializer(serializers.Serializer):
|
|||||||
|
|
||||||
def validate_supplier(self, company):
|
def validate_supplier(self, company):
|
||||||
"""Validation for the provided Supplier"""
|
"""Validation for the provided Supplier"""
|
||||||
|
|
||||||
if company and not company.is_supplier:
|
if company and not company.is_supplier:
|
||||||
raise serializers.ValidationError(_('Selected company is not a valid supplier'))
|
raise serializers.ValidationError(_('Selected company is not a valid supplier'))
|
||||||
|
|
||||||
@ -461,7 +455,6 @@ class InitialSupplierSerializer(serializers.Serializer):
|
|||||||
|
|
||||||
def validate_manufacturer(self, company):
|
def validate_manufacturer(self, company):
|
||||||
"""Validation for the provided Manufacturer"""
|
"""Validation for the provided Manufacturer"""
|
||||||
|
|
||||||
if company and not company.is_manufacturer:
|
if company and not company.is_manufacturer:
|
||||||
raise serializers.ValidationError(_('Selected company is not a valid manufacturer'))
|
raise serializers.ValidationError(_('Selected company is not a valid manufacturer'))
|
||||||
|
|
||||||
@ -469,7 +462,6 @@ class InitialSupplierSerializer(serializers.Serializer):
|
|||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
"""Extra validation for this serializer"""
|
"""Extra validation for this serializer"""
|
||||||
|
|
||||||
if company.models.ManufacturerPart.objects.filter(
|
if company.models.ManufacturerPart.objects.filter(
|
||||||
manufacturer=data.get('manufacturer', None),
|
manufacturer=data.get('manufacturer', None),
|
||||||
MPN=data.get('mpn', '')
|
MPN=data.get('mpn', '')
|
||||||
@ -603,7 +595,6 @@ class PartSerializer(InvenTree.serializers.RemoteImageMixin, InvenTree.serialize
|
|||||||
|
|
||||||
def skip_create_fields(self):
|
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 = super().skip_create_fields()
|
||||||
|
|
||||||
fields += [
|
fields += [
|
||||||
@ -621,7 +612,6 @@ class PartSerializer(InvenTree.serializers.RemoteImageMixin, InvenTree.serialize
|
|||||||
|
|
||||||
Performing database queries as efficiently as possible, to reduce database trips.
|
Performing database queries as efficiently as possible, to reduce database trips.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Annotate with the total number of stock items
|
# Annotate with the total number of stock items
|
||||||
queryset = queryset.annotate(
|
queryset = queryset.annotate(
|
||||||
stock_item_count=SubqueryCount('stock_items')
|
stock_item_count=SubqueryCount('stock_items')
|
||||||
@ -759,7 +749,6 @@ class PartSerializer(InvenTree.serializers.RemoteImageMixin, InvenTree.serialize
|
|||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def create(self, validated_data):
|
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)
|
duplicate = validated_data.pop('duplicate', None)
|
||||||
initial_stock = validated_data.pop('initial_stock', None)
|
initial_stock = validated_data.pop('initial_stock', None)
|
||||||
initial_supplier = validated_data.pop('initial_supplier', None)
|
initial_supplier = validated_data.pop('initial_supplier', None)
|
||||||
@ -862,7 +851,6 @@ class PartSerializer(InvenTree.serializers.RemoteImageMixin, InvenTree.serialize
|
|||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
"""Save the Part instance"""
|
"""Save the Part instance"""
|
||||||
|
|
||||||
super().save()
|
super().save()
|
||||||
|
|
||||||
part = self.instance
|
part = self.instance
|
||||||
@ -925,7 +913,6 @@ class PartStocktakeSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
|||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
"""Called when this serializer is saved"""
|
"""Called when this serializer is saved"""
|
||||||
|
|
||||||
data = self.validated_data
|
data = self.validated_data
|
||||||
|
|
||||||
# Add in user information automatically
|
# Add in user information automatically
|
||||||
@ -997,7 +984,6 @@ class PartStocktakeReportGenerateSerializer(serializers.Serializer):
|
|||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
"""Custom validation for this serializer"""
|
"""Custom validation for this serializer"""
|
||||||
|
|
||||||
# Stocktake functionality must be enabled
|
# Stocktake functionality must be enabled
|
||||||
if not common.models.InvenTreeSetting.get_setting('STOCKTAKE_ENABLE', False):
|
if not common.models.InvenTreeSetting.get_setting('STOCKTAKE_ENABLE', False):
|
||||||
raise serializers.ValidationError(_("Stocktake functionality is not enabled"))
|
raise serializers.ValidationError(_("Stocktake functionality is not enabled"))
|
||||||
@ -1010,7 +996,6 @@ class PartStocktakeReportGenerateSerializer(serializers.Serializer):
|
|||||||
|
|
||||||
def save(self):
|
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
|
data = self.validated_data
|
||||||
user = self.context['request'].user
|
user = self.context['request'].user
|
||||||
|
|
||||||
|
@ -41,7 +41,6 @@ def perform_stocktake(target: part.models.Part, user: User, note: str = '', comm
|
|||||||
|
|
||||||
In this case, the stocktake *report* will be limited to the specified location.
|
In this case, the stocktake *report* will be limited to the specified location.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Determine which locations are "valid" for the generated report
|
# Determine which locations are "valid" for the generated report
|
||||||
location = kwargs.get('location', None)
|
location = kwargs.get('location', None)
|
||||||
locations = location.get_descendants(include_self=True) if location else []
|
locations = location.get_descendants(include_self=True) if location else []
|
||||||
@ -158,7 +157,6 @@ def generate_stocktake_report(**kwargs):
|
|||||||
generate_report: If True, generate a stocktake report from the calculated data (default=True)
|
generate_report: If True, generate a stocktake report from the calculated data (default=True)
|
||||||
update_parts: If True, save stocktake information against each filtered Part (default = True)
|
update_parts: If True, save stocktake information against each filtered Part (default = True)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Determine if external locations should be excluded
|
# Determine if external locations should be excluded
|
||||||
exclude_external = kwargs.get(
|
exclude_external = kwargs.get(
|
||||||
'exclude_exernal',
|
'exclude_exernal',
|
||||||
|
@ -74,7 +74,6 @@ def update_part_pricing(pricing: part.models.PartPricing, counter: int = 0):
|
|||||||
pricing: The target PartPricing instance to be updated
|
pricing: The target PartPricing instance to be updated
|
||||||
counter: How many times this function has been called in sequence
|
counter: How many times this function has been called in sequence
|
||||||
"""
|
"""
|
||||||
|
|
||||||
logger.info("Updating part pricing for %s", pricing.part)
|
logger.info("Updating part pricing for %s", pricing.part)
|
||||||
|
|
||||||
pricing.update_pricing(counter=counter)
|
pricing.update_pricing(counter=counter)
|
||||||
@ -91,7 +90,6 @@ def check_missing_pricing(limit=250):
|
|||||||
Arguments:
|
Arguments:
|
||||||
limit: Maximum number of parts to process at once
|
limit: Maximum number of parts to process at once
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Find parts for which pricing information has never been updated
|
# Find parts for which pricing information has never been updated
|
||||||
results = part.models.PartPricing.objects.filter(updated=None)[:limit]
|
results = part.models.PartPricing.objects.filter(updated=None)[:limit]
|
||||||
|
|
||||||
@ -144,7 +142,6 @@ def scheduled_stocktake_reports():
|
|||||||
- Delete 'old' stocktake report files after the specified period
|
- Delete 'old' stocktake report files after the specified period
|
||||||
- Generate new reports at the specified period
|
- Generate new reports at the specified period
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Sleep a random number of seconds to prevent worker conflict
|
# Sleep a random number of seconds to prevent worker conflict
|
||||||
time.sleep(random.randint(1, 5))
|
time.sleep(random.randint(1, 5))
|
||||||
|
|
||||||
@ -185,7 +182,6 @@ def rebuild_parameters(template_id):
|
|||||||
This function is called when a base template is changed,
|
This function is called when a base template is changed,
|
||||||
which may cause the base unit to be adjusted.
|
which may cause the base unit to be adjusted.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
template = part.models.PartParameterTemplate.objects.get(pk=template_id)
|
template = part.models.PartParameterTemplate.objects.get(pk=template_id)
|
||||||
except part.models.PartParameterTemplate.DoesNotExist:
|
except part.models.PartParameterTemplate.DoesNotExist:
|
||||||
@ -215,7 +211,6 @@ def rebuild_supplier_parts(part_id):
|
|||||||
This function is called when a bart part is changed,
|
This function is called when a bart part is changed,
|
||||||
which may cause the native units of any supplier parts to be updated
|
which may cause the native units of any supplier parts to be updated
|
||||||
"""
|
"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
prt = part.models.Part.objects.get(pk=part_id)
|
prt = part.models.Part.objects.get(pk=part_id)
|
||||||
except part.models.Part.DoesNotExist:
|
except part.models.Part.DoesNotExist:
|
||||||
|
@ -18,7 +18,6 @@ register = template.Library()
|
|||||||
@register.simple_tag()
|
@register.simple_tag()
|
||||||
def translation_stats(lang_code):
|
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:
|
if lang_code is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@ -30,7 +29,6 @@ class CustomTranslateNode(TranslateNode):
|
|||||||
|
|
||||||
def render(self, context):
|
def render(self, context):
|
||||||
"""Custom render function overrides / extends default behaviour"""
|
"""Custom render function overrides / extends default behaviour"""
|
||||||
|
|
||||||
result = super().render(context)
|
result = super().render(context)
|
||||||
|
|
||||||
result = bleach.clean(result)
|
result = bleach.clean(result)
|
||||||
@ -58,7 +56,6 @@ def do_translate(parser, token):
|
|||||||
|
|
||||||
The only difference is that we pass this to our custom rendering node class
|
The only difference is that we pass this to our custom rendering node class
|
||||||
"""
|
"""
|
||||||
|
|
||||||
bits = token.split_contents()
|
bits = token.split_contents()
|
||||||
if len(bits) < 2:
|
if len(bits) < 2:
|
||||||
raise TemplateSyntaxError("'%s' takes at least one argument" % bits[0])
|
raise TemplateSyntaxError("'%s' takes at least one argument" % bits[0])
|
||||||
|
@ -105,7 +105,6 @@ def render_date(context, date_object):
|
|||||||
@register.simple_tag
|
@register.simple_tag
|
||||||
def render_currency(money, **kwargs):
|
def render_currency(money, **kwargs):
|
||||||
"""Render a currency / Money object"""
|
"""Render a currency / Money object"""
|
||||||
|
|
||||||
return InvenTree.helpers_model.render_currency(money, **kwargs)
|
return InvenTree.helpers_model.render_currency(money, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
@ -211,14 +210,12 @@ def inventree_logo(**kwargs):
|
|||||||
|
|
||||||
Returns a path to an image file, which can be rendered in the web interface
|
Returns a path to an image file, which can be rendered in the web interface
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return InvenTree.helpers.getLogoImage(**kwargs)
|
return InvenTree.helpers.getLogoImage(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag()
|
@register.simple_tag()
|
||||||
def inventree_splash(**kwargs):
|
def inventree_splash(**kwargs):
|
||||||
"""Return the URL for the InvenTree splash screen, *or* a custom screen if the user has provided one."""
|
"""Return the URL for the InvenTree splash screen, *or* a custom screen if the user has provided one."""
|
||||||
|
|
||||||
return InvenTree.helpers.getSplashScreen(**kwargs)
|
return InvenTree.helpers.getSplashScreen(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
@ -344,7 +341,6 @@ def setting_object(key, *args, **kwargs):
|
|||||||
(Or return None if the setting does not exist)
|
(Or return None if the setting does not exist)
|
||||||
if a user-setting was requested return that
|
if a user-setting was requested return that
|
||||||
"""
|
"""
|
||||||
|
|
||||||
cache = kwargs.get('cache', True)
|
cache = kwargs.get('cache', True)
|
||||||
|
|
||||||
if 'plugin' in kwargs:
|
if 'plugin' in kwargs:
|
||||||
@ -499,7 +495,6 @@ def primitive_to_javascript(primitive):
|
|||||||
@register.simple_tag()
|
@register.simple_tag()
|
||||||
def js_bool(val):
|
def js_bool(val):
|
||||||
"""Return a javascript boolean value (true or false)"""
|
"""Return a javascript boolean value (true or false)"""
|
||||||
|
|
||||||
if val:
|
if val:
|
||||||
return 'true'
|
return 'true'
|
||||||
else:
|
else:
|
||||||
|
@ -14,7 +14,6 @@ logger = logging.getLogger('inventree')
|
|||||||
@register.simple_tag()
|
@register.simple_tag()
|
||||||
def sso_login_enabled():
|
def sso_login_enabled():
|
||||||
"""Return True if single-sign-on is enabled"""
|
"""Return True if single-sign-on is enabled"""
|
||||||
|
|
||||||
return str2bool(InvenTreeSetting.get_setting('LOGIN_ENABLE_SSO'))
|
return str2bool(InvenTreeSetting.get_setting('LOGIN_ENABLE_SSO'))
|
||||||
|
|
||||||
|
|
||||||
@ -33,7 +32,6 @@ def sso_auto_enabled():
|
|||||||
@register.simple_tag()
|
@register.simple_tag()
|
||||||
def sso_check_provider(provider):
|
def sso_check_provider(provider):
|
||||||
"""Return True if the given provider is correctly configured"""
|
"""Return True if the given provider is correctly configured"""
|
||||||
|
|
||||||
import allauth.app_settings
|
import allauth.app_settings
|
||||||
from allauth.socialaccount.models import SocialApp
|
from allauth.socialaccount.models import SocialApp
|
||||||
|
|
||||||
|
@ -109,7 +109,6 @@ class PartCategoryAPITest(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
def test_part_count(self):
|
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')
|
url = reverse('api-part-category-list')
|
||||||
|
|
||||||
# Create a parent category
|
# Create a parent category
|
||||||
@ -162,7 +161,6 @@ class PartCategoryAPITest(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
def test_category_parameters(self):
|
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')
|
url = reverse('api-part-category-parameter-list')
|
||||||
|
|
||||||
response = self.get(url, {}, expected_code=200)
|
response = self.get(url, {}, expected_code=200)
|
||||||
@ -216,7 +214,6 @@ class PartCategoryAPITest(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
This helps to protect against XSS injection
|
This helps to protect against XSS injection
|
||||||
"""
|
"""
|
||||||
|
|
||||||
url = reverse('api-part-category-detail', kwargs={'pk': 1})
|
url = reverse('api-part-category-detail', kwargs={'pk': 1})
|
||||||
|
|
||||||
# Invalid values containing tags
|
# Invalid values containing tags
|
||||||
@ -258,7 +255,6 @@ class PartCategoryAPITest(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
def test_invisible_chars(self):
|
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})
|
url = reverse('api-part-category-detail', kwargs={'pk': 1})
|
||||||
|
|
||||||
values = [
|
values = [
|
||||||
@ -395,7 +391,6 @@ class PartCategoryAPITest(InvenTreeAPITestCase):
|
|||||||
- Parts cannot be created in structural categories
|
- Parts cannot be created in structural categories
|
||||||
- Parts cannot be assigned to structural categories
|
- Parts cannot be assigned to structural categories
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Create our structural part category
|
# Create our structural part category
|
||||||
structural_category = PartCategory.objects.create(
|
structural_category = PartCategory.objects.create(
|
||||||
name='Structural category',
|
name='Structural category',
|
||||||
@ -443,7 +438,6 @@ class PartCategoryAPITest(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
def test_path_detail(self):
|
def test_path_detail(self):
|
||||||
"""Test path_detail information"""
|
"""Test path_detail information"""
|
||||||
|
|
||||||
url = reverse('api-part-category-detail', kwargs={'pk': 5})
|
url = reverse('api-part-category-detail', kwargs={'pk': 5})
|
||||||
|
|
||||||
# First, request without path detail
|
# First, request without path detail
|
||||||
@ -718,7 +712,6 @@ class PartAPITest(PartAPITestBase):
|
|||||||
|
|
||||||
def test_filter_by_in_bom(self):
|
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')
|
url = reverse('api-part-list')
|
||||||
|
|
||||||
response = self.get(
|
response = self.get(
|
||||||
@ -755,7 +748,6 @@ class PartAPITest(PartAPITestBase):
|
|||||||
|
|
||||||
def test_filter_by_convert(self):
|
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)
|
category = PartCategory.objects.get(pk=3)
|
||||||
|
|
||||||
# First, construct a set of template / variant parts
|
# First, construct a set of template / variant parts
|
||||||
@ -1207,7 +1199,6 @@ class PartCreationTests(PartAPITestBase):
|
|||||||
|
|
||||||
def submit(stock_data, expected_code=None):
|
def submit(stock_data, expected_code=None):
|
||||||
"""Helper function for submitting with initial stock data"""
|
"""Helper function for submitting with initial stock data"""
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
'category': 1,
|
'category': 1,
|
||||||
'name': "My lil' test part",
|
'name': "My lil' test part",
|
||||||
@ -1252,7 +1243,6 @@ class PartCreationTests(PartAPITestBase):
|
|||||||
|
|
||||||
def submit(supplier_data, expected_code=400):
|
def submit(supplier_data, expected_code=400):
|
||||||
"""Helper function for submitting with supplier data"""
|
"""Helper function for submitting with supplier data"""
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
'name': 'My test part',
|
'name': 'My test part',
|
||||||
'description': 'A test part thingy',
|
'description': 'A test part thingy',
|
||||||
@ -1355,7 +1345,6 @@ class PartCreationTests(PartAPITestBase):
|
|||||||
|
|
||||||
def test_duplication(self):
|
def test_duplication(self):
|
||||||
"""Test part duplication options"""
|
"""Test part duplication options"""
|
||||||
|
|
||||||
# Run a matrix of tests
|
# Run a matrix of tests
|
||||||
for bom in [True, False]:
|
for bom in [True, False]:
|
||||||
for img in [True, False]:
|
for img in [True, False]:
|
||||||
@ -1384,7 +1373,6 @@ class PartCreationTests(PartAPITestBase):
|
|||||||
|
|
||||||
def test_category_parameters(self):
|
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)
|
cat = PartCategory.objects.get(pk=1)
|
||||||
|
|
||||||
# Add some parameter template to the parent category
|
# Add some parameter template to the parent category
|
||||||
@ -1684,7 +1672,6 @@ class PartDetailTests(PartAPITestBase):
|
|||||||
|
|
||||||
def test_path_detail(self):
|
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(
|
response = self.get(
|
||||||
reverse('api-part-detail', kwargs={'pk': 1}),
|
reverse('api-part-detail', kwargs={'pk': 1}),
|
||||||
{
|
{
|
||||||
@ -1702,7 +1689,6 @@ class PartListTests(PartAPITestBase):
|
|||||||
|
|
||||||
def test_query_count(self):
|
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 = [
|
queries = [
|
||||||
{'limit': 1},
|
{'limit': 1},
|
||||||
{'limit': 10},
|
{'limit': 10},
|
||||||
@ -1768,7 +1754,6 @@ class PartNotesTests(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
def test_long_notes(self):
|
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
|
# Ensure that we cannot upload a very long piece of text
|
||||||
url = reverse('api-part-detail', kwargs={'pk': 1})
|
url = reverse('api-part-detail', kwargs={'pk': 1})
|
||||||
|
|
||||||
@ -1784,7 +1769,6 @@ class PartNotesTests(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
def test_multiline_formatting(self):
|
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})
|
url = reverse('api-part-detail', kwargs={'pk': 1})
|
||||||
|
|
||||||
notes = """
|
notes = """
|
||||||
@ -1828,12 +1812,10 @@ class PartPricingDetailTests(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
def url(self, pk):
|
def url(self, pk):
|
||||||
"""Construct a pricing URL"""
|
"""Construct a pricing URL"""
|
||||||
|
|
||||||
return reverse('api-part-pricing', kwargs={'pk': pk})
|
return reverse('api-part-pricing', kwargs={'pk': pk})
|
||||||
|
|
||||||
def test_pricing_detail(self):
|
def test_pricing_detail(self):
|
||||||
"""Test an empty pricing detail"""
|
"""Test an empty pricing detail"""
|
||||||
|
|
||||||
response = self.get(
|
response = self.get(
|
||||||
self.url(1),
|
self.url(1),
|
||||||
expected_code=200
|
expected_code=200
|
||||||
@ -2100,7 +2082,6 @@ class PartAPIAggregationTest(InvenTreeAPITestCase):
|
|||||||
This queryset annotation takes into account any outstanding line items for active orders,
|
This queryset annotation takes into account any outstanding line items for active orders,
|
||||||
and should also use the 'pack_size' of the supplier part objects.
|
and should also use the 'pack_size' of the supplier part objects.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
supplier = Company.objects.create(
|
supplier = Company.objects.create(
|
||||||
name='Paint Supplies',
|
name='Paint Supplies',
|
||||||
description='A supplier of paints',
|
description='A supplier of paints',
|
||||||
@ -2284,7 +2265,6 @@ class BomItemTest(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
def test_bom_list_search(self):
|
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')
|
url = reverse('api-bom-list')
|
||||||
|
|
||||||
response = self.get(url, expected_code=200)
|
response = self.get(url, expected_code=200)
|
||||||
@ -2328,7 +2308,6 @@ class BomItemTest(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
def test_bom_list_ordering(self):
|
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')
|
url = reverse('api-bom-list')
|
||||||
|
|
||||||
# Order by increasing quantity
|
# Order by increasing quantity
|
||||||
@ -2698,7 +2677,6 @@ class PartAttachmentTest(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
def test_add_attachment(self):
|
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')
|
url = reverse('api-part-attachment-list')
|
||||||
|
|
||||||
# Upload without permission
|
# Upload without permission
|
||||||
@ -2795,7 +2773,6 @@ class PartInternalPriceBreakTest(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
def test_create_price_breaks(self):
|
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')
|
url = reverse('api-part-internal-price-list')
|
||||||
|
|
||||||
breaks = [
|
breaks = [
|
||||||
@ -2859,7 +2836,6 @@ class PartStocktakeTest(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
def test_list_endpoint(self):
|
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')
|
url = reverse('api-part-stocktake-list')
|
||||||
|
|
||||||
self.assignRole('part.view')
|
self.assignRole('part.view')
|
||||||
@ -2911,7 +2887,6 @@ class PartStocktakeTest(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
def test_create_stocktake(self):
|
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')
|
url = reverse('api-part-stocktake-list')
|
||||||
|
|
||||||
self.assignRole('stocktake.add')
|
self.assignRole('stocktake.add')
|
||||||
@ -2948,7 +2923,6 @@ class PartStocktakeTest(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
Note that only 'staff' users can perform these actions.
|
Note that only 'staff' users can perform these actions.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
p = Part.objects.all().first()
|
p = Part.objects.all().first()
|
||||||
|
|
||||||
st = PartStocktake.objects.create(part=p, quantity=10)
|
st = PartStocktake.objects.create(part=p, quantity=10)
|
||||||
@ -2989,7 +2963,6 @@ class PartStocktakeTest(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
def test_report_list(self):
|
def test_report_list(self):
|
||||||
"""Test for PartStocktakeReport list endpoint"""
|
"""Test for PartStocktakeReport list endpoint"""
|
||||||
|
|
||||||
from part.stocktake import generate_stocktake_report
|
from part.stocktake import generate_stocktake_report
|
||||||
|
|
||||||
# Initially, no stocktake records are available
|
# Initially, no stocktake records are available
|
||||||
@ -3021,7 +2994,6 @@ class PartStocktakeTest(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
def test_report_generate(self):
|
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')
|
url = reverse('api-part-stocktake-report-generate')
|
||||||
|
|
||||||
# Permission denied, initially
|
# Permission denied, initially
|
||||||
@ -3064,7 +3036,6 @@ class PartMetadataAPITest(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
def metatester(self, apikey, model):
|
def metatester(self, apikey, model):
|
||||||
"""Generic tester"""
|
"""Generic tester"""
|
||||||
|
|
||||||
modeldata = model.objects.first()
|
modeldata = model.objects.first()
|
||||||
|
|
||||||
# Useless test unless a model object is found
|
# Useless test unless a model object is found
|
||||||
@ -3093,7 +3064,6 @@ class PartMetadataAPITest(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
def test_metadata(self):
|
def test_metadata(self):
|
||||||
"""Test all endpoints"""
|
"""Test all endpoints"""
|
||||||
|
|
||||||
for apikey, model in {
|
for apikey, model in {
|
||||||
'api-part-category-parameter-metadata': PartCategoryParameterTemplate,
|
'api-part-category-parameter-metadata': PartCategoryParameterTemplate,
|
||||||
'api-part-category-metadata': PartCategory,
|
'api-part-category-metadata': PartCategory,
|
||||||
@ -3113,7 +3083,6 @@ class PartSchedulingTest(PartAPITestBase):
|
|||||||
|
|
||||||
def test_get_schedule(self):
|
def test_get_schedule(self):
|
||||||
"""Test that the scheduling endpoint returns OK"""
|
"""Test that the scheduling endpoint returns OK"""
|
||||||
|
|
||||||
part_ids = [
|
part_ids = [
|
||||||
1, 3, 100, 101,
|
1, 3, 100, 101,
|
||||||
]
|
]
|
||||||
|
@ -202,7 +202,6 @@ class BomItemTest(TestCase):
|
|||||||
|
|
||||||
def test_consumable(self):
|
def test_consumable(self):
|
||||||
"""Tests for the 'consumable' BomItem field"""
|
"""Tests for the 'consumable' BomItem field"""
|
||||||
|
|
||||||
# Create an assembly part
|
# Create an assembly part
|
||||||
assembly = Part.objects.create(name="An assembly", description="Made with parts", assembly=True)
|
assembly = Part.objects.create(name="An assembly", description="Made with parts", assembly=True)
|
||||||
|
|
||||||
|
@ -22,7 +22,6 @@ class CategoryTest(TestCase):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
"""Extract some interesting categories for time-saving"""
|
"""Extract some interesting categories for time-saving"""
|
||||||
|
|
||||||
super().setUpTestData()
|
super().setUpTestData()
|
||||||
|
|
||||||
cls.electronics = PartCategory.objects.get(name='Electronics')
|
cls.electronics = PartCategory.objects.get(name='Electronics')
|
||||||
@ -68,7 +67,6 @@ class CategoryTest(TestCase):
|
|||||||
|
|
||||||
def test_path_string(self):
|
def test_path_string(self):
|
||||||
"""Test that the category path string works correctly."""
|
"""Test that the category path string works correctly."""
|
||||||
|
|
||||||
# Note that due to data migrations, these fields need to be saved first
|
# Note that due to data migrations, these fields need to be saved first
|
||||||
self.resistors.save()
|
self.resistors.save()
|
||||||
self.transceivers.save()
|
self.transceivers.save()
|
||||||
@ -137,7 +135,6 @@ class CategoryTest(TestCase):
|
|||||||
|
|
||||||
def test_part_count(self):
|
def test_part_count(self):
|
||||||
"""Test that the Category part count works."""
|
"""Test that the Category part count works."""
|
||||||
|
|
||||||
self.assertEqual(self.fasteners.partcount(), 2)
|
self.assertEqual(self.fasteners.partcount(), 2)
|
||||||
self.assertEqual(self.capacitors.partcount(), 1)
|
self.assertEqual(self.capacitors.partcount(), 1)
|
||||||
|
|
||||||
@ -203,7 +200,6 @@ class CategoryTest(TestCase):
|
|||||||
|
|
||||||
def test_default_locations(self):
|
def test_default_locations(self):
|
||||||
"""Test traversal for default locations."""
|
"""Test traversal for default locations."""
|
||||||
|
|
||||||
self.assertIsNotNone(self.fasteners.default_location)
|
self.assertIsNotNone(self.fasteners.default_location)
|
||||||
self.fasteners.default_location.save()
|
self.fasteners.default_location.save()
|
||||||
self.assertEqual(str(self.fasteners.default_location), 'Office/Drawer_1 - In my desk')
|
self.assertEqual(str(self.fasteners.default_location), 'Office/Drawer_1 - In my desk')
|
||||||
|
@ -56,7 +56,6 @@ class TestBomItemMigrations(MigratorTestCase):
|
|||||||
|
|
||||||
def prepare(self):
|
def prepare(self):
|
||||||
"""Create initial dataset"""
|
"""Create initial dataset"""
|
||||||
|
|
||||||
Part = self.old_state.apps.get_model('part', 'part')
|
Part = self.old_state.apps.get_model('part', 'part')
|
||||||
BomItem = self.old_state.apps.get_model('part', 'bomitem')
|
BomItem = self.old_state.apps.get_model('part', 'bomitem')
|
||||||
|
|
||||||
@ -75,7 +74,6 @@ class TestBomItemMigrations(MigratorTestCase):
|
|||||||
|
|
||||||
def test_validated_field(self):
|
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')
|
BomItem = self.new_state.apps.get_model('part', 'bomitem')
|
||||||
|
|
||||||
self.assertEqual(BomItem.objects.count(), 2)
|
self.assertEqual(BomItem.objects.count(), 2)
|
||||||
@ -92,7 +90,6 @@ class TestParameterMigrations(MigratorTestCase):
|
|||||||
|
|
||||||
def prepare(self):
|
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')
|
Part = self.old_state.apps.get_model('part', 'part')
|
||||||
PartParameter = self.old_state.apps.get_model('part', 'partparameter')
|
PartParameter = self.old_state.apps.get_model('part', 'partparameter')
|
||||||
PartParameterTemlate = self.old_state.apps.get_model('part', 'partparametertemplate')
|
PartParameterTemlate = self.old_state.apps.get_model('part', 'partparametertemplate')
|
||||||
@ -121,7 +118,6 @@ class TestParameterMigrations(MigratorTestCase):
|
|||||||
|
|
||||||
def test_data_migration(self):
|
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')
|
Part = self.new_state.apps.get_model('part', 'part')
|
||||||
PartParameter = self.new_state.apps.get_model('part', 'partparameter')
|
PartParameter = self.new_state.apps.get_model('part', 'partparameter')
|
||||||
PartParameterTemlate = self.new_state.apps.get_model('part', 'partparametertemplate')
|
PartParameterTemlate = self.new_state.apps.get_model('part', 'partparametertemplate')
|
||||||
@ -164,7 +160,6 @@ class PartUnitsMigrationTest(MigratorTestCase):
|
|||||||
|
|
||||||
def prepare(self):
|
def prepare(self):
|
||||||
"""Prepare some parts with units"""
|
"""Prepare some parts with units"""
|
||||||
|
|
||||||
Part = self.old_state.apps.get_model('part', 'part')
|
Part = self.old_state.apps.get_model('part', 'part')
|
||||||
|
|
||||||
units = ['mm', 'INCH', '', '%']
|
units = ['mm', 'INCH', '', '%']
|
||||||
@ -177,7 +172,6 @@ class PartUnitsMigrationTest(MigratorTestCase):
|
|||||||
|
|
||||||
def test_units_migration(self):
|
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 = self.new_state.apps.get_model('part', 'part')
|
||||||
|
|
||||||
part_1 = Part.objects.get(name='Part 1')
|
part_1 = Part.objects.get(name='Part 1')
|
||||||
@ -202,7 +196,6 @@ class TestPartParameterTemplateMigration(MigratorTestCase):
|
|||||||
|
|
||||||
def prepare(self):
|
def prepare(self):
|
||||||
"""Prepare some parts with units"""
|
"""Prepare some parts with units"""
|
||||||
|
|
||||||
PartParameterTemplate = self.old_state.apps.get_model('part', 'partparametertemplate')
|
PartParameterTemplate = self.old_state.apps.get_model('part', 'partparametertemplate')
|
||||||
|
|
||||||
# Create a test template
|
# Create a test template
|
||||||
@ -217,7 +210,6 @@ class TestPartParameterTemplateMigration(MigratorTestCase):
|
|||||||
|
|
||||||
def test_units_migration(self):
|
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')
|
PartParameterTemplate = self.new_state.apps.get_model('part', 'partparametertemplate')
|
||||||
|
|
||||||
template = PartParameterTemplate.objects.get(name='Template 1')
|
template = PartParameterTemplate.objects.get(name='Template 1')
|
||||||
|
@ -66,7 +66,6 @@ class TestParams(TestCase):
|
|||||||
|
|
||||||
def test_get_parameter(self):
|
def test_get_parameter(self):
|
||||||
"""Test the Part.get_parameter method"""
|
"""Test the Part.get_parameter method"""
|
||||||
|
|
||||||
prt = Part.objects.get(pk=3)
|
prt = Part.objects.get(pk=3)
|
||||||
|
|
||||||
# Check that we can get a parameter by name
|
# Check that we can get a parameter by name
|
||||||
@ -119,7 +118,6 @@ class ParameterTests(TestCase):
|
|||||||
|
|
||||||
def test_choice_validation(self):
|
def test_choice_validation(self):
|
||||||
"""Test that parameter choices are correctly validated"""
|
"""Test that parameter choices are correctly validated"""
|
||||||
|
|
||||||
template = PartParameterTemplate.objects.create(
|
template = PartParameterTemplate.objects.create(
|
||||||
name='My Template',
|
name='My Template',
|
||||||
description='A template with choices',
|
description='A template with choices',
|
||||||
@ -142,7 +140,6 @@ class ParameterTests(TestCase):
|
|||||||
|
|
||||||
def test_unit_validation(self):
|
def test_unit_validation(self):
|
||||||
"""Test validation of 'units' field for PartParameterTemplate"""
|
"""Test validation of 'units' field for PartParameterTemplate"""
|
||||||
|
|
||||||
# Test that valid units pass
|
# Test that valid units pass
|
||||||
for unit in [None, '', '%', 'mm', 'A', 'm^2', 'Pa', 'V', 'C', 'F', 'uF', 'mF', 'millifarad']:
|
for unit in [None, '', '%', 'mm', 'A', 'm^2', 'Pa', 'V', 'C', 'F', 'uF', 'mF', 'millifarad']:
|
||||||
tmp = PartParameterTemplate(name='test', units=unit)
|
tmp = PartParameterTemplate(name='test', units=unit)
|
||||||
@ -156,7 +153,6 @@ class ParameterTests(TestCase):
|
|||||||
|
|
||||||
def test_param_unit_validation(self):
|
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(
|
template = PartParameterTemplate.objects.create(
|
||||||
name='My Template',
|
name='My Template',
|
||||||
units='m',
|
units='m',
|
||||||
@ -198,7 +194,6 @@ class ParameterTests(TestCase):
|
|||||||
|
|
||||||
def test_param_unit_conversion(self):
|
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(
|
template = PartParameterTemplate.objects.create(
|
||||||
name='My Template',
|
name='My Template',
|
||||||
units='m',
|
units='m',
|
||||||
@ -263,7 +258,6 @@ class PartParameterTest(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
def test_param_template_validation(self):
|
def test_param_template_validation(self):
|
||||||
"""Test that part parameter template validation routines work correctly."""
|
"""Test that part parameter template validation routines work correctly."""
|
||||||
|
|
||||||
# Checkbox parameter cannot have "units" specified
|
# Checkbox parameter cannot have "units" specified
|
||||||
with self.assertRaises(django_exceptions.ValidationError):
|
with self.assertRaises(django_exceptions.ValidationError):
|
||||||
template = PartParameterTemplate(
|
template = PartParameterTemplate(
|
||||||
|
@ -137,7 +137,6 @@ class PartTest(TestCase):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
"""Create some Part instances as part of init routine"""
|
"""Create some Part instances as part of init routine"""
|
||||||
|
|
||||||
super().setUpTestData()
|
super().setUpTestData()
|
||||||
|
|
||||||
cls.r1 = Part.objects.get(name='R_2K2_0805')
|
cls.r1 = Part.objects.get(name='R_2K2_0805')
|
||||||
@ -149,7 +148,6 @@ class PartTest(TestCase):
|
|||||||
|
|
||||||
def test_barcode_mixin(self):
|
def test_barcode_mixin(self):
|
||||||
"""Test the barcode mixin functionality"""
|
"""Test the barcode mixin functionality"""
|
||||||
|
|
||||||
self.assertEqual(Part.barcode_model_type(), 'part')
|
self.assertEqual(Part.barcode_model_type(), 'part')
|
||||||
|
|
||||||
p = Part.objects.get(pk=1)
|
p = Part.objects.get(pk=1)
|
||||||
@ -292,7 +290,6 @@ class PartTest(TestCase):
|
|||||||
|
|
||||||
def test_related(self):
|
def test_related(self):
|
||||||
"""Unit tests for the PartRelated model"""
|
"""Unit tests for the PartRelated model"""
|
||||||
|
|
||||||
# Create a part relationship
|
# Create a part relationship
|
||||||
# Count before creation
|
# Count before creation
|
||||||
countbefore = PartRelated.objects.count()
|
countbefore = PartRelated.objects.count()
|
||||||
@ -341,7 +338,6 @@ class PartTest(TestCase):
|
|||||||
|
|
||||||
def test_stocktake(self):
|
def test_stocktake(self):
|
||||||
"""Test for adding stocktake data"""
|
"""Test for adding stocktake data"""
|
||||||
|
|
||||||
# Grab a part
|
# Grab a part
|
||||||
p = Part.objects.all().first()
|
p = Part.objects.all().first()
|
||||||
|
|
||||||
@ -419,7 +415,6 @@ class PartSettingsTest(InvenTreeTestCase):
|
|||||||
|
|
||||||
def make_part(self):
|
def make_part(self):
|
||||||
"""Helper function to create a simple part."""
|
"""Helper function to create a simple part."""
|
||||||
|
|
||||||
cache.clear()
|
cache.clear()
|
||||||
|
|
||||||
part = Part.objects.create(
|
part = Part.objects.create(
|
||||||
@ -432,7 +427,6 @@ class PartSettingsTest(InvenTreeTestCase):
|
|||||||
|
|
||||||
def test_defaults(self):
|
def test_defaults(self):
|
||||||
"""Test that the default values for the part settings are correct."""
|
"""Test that the default values for the part settings are correct."""
|
||||||
|
|
||||||
cache.clear()
|
cache.clear()
|
||||||
|
|
||||||
self.assertTrue(part.settings.part_component_default())
|
self.assertTrue(part.settings.part_component_default())
|
||||||
@ -442,7 +436,6 @@ class PartSettingsTest(InvenTreeTestCase):
|
|||||||
|
|
||||||
def test_initial(self):
|
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()
|
cache.clear()
|
||||||
|
|
||||||
part = self.make_part()
|
part = self.make_part()
|
||||||
|
@ -20,7 +20,6 @@ class PartPricingTests(InvenTreeTestCase):
|
|||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
"""Setup routines"""
|
"""Setup routines"""
|
||||||
|
|
||||||
super().setUp()
|
super().setUp()
|
||||||
|
|
||||||
self.generate_exchange_rates()
|
self.generate_exchange_rates()
|
||||||
@ -37,7 +36,6 @@ class PartPricingTests(InvenTreeTestCase):
|
|||||||
|
|
||||||
def create_price_breaks(self):
|
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)
|
# First supplier part (CAD)
|
||||||
self.supplier_1 = company.models.Company.objects.create(
|
self.supplier_1 = company.models.Company.objects.create(
|
||||||
name='Supplier 1',
|
name='Supplier 1',
|
||||||
@ -104,7 +102,6 @@ class PartPricingTests(InvenTreeTestCase):
|
|||||||
|
|
||||||
def test_pricing_data(self):
|
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
|
# Initially there is no associated Pricing data
|
||||||
with self.assertRaises(ObjectDoesNotExist):
|
with self.assertRaises(ObjectDoesNotExist):
|
||||||
pricing = self.part.pricing_data
|
pricing = self.part.pricing_data
|
||||||
@ -130,7 +127,6 @@ class PartPricingTests(InvenTreeTestCase):
|
|||||||
|
|
||||||
def test_simple(self):
|
def test_simple(self):
|
||||||
"""Tests for hard-coded values"""
|
"""Tests for hard-coded values"""
|
||||||
|
|
||||||
pricing = self.part.pricing
|
pricing = self.part.pricing
|
||||||
|
|
||||||
# Add internal pricing
|
# Add internal pricing
|
||||||
@ -162,7 +158,6 @@ class PartPricingTests(InvenTreeTestCase):
|
|||||||
|
|
||||||
def test_supplier_part_pricing(self):
|
def test_supplier_part_pricing(self):
|
||||||
"""Test for supplier part pricing"""
|
"""Test for supplier part pricing"""
|
||||||
|
|
||||||
pricing = self.part.pricing
|
pricing = self.part.pricing
|
||||||
|
|
||||||
# Initially, no information (not yet calculated)
|
# Initially, no information (not yet calculated)
|
||||||
@ -189,7 +184,6 @@ class PartPricingTests(InvenTreeTestCase):
|
|||||||
|
|
||||||
def test_internal_pricing(self):
|
def test_internal_pricing(self):
|
||||||
"""Tests for internal price breaks"""
|
"""Tests for internal price breaks"""
|
||||||
|
|
||||||
# Ensure internal pricing is enabled
|
# Ensure internal pricing is enabled
|
||||||
common.models.InvenTreeSetting.set_setting('PART_INTERNAL_PRICE', True, None)
|
common.models.InvenTreeSetting.set_setting('PART_INTERNAL_PRICE', True, None)
|
||||||
|
|
||||||
@ -225,7 +219,6 @@ class PartPricingTests(InvenTreeTestCase):
|
|||||||
|
|
||||||
def test_stock_item_pricing(self):
|
def test_stock_item_pricing(self):
|
||||||
"""Test for stock item pricing data"""
|
"""Test for stock item pricing data"""
|
||||||
|
|
||||||
# Create a part
|
# Create a part
|
||||||
p = part.models.Part.objects.create(
|
p = part.models.Part.objects.create(
|
||||||
name='Test part for pricing',
|
name='Test part for pricing',
|
||||||
@ -273,7 +266,6 @@ class PartPricingTests(InvenTreeTestCase):
|
|||||||
|
|
||||||
def test_bom_pricing(self):
|
def test_bom_pricing(self):
|
||||||
"""Unit test for BOM pricing calculations"""
|
"""Unit test for BOM pricing calculations"""
|
||||||
|
|
||||||
pricing = self.part.pricing
|
pricing = self.part.pricing
|
||||||
|
|
||||||
self.assertIsNone(pricing.bom_cost_min)
|
self.assertIsNone(pricing.bom_cost_min)
|
||||||
@ -315,7 +307,6 @@ class PartPricingTests(InvenTreeTestCase):
|
|||||||
|
|
||||||
def test_purchase_pricing(self):
|
def test_purchase_pricing(self):
|
||||||
"""Unit tests for historical purchase pricing"""
|
"""Unit tests for historical purchase pricing"""
|
||||||
|
|
||||||
self.create_price_breaks()
|
self.create_price_breaks()
|
||||||
|
|
||||||
pricing = self.part.pricing
|
pricing = self.part.pricing
|
||||||
@ -380,7 +371,6 @@ class PartPricingTests(InvenTreeTestCase):
|
|||||||
|
|
||||||
def test_delete_with_pricing(self):
|
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
|
# Create some pricing data
|
||||||
self.create_price_breaks()
|
self.create_price_breaks()
|
||||||
|
|
||||||
@ -405,7 +395,6 @@ class PartPricingTests(InvenTreeTestCase):
|
|||||||
|
|
||||||
def test_delete_without_pricing(self):
|
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
|
pricing = self.part.pricing
|
||||||
|
|
||||||
self.assertIsNone(pricing.pk)
|
self.assertIsNone(pricing.pk)
|
||||||
@ -426,7 +415,6 @@ class PartPricingTests(InvenTreeTestCase):
|
|||||||
- Create PartPricing objects where there are none
|
- Create PartPricing objects where there are none
|
||||||
- Schedule pricing calculations for the newly created PartPricing objects
|
- Schedule pricing calculations for the newly created PartPricing objects
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from part.tasks import check_missing_pricing
|
from part.tasks import check_missing_pricing
|
||||||
|
|
||||||
# Create some parts
|
# Create some parts
|
||||||
@ -453,7 +441,6 @@ class PartPricingTests(InvenTreeTestCase):
|
|||||||
Essentially a series of on_delete listeners caused a new PartPricing object to be created,
|
Essentially a series of on_delete listeners caused a new PartPricing object to be created,
|
||||||
but it pointed to a Part instance which was slated to be deleted inside an atomic transaction.
|
but it pointed to a Part instance which was slated to be deleted inside an atomic transaction.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
p = part.models.Part.objects.create(
|
p = part.models.Part.objects.create(
|
||||||
name="my part",
|
name="my part",
|
||||||
description="my part description",
|
description="my part description",
|
||||||
|
@ -183,7 +183,6 @@ class PluginActivate(UpdateAPI):
|
|||||||
|
|
||||||
def perform_update(self, serializer):
|
def perform_update(self, serializer):
|
||||||
"""Activate the plugin."""
|
"""Activate the plugin."""
|
||||||
|
|
||||||
serializer.save()
|
serializer.save()
|
||||||
|
|
||||||
|
|
||||||
|
@ -101,7 +101,6 @@ class BarcodeAssign(APIView):
|
|||||||
|
|
||||||
Checks inputs and assign barcode (hash) to StockItem.
|
Checks inputs and assign barcode (hash) to StockItem.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
data = request.data
|
data = request.data
|
||||||
|
|
||||||
barcode_data = data.get('barcode', None)
|
barcode_data = data.get('barcode', None)
|
||||||
@ -180,7 +179,6 @@ class BarcodeUnassign(APIView):
|
|||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
"""Respond to a barcode unassign POST request"""
|
"""Respond to a barcode unassign POST request"""
|
||||||
|
|
||||||
# The following database models support assignment of third-party barcodes
|
# The following database models support assignment of third-party barcodes
|
||||||
supported_models = InvenTreeInternalBarcodePlugin.get_supported_barcode_models()
|
supported_models = InvenTreeInternalBarcodePlugin.get_supported_barcode_models()
|
||||||
|
|
||||||
|
@ -35,5 +35,4 @@ class BarcodeMixin:
|
|||||||
|
|
||||||
Default return value is None
|
Default return value is None
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
@ -99,7 +99,6 @@ def process_event(plugin_slug, event, *args, **kwargs):
|
|||||||
This function is run by the background worker process.
|
This function is run by the background worker process.
|
||||||
This function may queue multiple functions to be handled by the background worker.
|
This function may queue multiple functions to be handled by the background worker.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
plugin = registry.plugins.get(plugin_slug, None)
|
plugin = registry.plugins.get(plugin_slug, None)
|
||||||
|
|
||||||
if plugin is None: # pragma: no cover
|
if plugin is None: # pragma: no cover
|
||||||
|
@ -14,7 +14,6 @@ class EventMixin:
|
|||||||
|
|
||||||
Return true if you're interested in the given event, false if not.
|
Return true if you're interested in the given event, false if not.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Default implementation always returns true (backwards compatibility)
|
# Default implementation always returns true (backwards compatibility)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -144,7 +144,6 @@ class PanelMixin:
|
|||||||
Returns:
|
Returns:
|
||||||
Array of panels
|
Array of panels
|
||||||
"""
|
"""
|
||||||
|
|
||||||
panels = []
|
panels = []
|
||||||
|
|
||||||
# Construct an updated context object for template rendering
|
# Construct an updated context object for template rendering
|
||||||
|
@ -55,7 +55,6 @@ class LabelPrintingMixin:
|
|||||||
|
|
||||||
def render_to_png(self, label: LabelTemplate, request=None, **kwargs):
|
def render_to_png(self, label: LabelTemplate, request=None, **kwargs):
|
||||||
"""Render this label to PNG format"""
|
"""Render this label to PNG format"""
|
||||||
|
|
||||||
# Check if pdf data is provided
|
# Check if pdf data is provided
|
||||||
pdf_data = kwargs.get('pdf_data', None)
|
pdf_data = kwargs.get('pdf_data', None)
|
||||||
|
|
||||||
@ -85,7 +84,6 @@ class LabelPrintingMixin:
|
|||||||
The default implementation simply calls print_label() for each label, producing multiple single label output "jobs"
|
The default implementation simply calls print_label() for each label, producing multiple single label output "jobs"
|
||||||
but this can be overridden by the particular plugin.
|
but this can be overridden by the particular plugin.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
user = request.user
|
user = request.user
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
@ -152,7 +150,6 @@ class LabelPrintingMixin:
|
|||||||
|
|
||||||
Offloads a call to the 'print_label' method (of this plugin) to a background worker.
|
Offloads a call to the 'print_label' method (of this plugin) to a background worker.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Exclude the 'pdf_file' object - cannot be pickled
|
# Exclude the 'pdf_file' object - cannot be pickled
|
||||||
kwargs.pop('pdf_file', None)
|
kwargs.pop('pdf_file', None)
|
||||||
|
|
||||||
|
@ -30,12 +30,10 @@ class InvenTreeInternalBarcodePlugin(BarcodeMixin, InvenTreePlugin):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def get_supported_barcode_models():
|
def get_supported_barcode_models():
|
||||||
"""Returns a list of database models which support barcode functionality"""
|
"""Returns a list of database models which support barcode functionality"""
|
||||||
|
|
||||||
return getModelsWithMixin(InvenTreeBarcodeMixin)
|
return getModelsWithMixin(InvenTreeBarcodeMixin)
|
||||||
|
|
||||||
def format_matched_response(self, label, model, instance):
|
def format_matched_response(self, label, model, instance):
|
||||||
"""Format a response for the scanned data"""
|
"""Format a response for the scanned data"""
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
'pk': instance.pk
|
'pk': instance.pk
|
||||||
}
|
}
|
||||||
@ -65,7 +63,6 @@ class InvenTreeInternalBarcodePlugin(BarcodeMixin, InvenTreePlugin):
|
|||||||
|
|
||||||
Here we are looking for a dict object which contains a reference to a particular InvenTree database object
|
Here we are looking for a dict object which contains a reference to a particular InvenTree database object
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Create hash from raw barcode data
|
# Create hash from raw barcode data
|
||||||
barcode_hash = hash_barcode(barcode_data)
|
barcode_hash = hash_barcode(barcode_data)
|
||||||
|
|
||||||
|
@ -45,7 +45,6 @@ class TestInvenTreeBarcode(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
def assign(self, data, expected_code=None):
|
def assign(self, data, expected_code=None):
|
||||||
"""Perform a 'barcode assign' request"""
|
"""Perform a 'barcode assign' request"""
|
||||||
|
|
||||||
return self.post(
|
return self.post(
|
||||||
reverse('api-barcode-link'),
|
reverse('api-barcode-link'),
|
||||||
data=data,
|
data=data,
|
||||||
@ -54,7 +53,6 @@ class TestInvenTreeBarcode(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
def unassign(self, data, expected_code=None):
|
def unassign(self, data, expected_code=None):
|
||||||
"""Perform a 'barcode unassign' request"""
|
"""Perform a 'barcode unassign' request"""
|
||||||
|
|
||||||
return self.post(
|
return self.post(
|
||||||
reverse('api-barcode-unlink'),
|
reverse('api-barcode-unlink'),
|
||||||
data=data,
|
data=data,
|
||||||
@ -63,7 +61,6 @@ class TestInvenTreeBarcode(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
def scan(self, data, expected_code=None):
|
def scan(self, data, expected_code=None):
|
||||||
"""Perform a 'scan' operation"""
|
"""Perform a 'scan' operation"""
|
||||||
|
|
||||||
return self.post(
|
return self.post(
|
||||||
reverse('api-barcode-scan'),
|
reverse('api-barcode-scan'),
|
||||||
data=data,
|
data=data,
|
||||||
@ -72,7 +69,6 @@ class TestInvenTreeBarcode(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
def test_unassign_errors(self):
|
def test_unassign_errors(self):
|
||||||
"""Test various error conditions for the barcode unassign endpoint"""
|
"""Test various error conditions for the barcode unassign endpoint"""
|
||||||
|
|
||||||
# Fail without any fields provided
|
# Fail without any fields provided
|
||||||
response = self.unassign(
|
response = self.unassign(
|
||||||
{},
|
{},
|
||||||
@ -114,7 +110,6 @@ class TestInvenTreeBarcode(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
def test_assign_to_stock_item(self):
|
def test_assign_to_stock_item(self):
|
||||||
"""Test that we can assign a unique barcode to a StockItem object"""
|
"""Test that we can assign a unique barcode to a StockItem object"""
|
||||||
|
|
||||||
# Test without providing any fields
|
# Test without providing any fields
|
||||||
response = self.assign(
|
response = self.assign(
|
||||||
{
|
{
|
||||||
@ -198,7 +193,6 @@ class TestInvenTreeBarcode(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
def test_assign_to_part(self):
|
def test_assign_to_part(self):
|
||||||
"""Test that we can assign a unique barcode to a Part instance"""
|
"""Test that we can assign a unique barcode to a Part instance"""
|
||||||
|
|
||||||
barcode = 'xyz-123'
|
barcode = 'xyz-123'
|
||||||
|
|
||||||
self.assignRole('part.change')
|
self.assignRole('part.change')
|
||||||
@ -281,7 +275,6 @@ class TestInvenTreeBarcode(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
def test_assign_to_location(self):
|
def test_assign_to_location(self):
|
||||||
"""Test that we can assign a unique barcode to a StockLocation instance"""
|
"""Test that we can assign a unique barcode to a StockLocation instance"""
|
||||||
|
|
||||||
barcode = '555555555555555555555555'
|
barcode = '555555555555555555555555'
|
||||||
|
|
||||||
# Assign random barcode data to a StockLocation instance
|
# Assign random barcode data to a StockLocation instance
|
||||||
@ -338,7 +331,6 @@ class TestInvenTreeBarcode(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
def test_scan_third_party(self):
|
def test_scan_third_party(self):
|
||||||
"""Test scanning of third-party barcodes"""
|
"""Test scanning of third-party barcodes"""
|
||||||
|
|
||||||
# First scanned barcode is for a 'third-party' barcode (which does not exist)
|
# First scanned barcode is for a 'third-party' barcode (which does not exist)
|
||||||
response = self.scan({'barcode': 'blbla=10008'}, expected_code=400)
|
response = self.scan({'barcode': 'blbla=10008'}, expected_code=400)
|
||||||
self.assertEqual(response.data['error'], 'No match found for barcode data')
|
self.assertEqual(response.data['error'], 'No match found for barcode data')
|
||||||
@ -367,7 +359,6 @@ class TestInvenTreeBarcode(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
def test_scan_inventree(self):
|
def test_scan_inventree(self):
|
||||||
"""Test scanning of first-party barcodes"""
|
"""Test scanning of first-party barcodes"""
|
||||||
|
|
||||||
# Scan a StockItem object (which does not exist)
|
# Scan a StockItem object (which does not exist)
|
||||||
response = self.scan(
|
response = self.scan(
|
||||||
{
|
{
|
||||||
|
@ -133,7 +133,6 @@ class InvenTreeCoreNotificationsPlugin(SettingsContentMixin, SettingsMixin, Inve
|
|||||||
|
|
||||||
def send_bulk(self):
|
def send_bulk(self):
|
||||||
"""Send the notifications out via slack."""
|
"""Send the notifications out via slack."""
|
||||||
|
|
||||||
instance = registry.plugins.get(self.get_plugin().NAME.lower())
|
instance = registry.plugins.get(self.get_plugin().NAME.lower())
|
||||||
url = instance.get_setting('NOTIFICATION_SLACK_URL')
|
url = instance.get_setting('NOTIFICATION_SLACK_URL')
|
||||||
|
|
||||||
|
@ -26,7 +26,6 @@ class InvenTreeCurrencyExchange(APICallMixin, CurrencyExchangeMixin, InvenTreePl
|
|||||||
|
|
||||||
def update_exchange_rates(self, base_currency: str, symbols: list[str]) -> dict:
|
def update_exchange_rates(self, base_currency: str, symbols: list[str]) -> dict:
|
||||||
"""Request exchange rate data from external API"""
|
"""Request exchange rate data from external API"""
|
||||||
|
|
||||||
response = self.api_call(
|
response = self.api_call(
|
||||||
'latest',
|
'latest',
|
||||||
url_args={
|
url_args={
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user