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