mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Fix common spelling mistakes (#4956)
* add codespell * first fixes * doc fixes * fix docstrings and comments * functional changes * docstrings again * and docs again * rename args * add ignore * use pre-commit for filtering instead * ups * fix typo in filter
This commit is contained in:
parent
5e2bfaa43a
commit
21ed4b2081
2
.github/ISSUE_TEMPLATE/documentation.yaml
vendored
2
.github/ISSUE_TEMPLATE/documentation.yaml
vendored
@ -10,6 +10,6 @@ body:
|
|||||||
id: repro
|
id: repro
|
||||||
attributes:
|
attributes:
|
||||||
label: Body of the issue
|
label: Body of the issue
|
||||||
description: Please provide one distinct thing to fix or a clearly defined enhancment
|
description: Please provide one distinct thing to fix or a clearly defined enhancement
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
4
.github/actions/setup/action.yaml
vendored
4
.github/actions/setup/action.yaml
vendored
@ -1,5 +1,5 @@
|
|||||||
name: 'Setup Enviroment'
|
name: 'Setup Enviroment'
|
||||||
description: 'Setup the enviroment for general InvenTree tests'
|
description: 'Setup the environment for general InvenTree tests'
|
||||||
author: 'InvenTree'
|
author: 'InvenTree'
|
||||||
inputs:
|
inputs:
|
||||||
python:
|
python:
|
||||||
@ -62,7 +62,7 @@ runs:
|
|||||||
with:
|
with:
|
||||||
node-version: ${{ env.node_version }}
|
node-version: ${{ env.node_version }}
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
- name: Intall npm packages
|
- name: Install npm packages
|
||||||
if: ${{ inputs.npm == 'true' }}
|
if: ${{ inputs.npm == 'true' }}
|
||||||
shell: bash
|
shell: bash
|
||||||
run: npm install
|
run: npm install
|
||||||
|
14
.github/workflows/qc_checks.yaml
vendored
14
.github/workflows/qc_checks.yaml
vendored
@ -49,7 +49,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # pin@v3.1.0
|
- uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # pin@v3.1.0
|
||||||
- name: Enviroment Setup
|
- name: Environment Setup
|
||||||
uses: ./.github/actions/setup
|
uses: ./.github/actions/setup
|
||||||
with:
|
with:
|
||||||
dev-install: true
|
dev-install: true
|
||||||
@ -64,7 +64,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # pin@v3.1.0
|
- uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # pin@v3.1.0
|
||||||
- name: Enviroment Setup
|
- name: Environment Setup
|
||||||
uses: ./.github/actions/setup
|
uses: ./.github/actions/setup
|
||||||
with:
|
with:
|
||||||
npm: true
|
npm: true
|
||||||
@ -139,7 +139,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # pin@v3.1.0
|
- uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # pin@v3.1.0
|
||||||
- name: Enviroment Setup
|
- name: Environment Setup
|
||||||
uses: ./.github/actions/setup
|
uses: ./.github/actions/setup
|
||||||
with:
|
with:
|
||||||
apt-dependency: gettext poppler-utils
|
apt-dependency: gettext poppler-utils
|
||||||
@ -169,7 +169,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # pin@v3.1.0
|
- uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # pin@v3.1.0
|
||||||
- name: Enviroment Setup
|
- name: Environment Setup
|
||||||
uses: ./.github/actions/setup
|
uses: ./.github/actions/setup
|
||||||
with:
|
with:
|
||||||
install: true
|
install: true
|
||||||
@ -191,7 +191,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # pin@v3.1.0
|
- uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # pin@v3.1.0
|
||||||
- name: Enviroment Setup
|
- name: Environment Setup
|
||||||
uses: ./.github/actions/setup
|
uses: ./.github/actions/setup
|
||||||
with:
|
with:
|
||||||
apt-dependency: gettext poppler-utils
|
apt-dependency: gettext poppler-utils
|
||||||
@ -241,7 +241,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # pin@v3.1.0
|
- uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # pin@v3.1.0
|
||||||
- name: Enviroment Setup
|
- name: Environment Setup
|
||||||
uses: ./.github/actions/setup
|
uses: ./.github/actions/setup
|
||||||
with:
|
with:
|
||||||
apt-dependency: gettext poppler-utils libpq-dev
|
apt-dependency: gettext poppler-utils libpq-dev
|
||||||
@ -286,7 +286,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # pin@v3.1.0
|
- uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # pin@v3.1.0
|
||||||
- name: Enviroment Setup
|
- name: Environment Setup
|
||||||
uses: ./.github/actions/setup
|
uses: ./.github/actions/setup
|
||||||
with:
|
with:
|
||||||
apt-dependency: gettext poppler-utils libmysqlclient-dev
|
apt-dependency: gettext poppler-utils libmysqlclient-dev
|
||||||
|
@ -44,3 +44,13 @@ repos:
|
|||||||
rev: v1.29.0
|
rev: v1.29.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: djlint-django
|
- id: djlint-django
|
||||||
|
- repo: https://github.com/codespell-project/codespell
|
||||||
|
rev: v2.2.4
|
||||||
|
hooks:
|
||||||
|
- id: codespell
|
||||||
|
exclude: >
|
||||||
|
(?x)^(
|
||||||
|
docs/docs/stylesheets/.*|
|
||||||
|
docs/docs/javascripts/.*|
|
||||||
|
docs/docs/webfonts/.*
|
||||||
|
)$
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
Hi there, thank you for your intrest in contributing!
|
Hi there, thank you for your interest in contributing!
|
||||||
Please read the contribution guidelines below, before submitting your first pull request to the InvenTree codebase.
|
Please read the contribution guidelines below, before submitting your first pull request to the InvenTree codebase.
|
||||||
|
|
||||||
## Quickstart
|
## Quickstart
|
||||||
@ -50,7 +50,7 @@ The HEAD of the "main" or "master" branch of InvenTree represents the current "l
|
|||||||
- All feature branches are merged into master
|
- All feature branches are merged into master
|
||||||
- All bug fixes are merged into master
|
- All bug fixes are merged into master
|
||||||
|
|
||||||
**No pushing to master:** New featues must be submitted as a pull request from a separate branch (one branch per feature).
|
**No pushing to master:** New features must be submitted as a pull request from a separate branch (one branch per feature).
|
||||||
|
|
||||||
### Feature Branches
|
### Feature Branches
|
||||||
|
|
||||||
@ -70,7 +70,7 @@ The HEAD of the "stable" branch represents the latest stable release code.
|
|||||||
#### Release Candidate Branches
|
#### Release Candidate Branches
|
||||||
|
|
||||||
- Release candidate branches are made from master, and merged into stable.
|
- Release candidate branches are made from master, and merged into stable.
|
||||||
- RC branches are targetted at a major/minor version e.g. "0.5"
|
- RC branches are targeted at a major/minor version e.g. "0.5"
|
||||||
- When a release candidate branch is merged into *stable*, the release is tagged
|
- When a release candidate branch is merged into *stable*, the release is tagged
|
||||||
|
|
||||||
#### Bugfix Branches
|
#### Bugfix Branches
|
||||||
@ -137,7 +137,7 @@ invoke test --runtest order
|
|||||||
|
|
||||||
## Code Style
|
## Code Style
|
||||||
|
|
||||||
Sumbitted Python code is automatically checked against PEP style guidelines. Locally you can run `invoke style` to ensure the style checks will pass, before submitting the PR.
|
Submitted Python code is automatically checked against PEP style guidelines. Locally you can run `invoke style` to ensure the style checks will pass, before submitting the PR.
|
||||||
Please write docstrings for each function and class - we follow the [google doc-style](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) for python. Docstrings for general javascript code is encouraged! Docstyles are checked by `invoke style`.
|
Please write docstrings for each function and class - we follow the [google doc-style](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) for python. Docstrings for general javascript code is encouraged! Docstyles are checked by `invoke style`.
|
||||||
|
|
||||||
### Django templates
|
### Django templates
|
||||||
|
@ -60,7 +60,7 @@ class NotFoundView(AjaxView):
|
|||||||
permission_classes = [permissions.AllowAny]
|
permission_classes = [permissions.AllowAny]
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
"""Proces an `not found` event on the API."""
|
"""Process an `not found` event on the API."""
|
||||||
data = {
|
data = {
|
||||||
'details': _('API endpoint not found'),
|
'details': _('API endpoint not found'),
|
||||||
'url': request.build_absolute_uri(),
|
'url': request.build_absolute_uri(),
|
||||||
|
@ -210,7 +210,7 @@ v64 -> 2022-07-08 : https://github.com/inventree/InvenTree/pull/3310
|
|||||||
- Allow BOM List API endpoint to be filtered by "on_order" parameter
|
- Allow BOM List API endpoint to be filtered by "on_order" parameter
|
||||||
|
|
||||||
v63 -> 2022-07-06 : https://github.com/inventree/InvenTree/pull/3301
|
v63 -> 2022-07-06 : https://github.com/inventree/InvenTree/pull/3301
|
||||||
- Allow BOM List API endpoint to be filtered by "available_stock" paramater
|
- Allow BOM List API endpoint to be filtered by "available_stock" parameter
|
||||||
|
|
||||||
v62 -> 2022-07-05 : https://github.com/inventree/InvenTree/pull/3296
|
v62 -> 2022-07-05 : https://github.com/inventree/InvenTree/pull/3296
|
||||||
- Allows search on BOM List API endpoint
|
- Allows search on BOM List API endpoint
|
||||||
|
@ -30,8 +30,8 @@ class InvenTreeConfig(AppConfig):
|
|||||||
- Checking if migrations should be run
|
- Checking if migrations should be run
|
||||||
- Cleaning up tasks
|
- Cleaning up tasks
|
||||||
- Starting regular tasks
|
- Starting regular tasks
|
||||||
- Updateing exchange rates
|
- Updating exchange rates
|
||||||
- Collecting notification mehods
|
- Collecting notification methods
|
||||||
- Adding users set in the current environment
|
- Adding users set in the current environment
|
||||||
"""
|
"""
|
||||||
if canAppAccessDatabase() or settings.TESTING_ENV:
|
if canAppAccessDatabase() or settings.TESTING_ENV:
|
||||||
@ -84,7 +84,7 @@ class InvenTreeConfig(AppConfig):
|
|||||||
minutes=task.minutes,
|
minutes=task.minutes,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Put at least one task onto the backround worker stack,
|
# Put at least one task onto the background worker stack,
|
||||||
# which will be processed as soon as the worker comes online
|
# which will be processed as soon as the worker comes online
|
||||||
InvenTree.tasks.offload_task(
|
InvenTree.tasks.offload_task(
|
||||||
InvenTree.tasks.heartbeat,
|
InvenTree.tasks.heartbeat,
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
"""Pull rendered copies of the templated.
|
"""Pull rendered copies of the templated.
|
||||||
|
|
||||||
Only used for testing the js files! - This file is omited from coverage.
|
Only used for testing the js files! - This file is omitted from coverage.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os # pragma: no cover
|
import os # pragma: no cover
|
||||||
@ -17,7 +17,7 @@ class RenderJavascriptFiles(InvenTreeTestCase): # pragma: no cover
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def download_file(self, filename, prefix):
|
def download_file(self, filename, prefix):
|
||||||
"""Function to `download`(copy) a file to a temporay firectory."""
|
"""Function to `download`(copy) a file to a temporary firectory."""
|
||||||
url = os.path.join(prefix, filename)
|
url = os.path.join(prefix, filename)
|
||||||
|
|
||||||
response = self.client.get(url)
|
response = self.client.get(url)
|
||||||
|
@ -200,7 +200,7 @@ def get_setting(env_var=None, config_key=None, default_value=None, typecast=None
|
|||||||
|
|
||||||
|
|
||||||
def get_boolean_setting(env_var=None, config_key=None, default_value=False):
|
def get_boolean_setting(env_var=None, config_key=None, default_value=False):
|
||||||
"""Helper function for retreiving a boolean configuration setting"""
|
"""Helper function for retrieving a boolean configuration setting"""
|
||||||
|
|
||||||
return is_true(get_setting(env_var, config_key, default_value))
|
return is_true(get_setting(env_var, config_key, default_value))
|
||||||
|
|
||||||
|
@ -34,7 +34,7 @@ def log_error(path):
|
|||||||
|
|
||||||
kind, info, data = sys.exc_info()
|
kind, info, data = sys.exc_info()
|
||||||
|
|
||||||
# Check if the eror is on the ignore list
|
# Check if the error is on the ignore list
|
||||||
if kind in settings.IGNORED_ERRORS:
|
if kind in settings.IGNORED_ERRORS:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -13,7 +13,7 @@ class InvenTreeSearchFilter(filters.SearchFilter):
|
|||||||
"""Return a set of search fields for the request, adjusted based on request params.
|
"""Return a set of search fields for the request, adjusted based on request params.
|
||||||
|
|
||||||
The following query params are available to 'augment' the search (in decreasing order of priority)
|
The following query params are available to 'augment' the search (in decreasing order of priority)
|
||||||
- search_regex: If True, search is perfomed on 'regex' comparison
|
- search_regex: If True, search is performed on 'regex' comparison
|
||||||
"""
|
"""
|
||||||
|
|
||||||
regex = InvenTree.helpers.str2bool(request.query_params.get('search_regex', False))
|
regex = InvenTree.helpers.str2bool(request.query_params.get('search_regex', False))
|
||||||
|
@ -91,7 +91,7 @@ def construct_format_regex(fmt_string: str) -> str:
|
|||||||
# Add a named capture group for the format entry
|
# Add a named capture group for the format entry
|
||||||
if name:
|
if name:
|
||||||
|
|
||||||
# Check if integer values are requried
|
# Check if integer values are required
|
||||||
if format.endswith('d'):
|
if format.endswith('d'):
|
||||||
chr = '\d'
|
chr = '\d'
|
||||||
else:
|
else:
|
||||||
|
@ -212,7 +212,7 @@ class RegistratonMixin:
|
|||||||
def is_open_for_signup(self, request, *args, **kwargs):
|
def is_open_for_signup(self, request, *args, **kwargs):
|
||||||
"""Check if signup is enabled in settings.
|
"""Check if signup is enabled in settings.
|
||||||
|
|
||||||
Configure the class variable `REGISTRATION_SETTING` to set which setting should be used, defualt: `LOGIN_ENABLE_REG`.
|
Configure the class variable `REGISTRATION_SETTING` to set which setting should be used, default: `LOGIN_ENABLE_REG`.
|
||||||
"""
|
"""
|
||||||
if settings.EMAIL_HOST and (InvenTreeSetting.get_setting('LOGIN_ENABLE_REG') or InvenTreeSetting.get_setting('LOGIN_ENABLE_SSO_REG')):
|
if settings.EMAIL_HOST and (InvenTreeSetting.get_setting('LOGIN_ENABLE_REG') or InvenTreeSetting.get_setting('LOGIN_ENABLE_SSO_REG')):
|
||||||
return super().is_open_for_signup(request, *args, **kwargs)
|
return super().is_open_for_signup(request, *args, **kwargs)
|
||||||
@ -253,7 +253,7 @@ class RegistratonMixin:
|
|||||||
group = Group.objects.get(id=start_group)
|
group = Group.objects.get(id=start_group)
|
||||||
user.groups.add(group)
|
user.groups.add(group)
|
||||||
except Group.DoesNotExist:
|
except Group.DoesNotExist:
|
||||||
logger.error('The setting `SIGNUP_GROUP` contains an non existant group', start_group)
|
logger.error('The setting `SIGNUP_GROUP` contains an non existent group', start_group)
|
||||||
user.save()
|
user.save()
|
||||||
return user
|
return user
|
||||||
|
|
||||||
@ -276,7 +276,7 @@ class CustomAccountAdapter(CustomUrlMixin, RegistratonMixin, OTPAdapter, Default
|
|||||||
try:
|
try:
|
||||||
result = super().send_mail(template_prefix, email, context)
|
result = super().send_mail(template_prefix, email, context)
|
||||||
except Exception:
|
except Exception:
|
||||||
# An exception ocurred while attempting to send email
|
# An exception occurred while attempting to send email
|
||||||
# Log it (for admin users) and return silently
|
# Log it (for admin users) and return silently
|
||||||
log_error('account email')
|
log_error('account email')
|
||||||
result = False
|
result = False
|
||||||
|
@ -492,7 +492,7 @@ def extract_serial_numbers(input_string, expected_quantity: int, starting_value=
|
|||||||
|
|
||||||
serial = serial.strip()
|
serial = serial.strip()
|
||||||
|
|
||||||
# Ignore blank / emtpy serials
|
# Ignore blank / empty serials
|
||||||
if len(serial) == 0:
|
if len(serial) == 0:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -36,7 +36,7 @@ def construct_absolute_url(*arg, **kwargs):
|
|||||||
This is useful when (for example) sending an email to a user with a link
|
This is useful when (for example) sending an email to a user with a link
|
||||||
to something in the InvenTree web framework.
|
to something in the InvenTree web framework.
|
||||||
A URL is constructed in the following order:
|
A URL is constructed in the following order:
|
||||||
1. If setings.SITE_URL is set (e.g. in the Django settings), use that
|
1. If settings.SITE_URL is set (e.g. in the Django settings), use that
|
||||||
2. If the InvenTree setting INVENTREE_BASE_URL is set, use that
|
2. If the InvenTree setting INVENTREE_BASE_URL is set, use that
|
||||||
3. Otherwise, use the current request URL (if available)
|
3. Otherwise, use the current request URL (if available)
|
||||||
"""
|
"""
|
||||||
@ -150,7 +150,7 @@ def download_image_from_url(remote_url, timeout=2.5):
|
|||||||
raise ValueError(_("Image size is too large"))
|
raise ValueError(_("Image size is too large"))
|
||||||
|
|
||||||
# Download the file, ensuring we do not exceed the reported size
|
# Download the file, ensuring we do not exceed the reported size
|
||||||
fo = io.BytesIO()
|
file = io.BytesIO()
|
||||||
|
|
||||||
dl_size = 0
|
dl_size = 0
|
||||||
chunk_size = 64 * 1024
|
chunk_size = 64 * 1024
|
||||||
@ -161,7 +161,7 @@ def download_image_from_url(remote_url, timeout=2.5):
|
|||||||
if dl_size > max_size:
|
if dl_size > max_size:
|
||||||
raise ValueError(_("Image download exceeded maximum size"))
|
raise ValueError(_("Image download exceeded maximum size"))
|
||||||
|
|
||||||
fo.write(chunk)
|
file.write(chunk)
|
||||||
|
|
||||||
if dl_size == 0:
|
if dl_size == 0:
|
||||||
raise ValueError(_("Remote server returned empty response"))
|
raise ValueError(_("Remote server returned empty response"))
|
||||||
@ -169,7 +169,7 @@ def download_image_from_url(remote_url, timeout=2.5):
|
|||||||
# Now, attempt to convert the downloaded data to a valid image file
|
# Now, attempt to convert the downloaded data to a valid image file
|
||||||
# img.verify() will throw an exception if the image is not valid
|
# img.verify() will throw an exception if the image is not valid
|
||||||
try:
|
try:
|
||||||
img = Image.open(fo).convert()
|
img = Image.open(file).convert()
|
||||||
img.verify()
|
img.verify()
|
||||||
except Exception:
|
except Exception:
|
||||||
raise TypeError(_("Supplied URL is not a valid image file"))
|
raise TypeError(_("Supplied URL is not a valid image file"))
|
||||||
|
@ -23,8 +23,8 @@ def render_file(file_name, source, target, locales, ctx):
|
|||||||
|
|
||||||
with open(target_file, 'w') as localised_file:
|
with open(target_file, 'w') as localised_file:
|
||||||
with lang_over(locale):
|
with lang_over(locale):
|
||||||
renderd = render_to_string(os.path.join(source, file_name), ctx)
|
rendered = render_to_string(os.path.join(source, file_name), ctx)
|
||||||
localised_file.write(renderd)
|
localised_file.write(rendered)
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
|
@ -34,4 +34,4 @@ class Command(BaseCommand):
|
|||||||
self.stdout.write("Database configuration is not usable")
|
self.stdout.write("Database configuration is not usable")
|
||||||
|
|
||||||
if connected:
|
if connected:
|
||||||
self.stdout.write("Database connection sucessful!")
|
self.stdout.write("Database connection successful!")
|
||||||
|
@ -28,7 +28,7 @@ class InvenTreeMetadata(SimpleMetadata):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def determine_metadata(self, request, view):
|
def determine_metadata(self, request, view):
|
||||||
"""Overwrite the metadata to adapt to hte request user."""
|
"""Overwrite the metadata to adapt to the request user."""
|
||||||
self.request = request
|
self.request = request
|
||||||
self.view = view
|
self.view = view
|
||||||
|
|
||||||
@ -36,7 +36,7 @@ class InvenTreeMetadata(SimpleMetadata):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
Custom context information to pass through to the OPTIONS endpoint,
|
Custom context information to pass through to the OPTIONS endpoint,
|
||||||
if the "context=True" is supplied to the OPTIONS requst
|
if the "context=True" is supplied to the OPTIONS request
|
||||||
|
|
||||||
Serializer class can supply context data by defining a get_context_data() method (no arguments)
|
Serializer class can supply context data by defining a get_context_data() method (no arguments)
|
||||||
"""
|
"""
|
||||||
|
@ -158,10 +158,10 @@ class InvenTreeExceptionProcessor(ExceptionProcessor):
|
|||||||
"""Custom exception processor that respects blocked errors."""
|
"""Custom exception processor that respects blocked errors."""
|
||||||
|
|
||||||
def process_exception(self, request, exception):
|
def process_exception(self, request, exception):
|
||||||
"""Check if kind is ignored before procesing."""
|
"""Check if kind is ignored before processing."""
|
||||||
kind, info, data = sys.exc_info()
|
kind, info, data = sys.exc_info()
|
||||||
|
|
||||||
# Check if the eror is on the ignore list
|
# Check if the error is on the ignore list
|
||||||
if kind in settings.IGNORED_ERRORS:
|
if kind in settings.IGNORED_ERRORS:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -125,7 +125,7 @@ class CreateAPI(CleanMixin, generics.CreateAPIView):
|
|||||||
|
|
||||||
|
|
||||||
class RetrieveAPI(generics.RetrieveAPIView):
|
class RetrieveAPI(generics.RetrieveAPIView):
|
||||||
"""View for retreive API."""
|
"""View for retrieve API."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@ -125,7 +125,7 @@ class DataImportMixin(object):
|
|||||||
Models which implement this mixin should provide information on the fields available for import
|
Models which implement this mixin should provide information on the fields available for import
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Define a map of fields avaialble for import
|
# Define a map of fields available for import
|
||||||
IMPORT_FIELDS = {}
|
IMPORT_FIELDS = {}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -712,12 +712,12 @@ class InvenTreeTree(MPTTModel):
|
|||||||
available = contents.get_all_objects_for_this_type()
|
available = contents.get_all_objects_for_this_type()
|
||||||
|
|
||||||
# List of child IDs
|
# List of child IDs
|
||||||
childs = self.getUniqueChildren()
|
children = self.getUniqueChildren()
|
||||||
|
|
||||||
acceptable = [None]
|
acceptable = [None]
|
||||||
|
|
||||||
for a in available:
|
for a in available:
|
||||||
if a.id not in childs:
|
if a.id not in children:
|
||||||
acceptable.append(a)
|
acceptable.append(a)
|
||||||
|
|
||||||
return acceptable
|
return acceptable
|
||||||
|
@ -34,7 +34,7 @@ class InvenTreeMoneySerializer(MoneyField):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
"""Overrite default values."""
|
"""Override default values."""
|
||||||
kwargs["max_digits"] = kwargs.get("max_digits", 19)
|
kwargs["max_digits"] = kwargs.get("max_digits", 19)
|
||||||
self.decimal_places = kwargs["decimal_places"] = kwargs.get("decimal_places", 6)
|
self.decimal_places = kwargs["decimal_places"] = kwargs.get("decimal_places", 6)
|
||||||
kwargs["required"] = kwargs.get("required", False)
|
kwargs["required"] = kwargs.get("required", False)
|
||||||
@ -269,13 +269,13 @@ class InvenTreeTaggitSerializer(TaggitSerializer):
|
|||||||
"""Updated from https://github.com/glemmaPaul/django-taggit-serializer."""
|
"""Updated from https://github.com/glemmaPaul/django-taggit-serializer."""
|
||||||
|
|
||||||
def update(self, instance, validated_data):
|
def update(self, instance, validated_data):
|
||||||
"""Overriden update method to readd the tagmanager."""
|
"""Overridden update method to re-add the tagmanager."""
|
||||||
to_be_tagged, validated_data = self._pop_tags(validated_data)
|
to_be_tagged, validated_data = self._pop_tags(validated_data)
|
||||||
|
|
||||||
tag_object = super().update(instance, validated_data)
|
tag_object = super().update(instance, validated_data)
|
||||||
|
|
||||||
for key in to_be_tagged.keys():
|
for key in to_be_tagged.keys():
|
||||||
# readd the tagmanager
|
# re-add the tagmanager
|
||||||
new_tagobject = tag_object.__class__.objects.get(id=tag_object.id)
|
new_tagobject = tag_object.__class__.objects.get(id=tag_object.id)
|
||||||
setattr(tag_object, key, getattr(new_tagobject, key))
|
setattr(tag_object, key, getattr(new_tagobject, key))
|
||||||
|
|
||||||
|
@ -480,7 +480,7 @@ if "postgres" in db_engine: # pragma: no cover
|
|||||||
if "connect_timeout" not in db_options:
|
if "connect_timeout" not in db_options:
|
||||||
# The DB server is in the same data center, it should not take very
|
# The DB server is in the same data center, it should not take very
|
||||||
# long to connect to the database server
|
# long to connect to the database server
|
||||||
# # seconds, 2 is minium allowed by libpq
|
# # seconds, 2 is minimum allowed by libpq
|
||||||
db_options["connect_timeout"] = int(
|
db_options["connect_timeout"] = int(
|
||||||
get_setting('INVENTREE_DB_TIMEOUT', 'database.timeout', 2)
|
get_setting('INVENTREE_DB_TIMEOUT', 'database.timeout', 2)
|
||||||
)
|
)
|
||||||
@ -576,7 +576,7 @@ REMOTE_LOGIN_HEADER = get_setting('INVENTREE_REMOTE_LOGIN_HEADER', 'remote_login
|
|||||||
# sentry.io integration for error reporting
|
# sentry.io integration for error reporting
|
||||||
SENTRY_ENABLED = get_boolean_setting('INVENTREE_SENTRY_ENABLED', 'sentry_enabled', False)
|
SENTRY_ENABLED = get_boolean_setting('INVENTREE_SENTRY_ENABLED', 'sentry_enabled', False)
|
||||||
|
|
||||||
# Default Sentry DSN (can be overriden if user wants custom sentry integration)
|
# Default Sentry DSN (can be overridden if user wants custom sentry integration)
|
||||||
SENTRY_DSN = get_setting('INVENTREE_SENTRY_DSN', 'sentry_dsn', default_sentry_dsn())
|
SENTRY_DSN = get_setting('INVENTREE_SENTRY_DSN', 'sentry_dsn', default_sentry_dsn())
|
||||||
SENTRY_SAMPLE_RATE = float(get_setting('INVENTREE_SENTRY_SAMPLE_RATE', 'sentry_sample_rate', 0.1))
|
SENTRY_SAMPLE_RATE = float(get_setting('INVENTREE_SENTRY_SAMPLE_RATE', 'sentry_sample_rate', 0.1))
|
||||||
|
|
||||||
@ -598,7 +598,7 @@ cache_port = get_setting('INVENTREE_CACHE_PORT', 'cache.port', '6379', typecast=
|
|||||||
if cache_host: # pragma: no cover
|
if cache_host: # pragma: no cover
|
||||||
# We are going to rely upon a possibly non-localhost for our cache,
|
# We are going to rely upon a possibly non-localhost for our cache,
|
||||||
# so don't wait too long for the cache as nothing in the cache should be
|
# so don't wait too long for the cache as nothing in the cache should be
|
||||||
# irreplacable.
|
# irreplaceable.
|
||||||
_cache_options = {
|
_cache_options = {
|
||||||
"CLIENT_CLASS": "django_redis.client.DefaultClient",
|
"CLIENT_CLASS": "django_redis.client.DefaultClient",
|
||||||
"SOCKET_CONNECT_TIMEOUT": int(os.getenv("CACHE_CONNECT_TIMEOUT", "2")),
|
"SOCKET_CONNECT_TIMEOUT": int(os.getenv("CACHE_CONNECT_TIMEOUT", "2")),
|
||||||
@ -910,7 +910,7 @@ PLUGINS_ENABLED = get_boolean_setting('INVENTREE_PLUGINS_ENABLED', 'plugins_enab
|
|||||||
PLUGIN_FILE = config.get_plugin_file()
|
PLUGIN_FILE = config.get_plugin_file()
|
||||||
|
|
||||||
# Plugin test settings
|
# Plugin test settings
|
||||||
PLUGIN_TESTING = get_setting('INVENTREE_PLUGIN_TESTING', 'PLUGIN_TESTING', TESTING) # Are plugins beeing tested?
|
PLUGIN_TESTING = get_setting('INVENTREE_PLUGIN_TESTING', 'PLUGIN_TESTING', TESTING) # Are plugins being tested?
|
||||||
PLUGIN_TESTING_SETUP = get_setting('INVENTREE_PLUGIN_TESTING_SETUP', 'PLUGIN_TESTING_SETUP', False) # Load plugins from setup hooks in testing?
|
PLUGIN_TESTING_SETUP = get_setting('INVENTREE_PLUGIN_TESTING_SETUP', 'PLUGIN_TESTING_SETUP', False) # Load plugins from setup hooks in testing?
|
||||||
PLUGIN_TESTING_EVENTS = False # Flag if events are tested right now
|
PLUGIN_TESTING_EVENTS = False # Flag if events are tested right now
|
||||||
PLUGIN_RETRY = get_setting('INVENTREE_PLUGIN_RETRY', 'PLUGIN_RETRY', 5) # How often should plugin loading be tried?
|
PLUGIN_RETRY = get_setting('INVENTREE_PLUGIN_RETRY', 'PLUGIN_RETRY', 5) # How often should plugin loading be tried?
|
||||||
|
@ -249,7 +249,7 @@ class ScheduledTask:
|
|||||||
|
|
||||||
|
|
||||||
class TaskRegister:
|
class TaskRegister:
|
||||||
"""Registery for periodicall tasks."""
|
"""Registry for periodicall tasks."""
|
||||||
task_list: List[ScheduledTask] = []
|
task_list: List[ScheduledTask] = []
|
||||||
|
|
||||||
def register(self, task, schedule, minutes: int = None):
|
def register(self, task, schedule, minutes: int = None):
|
||||||
@ -562,7 +562,7 @@ def run_backup():
|
|||||||
def check_for_migrations(worker: bool = True):
|
def check_for_migrations(worker: bool = True):
|
||||||
"""Checks if migrations are needed.
|
"""Checks if migrations are needed.
|
||||||
|
|
||||||
If the setting auto_update is enabled we will start updateing.
|
If the setting auto_update is enabled we will start updating.
|
||||||
"""
|
"""
|
||||||
# Test if auto-updates are enabled
|
# Test if auto-updates are enabled
|
||||||
if not get_setting('INVENTREE_AUTO_UPDATE', 'auto_update'):
|
if not get_setting('INVENTREE_AUTO_UPDATE', 'auto_update'):
|
||||||
|
@ -28,13 +28,13 @@ class MiddlewareTests(InvenTreeTestCase):
|
|||||||
self.client.logout()
|
self.client.logout()
|
||||||
|
|
||||||
# check that static files go through
|
# check that static files go through
|
||||||
# TODO @matmair reenable this check
|
# TODO @matmair re-enable this check
|
||||||
# self.check_path('/static/css/inventree.css', 302)
|
# self.check_path('/static/css/inventree.css', 302)
|
||||||
|
|
||||||
# check that account things go through
|
# check that account things go through
|
||||||
self.check_path(reverse('account_login'))
|
self.check_path(reverse('account_login'))
|
||||||
|
|
||||||
# logout goes diretly to login
|
# logout goes directly to login
|
||||||
self.check_path(reverse('account_logout'))
|
self.check_path(reverse('account_logout'))
|
||||||
|
|
||||||
# check that frontend code is redirected to login
|
# check that frontend code is redirected to login
|
||||||
|
@ -70,11 +70,11 @@ class InvenTreeTaskTests(TestCase):
|
|||||||
with self.assertWarnsMessage(UserWarning, "WARNING: 'InvenTree' not started - Malformed function path"):
|
with self.assertWarnsMessage(UserWarning, "WARNING: 'InvenTree' not started - Malformed function path"):
|
||||||
InvenTree.tasks.offload_task('InvenTree')
|
InvenTree.tasks.offload_task('InvenTree')
|
||||||
|
|
||||||
# Non exsistent app
|
# Non existent app
|
||||||
with self.assertWarnsMessage(UserWarning, "WARNING: 'InvenTreeABC.test_tasks.doesnotmatter' not started - No module named 'InvenTreeABC.test_tasks'"):
|
with self.assertWarnsMessage(UserWarning, "WARNING: 'InvenTreeABC.test_tasks.doesnotmatter' not started - No module named 'InvenTreeABC.test_tasks'"):
|
||||||
InvenTree.tasks.offload_task('InvenTreeABC.test_tasks.doesnotmatter')
|
InvenTree.tasks.offload_task('InvenTreeABC.test_tasks.doesnotmatter')
|
||||||
|
|
||||||
# Non exsistent function
|
# Non existent function
|
||||||
with self.assertWarnsMessage(UserWarning, "WARNING: 'InvenTree.test_tasks.doesnotexsist' not started - No function named 'doesnotexsist'"):
|
with self.assertWarnsMessage(UserWarning, "WARNING: 'InvenTree.test_tasks.doesnotexsist' not started - No function named 'doesnotexsist'"):
|
||||||
InvenTree.tasks.offload_task('InvenTree.test_tasks.doesnotexsist')
|
InvenTree.tasks.offload_task('InvenTree.test_tasks.doesnotexsist')
|
||||||
|
|
||||||
|
@ -288,7 +288,7 @@ class TestHelpers(TestCase):
|
|||||||
def dl_helper(url, expected_error, timeout=2.5, retries=3):
|
def dl_helper(url, expected_error, timeout=2.5, retries=3):
|
||||||
"""Helper function for unit testing downloads.
|
"""Helper function for unit testing downloads.
|
||||||
|
|
||||||
As the httpstat.us service occassionaly refuses a connection,
|
As the httpstat.us service occasionally refuses a connection,
|
||||||
we will simply try multiple times
|
we will simply try multiple times
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -542,7 +542,7 @@ class TestSerialNumberExtraction(TestCase):
|
|||||||
self.assertEqual(sn, ['5', '6', '7', '8'])
|
self.assertEqual(sn, ['5', '6', '7', '8'])
|
||||||
|
|
||||||
def test_failures(self):
|
def test_failures(self):
|
||||||
"""Test wron serial numbers."""
|
"""Test wrong serial numbers."""
|
||||||
e = helpers.extract_serial_numbers
|
e = helpers.extract_serial_numbers
|
||||||
|
|
||||||
# Test duplicates
|
# Test duplicates
|
||||||
@ -1033,7 +1033,7 @@ class SanitizerTest(TestCase):
|
|||||||
"""Simple tests for sanitizer functions."""
|
"""Simple tests for sanitizer functions."""
|
||||||
|
|
||||||
def test_svg_sanitizer(self):
|
def test_svg_sanitizer(self):
|
||||||
"""Test that SVGs are sanitized acordingly."""
|
"""Test that SVGs are sanitized accordingly."""
|
||||||
valid_string = """<svg xmlns="http://www.w3.org/2000/svg" version="1.1" id="svg2" height="400" width="400">{0}
|
valid_string = """<svg xmlns="http://www.w3.org/2000/svg" version="1.1" id="svg2" height="400" width="400">{0}
|
||||||
<path id="path1" d="m -151.78571,359.62883 v 112.76373 l 97.068507,-56.04253 V 303.14815 Z" style="fill:#ddbc91;"></path>
|
<path id="path1" d="m -151.78571,359.62883 v 112.76373 l 97.068507,-56.04253 V 303.14815 Z" style="fill:#ddbc91;"></path>
|
||||||
</svg>"""
|
</svg>"""
|
||||||
|
@ -354,27 +354,27 @@ class InvenTreeAPITestCase(ExchangeRateMixin, UserMixin, APITestCase):
|
|||||||
|
|
||||||
if decode:
|
if decode:
|
||||||
# Decode data and return as StringIO file object
|
# Decode data and return as StringIO file object
|
||||||
fo = io.StringIO()
|
file = io.StringIO()
|
||||||
fo.name = fo
|
file.name = file
|
||||||
fo.write(response.getvalue().decode('UTF-8'))
|
file.write(response.getvalue().decode('UTF-8'))
|
||||||
else:
|
else:
|
||||||
# Return a a BytesIO file object
|
# Return a a BytesIO file object
|
||||||
fo = io.BytesIO()
|
file = io.BytesIO()
|
||||||
fo.name = fn
|
file.name = fn
|
||||||
fo.write(response.getvalue())
|
file.write(response.getvalue())
|
||||||
|
|
||||||
fo.seek(0)
|
file.seek(0)
|
||||||
|
|
||||||
return fo
|
return file
|
||||||
|
|
||||||
def process_csv(self, fo, delimiter=',', required_cols=None, excluded_cols=None, required_rows=None):
|
def process_csv(self, file_object, delimiter=',', required_cols=None, excluded_cols=None, required_rows=None):
|
||||||
"""Helper function to process and validate a downloaded csv file."""
|
"""Helper function to process and validate a downloaded csv file."""
|
||||||
# Check that the correct object type has been passed
|
# Check that the correct object type has been passed
|
||||||
self.assertTrue(isinstance(fo, io.StringIO))
|
self.assertTrue(isinstance(file_object, io.StringIO))
|
||||||
|
|
||||||
fo.seek(0)
|
file_object.seek(0)
|
||||||
|
|
||||||
reader = csv.reader(fo, delimiter=delimiter)
|
reader = csv.reader(file_object, delimiter=delimiter)
|
||||||
|
|
||||||
headers = []
|
headers = []
|
||||||
rows = []
|
rows = []
|
||||||
|
@ -62,7 +62,7 @@ apipatterns = [
|
|||||||
# Plugin endpoints
|
# Plugin endpoints
|
||||||
path('', include(plugin_api_urls)),
|
path('', include(plugin_api_urls)),
|
||||||
|
|
||||||
# Common endpoints enpoint
|
# Common endpoints endpoint
|
||||||
path('', include(common_api_urls)),
|
path('', include(common_api_urls)),
|
||||||
|
|
||||||
# OpenAPI Schema
|
# OpenAPI Schema
|
||||||
@ -98,7 +98,7 @@ dynamic_javascript_urls = [
|
|||||||
re_path(r'^settings.js', DynamicJsView.as_view(template_name='js/dynamic/settings.js'), name='settings.js'),
|
re_path(r'^settings.js', DynamicJsView.as_view(template_name='js/dynamic/settings.js'), name='settings.js'),
|
||||||
]
|
]
|
||||||
|
|
||||||
# These javascript files are pased through the Django translation layer
|
# These javascript files are passed through the Django translation layer
|
||||||
translated_javascript_urls = [
|
translated_javascript_urls = [
|
||||||
re_path(r'^api.js', DynamicJsView.as_view(template_name='js/translated/api.js'), name='api.js'),
|
re_path(r'^api.js', DynamicJsView.as_view(template_name='js/translated/api.js'), name='api.js'),
|
||||||
re_path(r'^attachment.js', DynamicJsView.as_view(template_name='js/translated/attachment.js'), name='attachment.js'),
|
re_path(r'^attachment.js', DynamicJsView.as_view(template_name='js/translated/attachment.js'), name='attachment.js'),
|
||||||
|
@ -357,7 +357,7 @@ class AjaxUpdateView(AjaxMixin, UpdateView):
|
|||||||
- Updates model with POST field data
|
- Updates model with POST field data
|
||||||
- Performs form and object validation
|
- Performs form and object validation
|
||||||
- If errors exist, re-render the form
|
- If errors exist, re-render the form
|
||||||
- Otherwise, return sucess status
|
- Otherwise, return success status
|
||||||
"""
|
"""
|
||||||
self.request = request
|
self.request = request
|
||||||
|
|
||||||
@ -386,7 +386,7 @@ class AjaxUpdateView(AjaxMixin, UpdateView):
|
|||||||
|
|
||||||
if valid:
|
if valid:
|
||||||
|
|
||||||
# Save the updated objec to the database
|
# Save the updated object to the database
|
||||||
self.save(self.object, form)
|
self.save(self.object, form)
|
||||||
|
|
||||||
self.object = self.get_object()
|
self.object = self.get_object()
|
||||||
|
@ -131,7 +131,7 @@ class Build(MPTTModel, InvenTree.models.InvenTreeBarcodeMixin, InvenTree.models.
|
|||||||
# Order was completed within the specified range
|
# Order was completed within the specified range
|
||||||
completed = Q(status=BuildStatus.COMPLETE) & Q(completion_date__gte=min_date) & Q(completion_date__lte=max_date)
|
completed = Q(status=BuildStatus.COMPLETE) & Q(completion_date__gte=min_date) & Q(completion_date__lte=max_date)
|
||||||
|
|
||||||
# Order target date falls witin specified range
|
# Order target date falls within specified range
|
||||||
pending = Q(status__in=BuildStatus.ACTIVE_CODES) & ~Q(target_date=None) & Q(target_date__gte=min_date) & Q(target_date__lte=max_date)
|
pending = Q(status__in=BuildStatus.ACTIVE_CODES) & ~Q(target_date=None) & Q(target_date__gte=min_date) & Q(target_date__lte=max_date)
|
||||||
|
|
||||||
# TODO - Construct a queryset for "overdue" orders
|
# TODO - Construct a queryset for "overdue" orders
|
||||||
@ -310,7 +310,7 @@ class Build(MPTTModel, InvenTree.models.InvenTreeBarcodeMixin, InvenTree.models.
|
|||||||
"""Return the number of sub builds under this one.
|
"""Return the number of sub builds under this one.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
cascade: If True (defualt), include cascading builds under sub builds
|
cascade: If True (default), include cascading builds under sub builds
|
||||||
"""
|
"""
|
||||||
return self.sub_builds(cascade=cascade).count()
|
return self.sub_builds(cascade=cascade).count()
|
||||||
|
|
||||||
|
@ -91,7 +91,7 @@ class BuildSerializer(InvenTreeModelSerializer):
|
|||||||
def annotate_queryset(queryset):
|
def annotate_queryset(queryset):
|
||||||
"""Add custom annotations to the BuildSerializer queryset, performing database queries as efficiently as possible.
|
"""Add custom annotations to the BuildSerializer queryset, performing database queries as efficiently as possible.
|
||||||
|
|
||||||
The following annoted fields are added:
|
The following annotated fields are added:
|
||||||
|
|
||||||
- overdue: True if the build is outstanding *and* the completion date has past
|
- overdue: True if the build is outstanding *and* the completion date has past
|
||||||
"""
|
"""
|
||||||
|
@ -541,10 +541,10 @@ class BuildTest(BuildAPITest):
|
|||||||
{
|
{
|
||||||
'export': 'csv',
|
'export': 'csv',
|
||||||
}
|
}
|
||||||
) as fo:
|
) as file:
|
||||||
|
|
||||||
data = self.process_csv(
|
data = self.process_csv(
|
||||||
fo,
|
file,
|
||||||
required_cols=required_cols,
|
required_cols=required_cols,
|
||||||
excluded_cols=excluded_cols,
|
excluded_cols=excluded_cols,
|
||||||
required_rows=Build.objects.count()
|
required_rows=Build.objects.count()
|
||||||
|
@ -679,7 +679,7 @@ class AutoAllocationTests(BuildTestBase):
|
|||||||
self.assertEqual(self.build.unallocated_quantity(self.bom_item_1), 0)
|
self.assertEqual(self.build.unallocated_quantity(self.bom_item_1), 0)
|
||||||
self.assertEqual(self.build.unallocated_quantity(self.bom_item_2), 5)
|
self.assertEqual(self.build.unallocated_quantity(self.bom_item_2), 5)
|
||||||
|
|
||||||
# This time, allow substitue parts to be used!
|
# This time, allow substitute parts to be used!
|
||||||
self.build.auto_allocate_stock(
|
self.build.auto_allocate_stock(
|
||||||
interchangeable=True,
|
interchangeable=True,
|
||||||
substitutes=True,
|
substitutes=True,
|
||||||
|
@ -45,7 +45,7 @@ class WebhookView(CsrfExemptMixin, APIView):
|
|||||||
run_async = False
|
run_async = False
|
||||||
|
|
||||||
def post(self, request, endpoint, *args, **kwargs):
|
def post(self, request, endpoint, *args, **kwargs):
|
||||||
"""Process incomming webhook."""
|
"""Process incoming webhook."""
|
||||||
# get webhook definition
|
# get webhook definition
|
||||||
self._get_webhook(endpoint, request, *args, **kwargs)
|
self._get_webhook(endpoint, request, *args, **kwargs)
|
||||||
|
|
||||||
@ -164,7 +164,7 @@ class CurrencyRefreshView(APIView):
|
|||||||
class SettingsList(ListAPI):
|
class SettingsList(ListAPI):
|
||||||
"""Generic ListView for settings.
|
"""Generic ListView for settings.
|
||||||
|
|
||||||
This is inheritted by all list views for settings.
|
This is inherited by all list views for settings.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
filter_backends = SEARCH_ORDER_FILTER
|
filter_backends = SEARCH_ORDER_FILTER
|
||||||
|
@ -84,7 +84,7 @@ class FileManager:
|
|||||||
self.HEADERS = self.REQUIRED_HEADERS + self.ITEM_MATCH_HEADERS + self.OPTIONAL_MATCH_HEADERS + self.OPTIONAL_HEADERS
|
self.HEADERS = self.REQUIRED_HEADERS + self.ITEM_MATCH_HEADERS + self.OPTIONAL_MATCH_HEADERS + self.OPTIONAL_HEADERS
|
||||||
|
|
||||||
def setup(self):
|
def setup(self):
|
||||||
"""Setup headers should be overriden in usage to set the Different Headers."""
|
"""Setup headers should be overridden in usage to set the Different Headers."""
|
||||||
if not self.name:
|
if not self.name:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -46,7 +46,7 @@ class MatchFieldForm(forms.Form):
|
|||||||
"""Step 2 of FileManagementFormView."""
|
"""Step 2 of FileManagementFormView."""
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
"""Setup filemanager and check columsn."""
|
"""Setup filemanager and check columns."""
|
||||||
# Get FileManager
|
# Get FileManager
|
||||||
file_manager = None
|
file_manager = None
|
||||||
if 'file_manager' in kwargs:
|
if 'file_manager' in kwargs:
|
||||||
@ -106,7 +106,7 @@ class MatchItemForm(forms.Form):
|
|||||||
# Set field name
|
# Set field name
|
||||||
field_name = col_guess.lower() + '-' + str(row['index'])
|
field_name = col_guess.lower() + '-' + str(row['index'])
|
||||||
|
|
||||||
# check if field def was overriden
|
# check if field def was overridden
|
||||||
overriden_field = self.get_special_field(col_guess, row, file_manager)
|
overriden_field = self.get_special_field(col_guess, row, file_manager)
|
||||||
if overriden_field:
|
if overriden_field:
|
||||||
self.fields[field_name] = overriden_field
|
self.fields[field_name] = overriden_field
|
||||||
@ -174,5 +174,5 @@ class MatchItemForm(forms.Form):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def get_special_field(self, col_guess, row, file_manager):
|
def get_special_field(self, col_guess, row, file_manager):
|
||||||
"""Function to be overriden in inherited forms to add specific form settings."""
|
"""Function to be overridden in inherited forms to add specific form settings."""
|
||||||
return None
|
return None
|
||||||
|
@ -2395,7 +2395,7 @@ class WebhookEndpoint(models.Model):
|
|||||||
self.verify = self.VERIFICATION_METHOD
|
self.verify = self.VERIFICATION_METHOD
|
||||||
|
|
||||||
def process_webhook(self):
|
def process_webhook(self):
|
||||||
"""Process the webhook incomming.
|
"""Process the webhook incoming.
|
||||||
|
|
||||||
This does not deal with the data itself - that happens in process_payload.
|
This does not deal with the data itself - that happens in process_payload.
|
||||||
Do not touch or pickle data here - it was not verified to be safe.
|
Do not touch or pickle data here - it was not verified to be safe.
|
||||||
@ -2532,7 +2532,7 @@ class WebhookMessage(models.Model):
|
|||||||
|
|
||||||
|
|
||||||
class NotificationEntry(MetaMixin):
|
class NotificationEntry(MetaMixin):
|
||||||
"""A NotificationEntry records the last time a particular notifaction was sent out.
|
"""A NotificationEntry records the last time a particular notification was sent out.
|
||||||
|
|
||||||
It is recorded to ensure that notifications are not sent out "too often" to users.
|
It is recorded to ensure that notifications are not sent out "too often" to users.
|
||||||
|
|
||||||
|
@ -62,11 +62,11 @@ class NotificationMethod:
|
|||||||
def check_context(self, context):
|
def check_context(self, context):
|
||||||
"""Check that all values defined in the methods CONTEXT were provided in the current context."""
|
"""Check that all values defined in the methods CONTEXT were provided in the current context."""
|
||||||
def check(ref, obj):
|
def check(ref, obj):
|
||||||
# the obj is not accesible so we are on the end
|
# the obj is not accessible so we are on the end
|
||||||
if not isinstance(obj, (list, dict, tuple, )):
|
if not isinstance(obj, (list, dict, tuple, )):
|
||||||
return ref
|
return ref
|
||||||
|
|
||||||
# check if the ref exsists
|
# check if the ref exists
|
||||||
if isinstance(ref, str):
|
if isinstance(ref, str):
|
||||||
if not obj.get(ref):
|
if not obj.get(ref):
|
||||||
return ref
|
return ref
|
||||||
@ -150,16 +150,16 @@ class SingleNotificationMethod(NotificationMethod):
|
|||||||
"""NotificationMethod that sends notifications one by one."""
|
"""NotificationMethod that sends notifications one by one."""
|
||||||
|
|
||||||
def send(self, target):
|
def send(self, target):
|
||||||
"""This function must be overriden."""
|
"""This function must be overridden."""
|
||||||
raise NotImplementedError('The `send` method must be overriden!')
|
raise NotImplementedError('The `send` method must be overridden!')
|
||||||
|
|
||||||
|
|
||||||
class BulkNotificationMethod(NotificationMethod):
|
class BulkNotificationMethod(NotificationMethod):
|
||||||
"""NotificationMethod that sends all notifications in bulk."""
|
"""NotificationMethod that sends all notifications in bulk."""
|
||||||
|
|
||||||
def send_bulk(self):
|
def send_bulk(self):
|
||||||
"""This function must be overriden."""
|
"""This function must be overridden."""
|
||||||
raise NotImplementedError('The `send` method must be overriden!')
|
raise NotImplementedError('The `send` method must be overridden!')
|
||||||
# endregion
|
# endregion
|
||||||
|
|
||||||
|
|
||||||
@ -268,7 +268,7 @@ class NotificationBody:
|
|||||||
message (str): Notification message as text. Should not be longer than 120 chars.
|
message (str): Notification message as text. Should not be longer than 120 chars.
|
||||||
template (str): Reference to the html template for the notification.
|
template (str): Reference to the html template for the notification.
|
||||||
|
|
||||||
The strings support f-string sytle fomratting with context variables parsed at runtime.
|
The strings support f-string style formatting with context variables parsed at runtime.
|
||||||
|
|
||||||
Context variables:
|
Context variables:
|
||||||
instance: Text representing the instance
|
instance: Text representing the instance
|
||||||
|
@ -57,7 +57,7 @@ def update_news_feed():
|
|||||||
|
|
||||||
# Iterate over entries
|
# Iterate over entries
|
||||||
for entry in d.entries:
|
for entry in d.entries:
|
||||||
# Check if id already exsists
|
# Check if id already exists
|
||||||
if entry.id in id_list:
|
if entry.id in id_list:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
@ -47,7 +47,7 @@ class BaseNotificationTests(BaseNotificationIntegrationTest):
|
|||||||
with self.assertRaises(NotImplementedError):
|
with self.assertRaises(NotImplementedError):
|
||||||
NoNameNotificationMethod('', '', '', '', )
|
NoNameNotificationMethod('', '', '', '', )
|
||||||
|
|
||||||
# a not existant context check
|
# a not existent context check
|
||||||
with self.assertRaises(NotImplementedError):
|
with self.assertRaises(NotImplementedError):
|
||||||
WrongContextNotificationMethod('', '', '', '', )
|
WrongContextNotificationMethod('', '', '', '', )
|
||||||
|
|
||||||
@ -149,7 +149,7 @@ class NotificationUserSettingTests(BaseNotificationIntegrationTest):
|
|||||||
def send_bulk(self):
|
def send_bulk(self):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# run thorugh notification
|
# run through notification
|
||||||
self._notification_run(SampleImplementation)
|
self._notification_run(SampleImplementation)
|
||||||
# make sure the array fits
|
# make sure the array fits
|
||||||
array = storage.get_usersettings(self.user)
|
array = storage.get_usersettings(self.user)
|
||||||
|
@ -272,7 +272,7 @@ class GlobalSettingsApiTest(InvenTreeAPITestCase):
|
|||||||
self.assertEqual(len(response.data), len(InvenTreeSetting.SETTINGS.keys()))
|
self.assertEqual(len(response.data), len(InvenTreeSetting.SETTINGS.keys()))
|
||||||
|
|
||||||
def test_company_name(self):
|
def test_company_name(self):
|
||||||
"""Test a settings object lifecyle e2e."""
|
"""Test a settings object lifecycle e2e."""
|
||||||
setting = InvenTreeSetting.get_setting_object('INVENTREE_COMPANY_NAME')
|
setting = InvenTreeSetting.get_setting_object('INVENTREE_COMPANY_NAME')
|
||||||
|
|
||||||
# Check default value
|
# Check default value
|
||||||
@ -781,7 +781,7 @@ class NotificationTest(InvenTreeAPITestCase):
|
|||||||
for _ii in range(10):
|
for _ii in range(10):
|
||||||
Error.objects.create()
|
Error.objects.create()
|
||||||
|
|
||||||
# Check that messsages have been created
|
# Check that messages have been created
|
||||||
messages = NotificationMessage.objects.all()
|
messages = NotificationMessage.objects.all()
|
||||||
|
|
||||||
# As there are three staff users (including the 'test' user) we expect 30 notifications
|
# As there are three staff users (including the 'test' user) we expect 30 notifications
|
||||||
@ -860,7 +860,7 @@ class CommonTest(InvenTreeAPITestCase):
|
|||||||
self.user.is_superuser = True
|
self.user.is_superuser = True
|
||||||
self.user.save()
|
self.user.save()
|
||||||
|
|
||||||
# Successfull checks
|
# Successful checks
|
||||||
data = [
|
data = [
|
||||||
self.get(reverse('api-config-list'), expected_code=200).data[0], # list endpoint
|
self.get(reverse('api-config-list'), expected_code=200).data[0], # list endpoint
|
||||||
self.get(reverse('api-config-detail', kwargs={'key': 'INVENTREE_DEBUG'}), expected_code=200).data, # detail endpoint
|
self.get(reverse('api-config-detail', kwargs={'key': 'INVENTREE_DEBUG'}), expected_code=200).data, # detail endpoint
|
||||||
|
@ -363,7 +363,7 @@ class ManufacturerPartAttachment(InvenTreeAttachment):
|
|||||||
class ManufacturerPartParameter(models.Model):
|
class ManufacturerPartParameter(models.Model):
|
||||||
"""A ManufacturerPartParameter represents a key:value parameter for a MnaufacturerPart.
|
"""A ManufacturerPartParameter represents a key:value parameter for a MnaufacturerPart.
|
||||||
|
|
||||||
This is used to represent parmeters / properties for a particular manufacturer part.
|
This is used to represent parameters / properties for a particular manufacturer part.
|
||||||
|
|
||||||
Each parameter is a simple string (text) value.
|
Each parameter is a simple string (text) value.
|
||||||
"""
|
"""
|
||||||
|
@ -88,7 +88,7 @@ class SupplierPartPackUnitsTests(InvenTreeTestCase):
|
|||||||
}
|
}
|
||||||
|
|
||||||
# All these values are invalid for a part with dimension 'm'
|
# All these values are invalid for a part with dimension 'm'
|
||||||
# Either the values are invalid, or the units are incomaptible
|
# Either the values are invalid, or the units are incompatible
|
||||||
fail_tests = [
|
fail_tests = [
|
||||||
'-1',
|
'-1',
|
||||||
'-1m',
|
'-1m',
|
||||||
|
@ -125,7 +125,7 @@ class LabelConfig(AppConfig):
|
|||||||
logger.info(f"Creating required directory: '{dst_dir}'")
|
logger.info(f"Creating required directory: '{dst_dir}'")
|
||||||
dst_dir.mkdir(parents=True, exist_ok=True)
|
dst_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
# Create lables
|
# Create labels
|
||||||
for label in labels:
|
for label in labels:
|
||||||
self.create_template_label(model, src_dir, ref_name, label)
|
self.create_template_label(model, src_dir, ref_name, label)
|
||||||
|
|
||||||
@ -156,7 +156,7 @@ class LabelConfig(AppConfig):
|
|||||||
|
|
||||||
if to_copy:
|
if to_copy:
|
||||||
logger.info(f"Copying label template '{dst_file}'")
|
logger.info(f"Copying label template '{dst_file}'")
|
||||||
# Ensure destionation dir exists
|
# Ensure destination dir exists
|
||||||
dst_file.parent.mkdir(parents=True, exist_ok=True)
|
dst_file.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
# Copy file
|
# Copy file
|
||||||
|
@ -315,7 +315,7 @@ class PurchaseOrder(TotalPriceMixin, Order):
|
|||||||
|
|
||||||
- Specified as min_date, max_date
|
- Specified as min_date, max_date
|
||||||
- Both must be specified for filter to be applied
|
- Both must be specified for filter to be applied
|
||||||
- Determine which "interesting" orders exist bewteen these dates
|
- Determine which "interesting" orders exist between these dates
|
||||||
|
|
||||||
To be "interesting":
|
To be "interesting":
|
||||||
- A "received" order where the received date lies within the date range
|
- A "received" order where the received date lies within the date range
|
||||||
@ -1225,7 +1225,7 @@ class PurchaseOrderLineItem(OrderLineItem):
|
|||||||
def get_destination(self):
|
def get_destination(self):
|
||||||
"""Show where the line item is or should be placed.
|
"""Show where the line item is or should be placed.
|
||||||
|
|
||||||
NOTE: If a line item gets split when recieved, only an arbitrary
|
NOTE: If a line item gets split when received, only an arbitrary
|
||||||
stock items location will be reported as the location for the
|
stock items location will be reported as the location for the
|
||||||
entire line.
|
entire line.
|
||||||
"""
|
"""
|
||||||
|
@ -697,10 +697,10 @@ class PurchaseOrderDownloadTest(OrderTest):
|
|||||||
},
|
},
|
||||||
expected_code=200,
|
expected_code=200,
|
||||||
expected_fn='InvenTree_PurchaseOrders.csv',
|
expected_fn='InvenTree_PurchaseOrders.csv',
|
||||||
) as fo:
|
) as file:
|
||||||
|
|
||||||
data = self.process_csv(
|
data = self.process_csv(
|
||||||
fo,
|
file,
|
||||||
required_cols=self.required_cols,
|
required_cols=self.required_cols,
|
||||||
excluded_cols=self.excluded_cols,
|
excluded_cols=self.excluded_cols,
|
||||||
required_rows=models.PurchaseOrder.objects.count()
|
required_rows=models.PurchaseOrder.objects.count()
|
||||||
@ -722,9 +722,9 @@ class PurchaseOrderDownloadTest(OrderTest):
|
|||||||
decode=False,
|
decode=False,
|
||||||
expected_code=200,
|
expected_code=200,
|
||||||
expected_fn='InvenTree_PurchaseOrderItems.xlsx',
|
expected_fn='InvenTree_PurchaseOrderItems.xlsx',
|
||||||
) as fo:
|
) as file:
|
||||||
|
|
||||||
self.assertTrue(isinstance(fo, io.BytesIO))
|
self.assertTrue(isinstance(file, io.BytesIO))
|
||||||
|
|
||||||
|
|
||||||
class PurchaseOrderReceiveTest(OrderTest):
|
class PurchaseOrderReceiveTest(OrderTest):
|
||||||
@ -1558,8 +1558,8 @@ class SalesOrderDownloadTest(OrderTest):
|
|||||||
expected_code=200,
|
expected_code=200,
|
||||||
expected_fn='InvenTree_SalesOrders.xls',
|
expected_fn='InvenTree_SalesOrders.xls',
|
||||||
decode=False,
|
decode=False,
|
||||||
) as fo:
|
) as file:
|
||||||
self.assertTrue(isinstance(fo, io.BytesIO))
|
self.assertTrue(isinstance(file, io.BytesIO))
|
||||||
|
|
||||||
def test_download_csv(self):
|
def test_download_csv(self):
|
||||||
"""Tesst that the list of sales orders can be downloaded as a .csv file"""
|
"""Tesst that the list of sales orders can be downloaded as a .csv file"""
|
||||||
@ -1589,10 +1589,10 @@ class SalesOrderDownloadTest(OrderTest):
|
|||||||
expected_code=200,
|
expected_code=200,
|
||||||
expected_fn='InvenTree_SalesOrders.csv',
|
expected_fn='InvenTree_SalesOrders.csv',
|
||||||
decode=True
|
decode=True
|
||||||
) as fo:
|
) as file:
|
||||||
|
|
||||||
data = self.process_csv(
|
data = self.process_csv(
|
||||||
fo,
|
file,
|
||||||
required_cols=required_cols,
|
required_cols=required_cols,
|
||||||
excluded_cols=excluded_cols,
|
excluded_cols=excluded_cols,
|
||||||
required_rows=models.SalesOrder.objects.count()
|
required_rows=models.SalesOrder.objects.count()
|
||||||
@ -1615,10 +1615,10 @@ class SalesOrderDownloadTest(OrderTest):
|
|||||||
expected_code=200,
|
expected_code=200,
|
||||||
expected_fn='InvenTree_SalesOrders.tsv',
|
expected_fn='InvenTree_SalesOrders.tsv',
|
||||||
decode=True,
|
decode=True,
|
||||||
) as fo:
|
) as file:
|
||||||
|
|
||||||
self.process_csv(
|
self.process_csv(
|
||||||
fo,
|
file,
|
||||||
required_cols=required_cols,
|
required_cols=required_cols,
|
||||||
excluded_cols=excluded_cols,
|
excluded_cols=excluded_cols,
|
||||||
required_rows=models.SalesOrder.objects.filter(status__in=SalesOrderStatus.OPEN).count(),
|
required_rows=models.SalesOrder.objects.filter(status__in=SalesOrderStatus.OPEN).count(),
|
||||||
|
@ -268,7 +268,7 @@ class CategoryParameterList(ListCreateAPI):
|
|||||||
|
|
||||||
|
|
||||||
class CategoryParameterDetail(RetrieveUpdateDestroyAPI):
|
class CategoryParameterDetail(RetrieveUpdateDestroyAPI):
|
||||||
"""Detail endpoint fro the PartCategoryParameterTemplate model"""
|
"""Detail endpoint for the PartCategoryParameterTemplate model"""
|
||||||
|
|
||||||
queryset = PartCategoryParameterTemplate.objects.all()
|
queryset = PartCategoryParameterTemplate.objects.all()
|
||||||
serializer_class = part_serializers.CategoryParameterTemplateSerializer
|
serializer_class = part_serializers.CategoryParameterTemplateSerializer
|
||||||
@ -550,7 +550,7 @@ class PartScheduling(RetrieveAPI):
|
|||||||
|
|
||||||
This assumes that the user is responsible for correctly allocating parts.
|
This assumes that the user is responsible for correctly allocating parts.
|
||||||
|
|
||||||
However, it has the added benefit of side-stepping the various BOM substition options,
|
However, it has the added benefit of side-stepping the various BOM substitution options,
|
||||||
and just looking at what stock items the user has actually allocated against the Build.
|
and just looking at what stock items the user has actually allocated against the Build.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -565,10 +565,10 @@ class PartScheduling(RetrieveAPI):
|
|||||||
|
|
||||||
if bom_item.inherited:
|
if bom_item.inherited:
|
||||||
# An "inherited" BOM item filters down to variant parts also
|
# An "inherited" BOM item filters down to variant parts also
|
||||||
childs = bom_item.part.get_descendants(include_self=True)
|
children = bom_item.part.get_descendants(include_self=True)
|
||||||
builds = Build.objects.filter(
|
builds = Build.objects.filter(
|
||||||
status__in=BuildStatus.ACTIVE_CODES,
|
status__in=BuildStatus.ACTIVE_CODES,
|
||||||
part__in=childs,
|
part__in=children,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
builds = Build.objects.filter(
|
builds = Build.objects.filter(
|
||||||
@ -591,7 +591,7 @@ class PartScheduling(RetrieveAPI):
|
|||||||
# Non-trackable parts are allocated against the build itself
|
# Non-trackable parts are allocated against the build itself
|
||||||
required_quantity = build.quantity * bom_item.quantity
|
required_quantity = build.quantity * bom_item.quantity
|
||||||
|
|
||||||
# Grab all allocations against the spefied BomItem
|
# Grab all allocations against the specified BomItem
|
||||||
allocations = BuildItem.objects.filter(
|
allocations = BuildItem.objects.filter(
|
||||||
bom_item=bom_item,
|
bom_item=bom_item,
|
||||||
build=build,
|
build=build,
|
||||||
@ -1032,7 +1032,7 @@ class PartList(PartMixin, APIDownloadMixin, ListCreateAPI):
|
|||||||
return DownloadFile(filedata, filename)
|
return DownloadFile(filedata, filename)
|
||||||
|
|
||||||
def list(self, request, *args, **kwargs):
|
def list(self, request, *args, **kwargs):
|
||||||
"""Overide the 'list' method, as the PartCategory objects are very expensive to serialize!
|
"""Override the 'list' method, as the PartCategory objects are very expensive to serialize!
|
||||||
|
|
||||||
So we will serialize them first, and keep them in memory, so that they do not have to be serialized multiple times...
|
So we will serialize them first, and keep them in memory, so that they do not have to be serialized multiple times...
|
||||||
"""
|
"""
|
||||||
@ -1049,7 +1049,7 @@ class PartList(PartMixin, APIDownloadMixin, ListCreateAPI):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
Determine the response type based on the request.
|
Determine the response type based on the request.
|
||||||
a) For HTTP requests (e.g. via the browseable API) return a DRF response
|
a) For HTTP requests (e.g. via the browsable API) return a DRF response
|
||||||
b) For AJAX requests, simply return a JSON rendered response.
|
b) For AJAX requests, simply return a JSON rendered response.
|
||||||
"""
|
"""
|
||||||
if page is not None:
|
if page is not None:
|
||||||
@ -1213,7 +1213,7 @@ class PartList(PartMixin, APIDownloadMixin, ListCreateAPI):
|
|||||||
def filter_parameteric_data(self, queryset):
|
def filter_parameteric_data(self, queryset):
|
||||||
"""Filter queryset against part parameters.
|
"""Filter queryset against part parameters.
|
||||||
|
|
||||||
Here we can perfom a number of different functions:
|
Here we can perform a number of different functions:
|
||||||
|
|
||||||
Ordering Based on Parameter Value:
|
Ordering Based on Parameter Value:
|
||||||
- Used if the 'ordering' query param points to a parameter
|
- Used if the 'ordering' query param points to a parameter
|
||||||
@ -1492,7 +1492,7 @@ class PartParameterDetail(RetrieveUpdateDestroyAPI):
|
|||||||
|
|
||||||
|
|
||||||
class PartStocktakeFilter(rest_filters.FilterSet):
|
class PartStocktakeFilter(rest_filters.FilterSet):
|
||||||
"""Custom fitler for the PartStocktakeList endpoint"""
|
"""Custom filter for the PartStocktakeList endpoint"""
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
"""Metaclass options"""
|
"""Metaclass options"""
|
||||||
@ -1702,7 +1702,7 @@ class BomList(BomMixin, ListCreateDestroyAPIView):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
Determine the response type based on the request.
|
Determine the response type based on the request.
|
||||||
a) For HTTP requests (e.g. via the browseable API) return a DRF response
|
a) For HTTP requests (e.g. via the browsable API) return a DRF response
|
||||||
b) For AJAX requests, simply return a JSON rendered response.
|
b) For AJAX requests, simply return a JSON rendered response.
|
||||||
"""
|
"""
|
||||||
if page is not None:
|
if page is not None:
|
||||||
@ -1748,7 +1748,7 @@ class BomList(BomMixin, ListCreateDestroyAPIView):
|
|||||||
There are multiple ways that an assembly can "use" a sub-part:
|
There are multiple ways that an assembly can "use" a sub-part:
|
||||||
|
|
||||||
A) Directly specifying the sub_part in a BomItem field
|
A) Directly specifying the sub_part in a BomItem field
|
||||||
B) Specifing a "template" part with inherited=True
|
B) Specifying a "template" part with inherited=True
|
||||||
C) Allowing variant parts to be substituted
|
C) Allowing variant parts to be substituted
|
||||||
D) Allowing direct substitute parts to be specified
|
D) Allowing direct substitute parts to be specified
|
||||||
|
|
||||||
|
@ -53,10 +53,10 @@ def ExportBom(part: Part, fmt='csv', cascade: bool = False, max_levels: int = No
|
|||||||
max_levels (int, optional): Levels of items that should be included. None for np sublevels. Defaults to None.
|
max_levels (int, optional): Levels of items that should be included. None for np sublevels. Defaults to None.
|
||||||
|
|
||||||
kwargs:
|
kwargs:
|
||||||
parameter_data (bool, optional): Additonal data that should be added. Defaults to False.
|
parameter_data (bool, optional): Additional data that should be added. Defaults to False.
|
||||||
stock_data (bool, optional): Additonal data that should be added. Defaults to False.
|
stock_data (bool, optional): Additional data that should be added. Defaults to False.
|
||||||
supplier_data (bool, optional): Additonal data that should be added. Defaults to False.
|
supplier_data (bool, optional): Additional data that should be added. Defaults to False.
|
||||||
manufacturer_data (bool, optional): Additonal data that should be added. Defaults to False.
|
manufacturer_data (bool, optional): Additional data that should be added. Defaults to False.
|
||||||
pricing_data (bool, optional): Include pricing data in exported BOM. Defaults to False
|
pricing_data (bool, optional): Include pricing data in exported BOM. Defaults to False
|
||||||
substitute_part_data (bool, optional): Include substitute part numbers in exported BOM. Defaults to False
|
substitute_part_data (bool, optional): Include substitute part numbers in exported BOM. Defaults to False
|
||||||
|
|
||||||
|
@ -78,7 +78,7 @@ def annotate_total_stock(reference: str = ''):
|
|||||||
|
|
||||||
- This function calculates the 'total stock' for a given part
|
- This function calculates the 'total stock' for a given part
|
||||||
- Finds all stock items associated with each part (using the provided filter)
|
- Finds all stock items associated with each part (using the provided filter)
|
||||||
- Aggregates the 'quantity' of each relevent stock item
|
- Aggregates the 'quantity' of each relevant stock item
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
reference: The relationship reference of the part from the current model e.g. 'part'
|
reference: The relationship reference of the part from the current model e.g. 'part'
|
||||||
@ -103,7 +103,7 @@ def annotate_build_order_allocations(reference: str = ''):
|
|||||||
|
|
||||||
- This function calculates the total part quantity allocated to open build orders
|
- This function calculates the total part quantity allocated to open build orders
|
||||||
- Finds all build order allocations for each part (using the provided filter)
|
- Finds all build order allocations for each part (using the provided filter)
|
||||||
- Aggregates the 'allocated quantity' for each relevent build order allocation item
|
- Aggregates the 'allocated quantity' for each relevant build order allocation item
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
reference: The relationship reference of the part from the current model
|
reference: The relationship reference of the part from the current model
|
||||||
@ -128,7 +128,7 @@ def annotate_sales_order_allocations(reference: str = ''):
|
|||||||
|
|
||||||
- This function calculates the total part quantity allocated to open sales orders"
|
- This function calculates the total part quantity allocated to open sales orders"
|
||||||
- Finds all sales order allocations for each part (using the provided filter)
|
- Finds all sales order allocations for each part (using the provided filter)
|
||||||
- Aggregates the 'allocated quantity' for each relevent sales order allocation item
|
- Aggregates the 'allocated quantity' for each relevant sales order allocation item
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
reference: The relationship reference of the part from the current model
|
reference: The relationship reference of the part from the current model
|
||||||
|
@ -76,7 +76,7 @@
|
|||||||
pk: 7
|
pk: 7
|
||||||
fields:
|
fields:
|
||||||
name: Mechanical
|
name: Mechanical
|
||||||
description: Mechanical componenets
|
description: Mechanical components
|
||||||
default_location: null
|
default_location: null
|
||||||
level: 0
|
level: 0
|
||||||
tree_id: 2
|
tree_id: 2
|
||||||
|
@ -54,7 +54,7 @@ class Migration(migrations.Migration):
|
|||||||
('description', models.CharField(help_text='Part description', max_length=250)),
|
('description', models.CharField(help_text='Part description', max_length=250)),
|
||||||
('keywords', models.CharField(blank=True, help_text='Part keywords to improve visibility in search results', max_length=250)),
|
('keywords', models.CharField(blank=True, help_text='Part keywords to improve visibility in search results', max_length=250)),
|
||||||
('IPN', models.CharField(blank=True, help_text='Internal Part Number', max_length=100)),
|
('IPN', models.CharField(blank=True, help_text='Internal Part Number', max_length=100)),
|
||||||
('URL', models.URLField(blank=True, help_text='Link to extenal URL')),
|
('URL', models.URLField(blank=True, help_text='Link to external URL')),
|
||||||
('image', models.ImageField(blank=True, max_length=255, null=True, upload_to=part.models.rename_part_image)),
|
('image', models.ImageField(blank=True, max_length=255, null=True, upload_to=part.models.rename_part_image)),
|
||||||
('minimum_stock', models.PositiveIntegerField(default=0, help_text='Minimum allowed stock level', validators=[django.core.validators.MinValueValidator(0)])),
|
('minimum_stock', models.PositiveIntegerField(default=0, help_text='Minimum allowed stock level', validators=[django.core.validators.MinValueValidator(0)])),
|
||||||
('units', models.CharField(blank=True, default='pcs', help_text='Stock keeping units for this part', max_length=20)),
|
('units', models.CharField(blank=True, default='pcs', help_text='Stock keeping units for this part', max_length=20)),
|
||||||
|
@ -14,6 +14,6 @@ class Migration(migrations.Migration):
|
|||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='part',
|
model_name='part',
|
||||||
name='URL',
|
name='URL',
|
||||||
field=InvenTree.fields.InvenTreeURLField(blank=True, help_text='Link to extenal URL'),
|
field=InvenTree.fields.InvenTreeURLField(blank=True, help_text='Link to external URL'),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
@ -25,7 +25,7 @@ class Migration(migrations.Migration):
|
|||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='part',
|
model_name='part',
|
||||||
name='link',
|
name='link',
|
||||||
field=InvenTree.fields.InvenTreeURLField(blank=True, help_text='Link to extenal URL', null=True),
|
field=InvenTree.fields.InvenTreeURLField(blank=True, help_text='Link to external URL', null=True),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='part',
|
model_name='part',
|
||||||
|
@ -1543,7 +1543,7 @@ class Part(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, MPTTModel)
|
|||||||
|
|
||||||
A) This part may be directly specified in a BomItem instance
|
A) This part may be directly specified in a BomItem instance
|
||||||
B) This part may be a *variant* of a part which is directly specified in a BomItem instance
|
B) This part may be a *variant* of a part which is directly specified in a BomItem instance
|
||||||
C) This part may be a *substitute* for a part which is directly specifed in a BomItem instance
|
C) This part may be a *substitute* for a part which is directly specified in a BomItem instance
|
||||||
|
|
||||||
So we construct a query for each case, and combine them...
|
So we construct a query for each case, and combine them...
|
||||||
"""
|
"""
|
||||||
@ -2173,7 +2173,7 @@ class Part(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, MPTTModel)
|
|||||||
return self.parameters.order_by('template__name')
|
return self.parameters.order_by('template__name')
|
||||||
|
|
||||||
def parameters_map(self):
|
def parameters_map(self):
|
||||||
"""Return a map (dict) of parameter values assocaited with this Part instance, of the form.
|
"""Return a map (dict) of parameter values associated with this Part instance, of the form.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
{
|
{
|
||||||
@ -3198,7 +3198,7 @@ class PartTestTemplate(MetadataMixin, models.Model):
|
|||||||
"""A PartTestTemplate defines a 'template' for a test which is required to be run against a StockItem (an instance of the Part).
|
"""A PartTestTemplate defines a 'template' for a test which is required to be run against a StockItem (an instance of the Part).
|
||||||
|
|
||||||
The test template applies "recursively" to part variants, allowing tests to be
|
The test template applies "recursively" to part variants, allowing tests to be
|
||||||
defined in a heirarchy.
|
defined in a hierarchy.
|
||||||
|
|
||||||
Test names are simply strings, rather than enforcing any sort of structure or pattern.
|
Test names are simply strings, rather than enforcing any sort of structure or pattern.
|
||||||
It is up to the user to determine what tests are defined (and how they are run).
|
It is up to the user to determine what tests are defined (and how they are run).
|
||||||
|
@ -189,7 +189,7 @@ def perform_stocktake(target: part.models.Part, user: User, note: str = '', comm
|
|||||||
total_cost_max += pp
|
total_cost_max += pp
|
||||||
has_pricing = True
|
has_pricing = True
|
||||||
except MissingRate:
|
except MissingRate:
|
||||||
logger.warning(f"MissingRate exception occured converting {entry.purchase_price} to {base_currency}")
|
logger.warning(f"MissingRate exception occurred converting {entry.purchase_price} to {base_currency}")
|
||||||
|
|
||||||
if not has_pricing:
|
if not has_pricing:
|
||||||
# Fall back to the part pricing data
|
# Fall back to the part pricing data
|
||||||
|
@ -295,7 +295,7 @@ def inventree_github_url(*args, **kwargs):
|
|||||||
|
|
||||||
@register.simple_tag()
|
@register.simple_tag()
|
||||||
def inventree_docs_url(*args, **kwargs):
|
def inventree_docs_url(*args, **kwargs):
|
||||||
"""Return URL for InvenTree documenation site."""
|
"""Return URL for InvenTree documentation site."""
|
||||||
tag = version.inventreeDocsVersion()
|
tag = version.inventreeDocsVersion()
|
||||||
|
|
||||||
return f"https://docs.inventree.org/en/{tag}"
|
return f"https://docs.inventree.org/en/{tag}"
|
||||||
@ -605,7 +605,7 @@ else: # pragma: no cover
|
|||||||
bits = token.split_contents()
|
bits = token.split_contents()
|
||||||
loc_name = settings.STATICFILES_I18_PREFIX
|
loc_name = settings.STATICFILES_I18_PREFIX
|
||||||
|
|
||||||
# change path to called ressource
|
# change path to called resource
|
||||||
bits[1] = f"'{loc_name}/{{lng}}.{bits[1][1:-1]}'"
|
bits[1] = f"'{loc_name}/{{lng}}.{bits[1][1:-1]}'"
|
||||||
token.contents = ' '.join(bits)
|
token.contents = ' '.join(bits)
|
||||||
|
|
||||||
|
@ -1078,10 +1078,10 @@ class PartAPITest(PartAPITestBase):
|
|||||||
'export': 'csv',
|
'export': 'csv',
|
||||||
},
|
},
|
||||||
expected_fn='InvenTree_Parts.csv',
|
expected_fn='InvenTree_Parts.csv',
|
||||||
) as fo:
|
) as file:
|
||||||
|
|
||||||
data = self.process_csv(
|
data = self.process_csv(
|
||||||
fo,
|
file,
|
||||||
excluded_cols=excluded_cols,
|
excluded_cols=excluded_cols,
|
||||||
required_cols=required_cols,
|
required_cols=required_cols,
|
||||||
required_rows=Part.objects.count(),
|
required_rows=Part.objects.count(),
|
||||||
|
@ -141,7 +141,7 @@ class BomItemTest(TestCase):
|
|||||||
|
|
||||||
def test_substitutes(self):
|
def test_substitutes(self):
|
||||||
"""Tests for BOM item substitutes."""
|
"""Tests for BOM item substitutes."""
|
||||||
# We will make some subtitute parts for the "orphan" part
|
# We will make some substitute parts for the "orphan" part
|
||||||
bom_item = BomItem.objects.get(
|
bom_item = BomItem.objects.get(
|
||||||
part=self.bob,
|
part=self.bob,
|
||||||
sub_part=self.orphan
|
sub_part=self.orphan
|
||||||
|
@ -49,14 +49,14 @@ class CategoryTest(TestCase):
|
|||||||
self.assertEqual(len(self.electronics.children.all()), 3)
|
self.assertEqual(len(self.electronics.children.all()), 3)
|
||||||
self.assertEqual(len(self.mechanical.children.all()), 1)
|
self.assertEqual(len(self.mechanical.children.all()), 1)
|
||||||
|
|
||||||
def test_unique_childs(self):
|
def test_unique_children(self):
|
||||||
"""Test the 'unique_children' functionality."""
|
"""Test the 'unique_children' functionality."""
|
||||||
childs = [item.pk for item in self.electronics.getUniqueChildren()]
|
children = [item.pk for item in self.electronics.getUniqueChildren()]
|
||||||
|
|
||||||
self.assertIn(self.transceivers.id, childs)
|
self.assertIn(self.transceivers.id, children)
|
||||||
self.assertIn(self.ic.id, childs)
|
self.assertIn(self.ic.id, children)
|
||||||
|
|
||||||
self.assertNotIn(self.fasteners.id, childs)
|
self.assertNotIn(self.fasteners.id, children)
|
||||||
|
|
||||||
def test_unique_parents(self):
|
def test_unique_parents(self):
|
||||||
"""Test the 'unique_parents' functionality."""
|
"""Test the 'unique_parents' functionality."""
|
||||||
@ -168,7 +168,7 @@ class CategoryTest(TestCase):
|
|||||||
parts_parameters = self.fasteners.get_parts_parameters(prefetch=fasteners)
|
parts_parameters = self.fasteners.get_parts_parameters(prefetch=fasteners)
|
||||||
part_infos = ['pk', 'name', 'description']
|
part_infos = ['pk', 'name', 'description']
|
||||||
for part_parameter in parts_parameters:
|
for part_parameter in parts_parameters:
|
||||||
# Remove part informations
|
# Remove part information
|
||||||
for item in part_infos:
|
for item in part_infos:
|
||||||
part_parameter.pop(item)
|
part_parameter.pop(item)
|
||||||
self.assertEqual(len(part_parameter), 1)
|
self.assertEqual(len(part_parameter), 1)
|
||||||
|
@ -55,7 +55,7 @@ class TestBomItemMigrations(MigratorTestCase):
|
|||||||
migrate_to = ('part', unit_test.getNewestMigrationFile('part'))
|
migrate_to = ('part', unit_test.getNewestMigrationFile('part'))
|
||||||
|
|
||||||
def prepare(self):
|
def prepare(self):
|
||||||
"""Create intial dataset"""
|
"""Create initial dataset"""
|
||||||
|
|
||||||
Part = self.old_state.apps.get_model('part', 'part')
|
Part = self.old_state.apps.get_model('part', 'part')
|
||||||
BomItem = self.old_state.apps.get_model('part', 'bomitem')
|
BomItem = self.old_state.apps.get_model('part', 'bomitem')
|
||||||
|
@ -515,7 +515,7 @@ class PartSettingsTest(InvenTreeTestCase):
|
|||||||
Part.objects.create(name='zyx', description='A part', IPN='UNIQUE')
|
Part.objects.create(name='zyx', description='A part', IPN='UNIQUE')
|
||||||
|
|
||||||
# However, *blank* / empty IPN values should be allowed, even if duplicates are not
|
# However, *blank* / empty IPN values should be allowed, even if duplicates are not
|
||||||
# Note that leading / trailling whitespace characters are trimmed, too
|
# Note that leading / trailing whitespace characters are trimmed, too
|
||||||
Part.objects.create(name='abc', revision='1', description='A part', IPN=None)
|
Part.objects.create(name='abc', revision='1', description='A part', IPN=None)
|
||||||
Part.objects.create(name='abc', revision='2', description='A part', IPN='')
|
Part.objects.create(name='abc', revision='2', description='A part', IPN='')
|
||||||
Part.objects.create(name='abc', revision='3', description='A part', IPN=None)
|
Part.objects.create(name='abc', revision='3', description='A part', IPN=None)
|
||||||
|
@ -233,7 +233,7 @@ class PartImport(FileManagementFormView):
|
|||||||
image=part_data.get('image', None),
|
image=part_data.get('image', None),
|
||||||
)
|
)
|
||||||
|
|
||||||
# check if theres a category assigned, if not skip this part or else bad things happen
|
# check if there's a category assigned, if not skip this part or else bad things happen
|
||||||
if not optional_matches['Category']:
|
if not optional_matches['Category']:
|
||||||
import_error.append(_("Can't import part {name} because there is no category assigned").format(name=new_part.name))
|
import_error.append(_("Can't import part {name} because there is no category assigned").format(name=new_part.name))
|
||||||
continue
|
continue
|
||||||
@ -260,7 +260,7 @@ class PartImport(FileManagementFormView):
|
|||||||
messages.success(self.request, alert)
|
messages.success(self.request, alert)
|
||||||
if import_error:
|
if import_error:
|
||||||
error_text = '\n'.join([f'<li><strong>{import_error.count(a)}</strong>: {a}</li>' for a in set(import_error)])
|
error_text = '\n'.join([f'<li><strong>{import_error.count(a)}</strong>: {a}</li>' for a in set(import_error)])
|
||||||
messages.error(self.request, f"<strong>{_('Some errors occured:')}</strong><br><ul>{error_text}</ul>")
|
messages.error(self.request, f"<strong>{_('Some errors occurred:')}</strong><br><ul>{error_text}</ul>")
|
||||||
|
|
||||||
return HttpResponseRedirect(reverse('part-index'))
|
return HttpResponseRedirect(reverse('part-index'))
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
"""Apps file for plugin app.
|
"""Apps file for plugin app.
|
||||||
|
|
||||||
This initializes the plugin mechanisms and handles reloading throught the lifecycle.
|
This initializes the plugin mechanisms and handles reloading throughout the lifecycle.
|
||||||
The main code for plugin special sauce is in the plugin registry in `InvenTree/plugin/registry.py`.
|
The main code for plugin special sauce is in the plugin registry in `InvenTree/plugin/registry.py`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -59,7 +59,7 @@ class ActionMixinTests(TestCase):
|
|||||||
"info": None,
|
"info": None,
|
||||||
})
|
})
|
||||||
|
|
||||||
# overriden functions
|
# overridden functions
|
||||||
self.assertEqual(self.action_plugin.perform_action(), self.ACTION_RETURN + 'action')
|
self.assertEqual(self.action_plugin.perform_action(), self.ACTION_RETURN + 'action')
|
||||||
self.assertEqual(self.action_plugin.get_result(), self.ACTION_RETURN + 'result')
|
self.assertEqual(self.action_plugin.get_result(), self.ACTION_RETURN + 'result')
|
||||||
self.assertEqual(self.action_plugin.get_info(), self.ACTION_RETURN + 'info')
|
self.assertEqual(self.action_plugin.get_info(), self.ACTION_RETURN + 'info')
|
||||||
|
@ -48,7 +48,7 @@ class BarcodeAPITest(InvenTreeAPITestCase):
|
|||||||
def test_empty(self):
|
def test_empty(self):
|
||||||
"""Test an empty barcode scan.
|
"""Test an empty barcode scan.
|
||||||
|
|
||||||
Ensure that all required data is in teh respomse.
|
Ensure that all required data is in the respomse.
|
||||||
"""
|
"""
|
||||||
response = self.postBarcode(self.scan_url, '')
|
response = self.postBarcode(self.scan_url, '')
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@ def trigger_event(event, *args, **kwargs):
|
|||||||
# Do nothing if plugins are not enabled
|
# Do nothing if plugins are not enabled
|
||||||
return # pragma: no cover
|
return # pragma: no cover
|
||||||
|
|
||||||
# Make sure the database can be accessed and is not beeing tested rn
|
# Make sure the database can be accessed and is not being tested rn
|
||||||
if not canAppAccessDatabase() and not settings.PLUGIN_TESTING_EVENTS:
|
if not canAppAccessDatabase() and not settings.PLUGIN_TESTING_EVENTS:
|
||||||
logger.debug(f"Ignoring triggered event '{event}' - database not ready")
|
logger.debug(f"Ignoring triggered event '{event}' - database not ready")
|
||||||
return
|
return
|
||||||
|
@ -71,7 +71,7 @@ class AppMixin:
|
|||||||
"""
|
"""
|
||||||
# unregister models from admin
|
# unregister models from admin
|
||||||
for plugin_path in registry.installed_apps:
|
for plugin_path in registry.installed_apps:
|
||||||
models = [] # the modelrefs need to be collected as poping an item in a iter is not welcomed
|
models = [] # the modelrefs need to be collected as popping an item in a iter is not welcomed
|
||||||
app_name = plugin_path.split('.')[-1]
|
app_name = plugin_path.split('.')[-1]
|
||||||
try:
|
try:
|
||||||
app_config = apps.get_app_config(app_name)
|
app_config = apps.get_app_config(app_name)
|
||||||
@ -128,7 +128,7 @@ class AppMixin:
|
|||||||
|
|
||||||
# reload models if they were set
|
# reload models if they were set
|
||||||
# models_module gets set if models were defined - even after multiple loads
|
# models_module gets set if models were defined - even after multiple loads
|
||||||
# on a reload the models registery is empty but models_module is not
|
# on a reload the models registry is empty but models_module is not
|
||||||
if app_config.models_module and len(app_config.models) == 0:
|
if app_config.models_module and len(app_config.models) == 0:
|
||||||
reload(app_config.models_module)
|
reload(app_config.models_module)
|
||||||
|
|
||||||
@ -140,7 +140,7 @@ class AppMixin:
|
|||||||
|
|
||||||
# reload admin if at least one model is not registered
|
# reload admin if at least one model is not registered
|
||||||
# models are registered with admin in the 'admin.py' file - so we check
|
# models are registered with admin in the 'admin.py' file - so we check
|
||||||
# if the app_config has an admin module before trying to laod it
|
# if the app_config has an admin module before trying to load it
|
||||||
if model_not_reg and hasattr(app_config.module, 'admin'):
|
if model_not_reg and hasattr(app_config.module, 'admin'):
|
||||||
reload(app_config.module.admin)
|
reload(app_config.module.admin)
|
||||||
|
|
||||||
@ -148,7 +148,7 @@ class AppMixin:
|
|||||||
def _get_plugin_path(cls, plugin):
|
def _get_plugin_path(cls, plugin):
|
||||||
"""Parse plugin path.
|
"""Parse plugin path.
|
||||||
|
|
||||||
The input can be eiter:
|
The input can be either:
|
||||||
- a local file / dir
|
- a local file / dir
|
||||||
- a package
|
- a package
|
||||||
"""
|
"""
|
||||||
|
@ -218,7 +218,7 @@ class APICallMixin:
|
|||||||
|
|
||||||
Steps to set up:
|
Steps to set up:
|
||||||
1. Add this mixin before (left of) SettingsMixin and PluginBase
|
1. Add this mixin before (left of) SettingsMixin and PluginBase
|
||||||
2. Add two settings for the required url and token/passowrd (use `SettingsMixin`)
|
2. Add two settings for the required url and token/password (use `SettingsMixin`)
|
||||||
3. Save the references to keys of the settings in `API_URL_SETTING` and `API_TOKEN_SETTING`
|
3. Save the references to keys of the settings in `API_URL_SETTING` and `API_TOKEN_SETTING`
|
||||||
4. (Optional) Set `API_TOKEN` to the name required for the token by the external API - Defaults to `Bearer`
|
4. (Optional) Set `API_TOKEN` to the name required for the token by the external API - Defaults to `Bearer`
|
||||||
5. (Optional) Override the `api_url` property method if the setting needs to be extended
|
5. (Optional) Override the `api_url` property method if the setting needs to be extended
|
||||||
|
@ -43,7 +43,7 @@ class LocateMixin:
|
|||||||
|
|
||||||
An attempt is only made if the StockItem is *in stock*
|
An attempt is only made if the StockItem is *in stock*
|
||||||
|
|
||||||
Note: A custom implemenation could always change this behaviour
|
Note: A custom implementation could always change this behaviour
|
||||||
"""
|
"""
|
||||||
logger.info(f"LocateMixin: Attempting to locate StockItem pk={item_pk}")
|
logger.info(f"LocateMixin: Attempting to locate StockItem pk={item_pk}")
|
||||||
|
|
||||||
|
@ -20,7 +20,7 @@ class TestInvenTreeBarcode(InvenTreeAPITestCase):
|
|||||||
]
|
]
|
||||||
|
|
||||||
def test_assign_errors(self):
|
def test_assign_errors(self):
|
||||||
"""Test error cases for assigment action."""
|
"""Test error cases for assignment action."""
|
||||||
|
|
||||||
def test_assert_error(barcode_data):
|
def test_assert_error(barcode_data):
|
||||||
response = self.post(
|
response = self.post(
|
||||||
@ -44,7 +44,7 @@ class TestInvenTreeBarcode(InvenTreeAPITestCase):
|
|||||||
test_assert_error('{"part": 10004}')
|
test_assert_error('{"part": 10004}')
|
||||||
|
|
||||||
def assign(self, data, expected_code=None):
|
def assign(self, data, expected_code=None):
|
||||||
"""Peform a 'barcode assign' request"""
|
"""Perform a 'barcode assign' request"""
|
||||||
|
|
||||||
return self.post(
|
return self.post(
|
||||||
reverse('api-barcode-link'),
|
reverse('api-barcode-link'),
|
||||||
|
@ -32,7 +32,7 @@ class InvenTreeCoreNotificationsPlugin(SettingsContentMixin, SettingsMixin, Inve
|
|||||||
NAME = "InvenTreeCoreNotificationsPlugin"
|
NAME = "InvenTreeCoreNotificationsPlugin"
|
||||||
TITLE = _("InvenTree Notifications")
|
TITLE = _("InvenTree Notifications")
|
||||||
AUTHOR = _('InvenTree contributors')
|
AUTHOR = _('InvenTree contributors')
|
||||||
DESCRIPTION = _('Integrated outgoing notificaton methods')
|
DESCRIPTION = _('Integrated outgoing notification methods')
|
||||||
VERSION = "1.0.0"
|
VERSION = "1.0.0"
|
||||||
|
|
||||||
SETTINGS = {
|
SETTINGS = {
|
||||||
|
@ -25,7 +25,7 @@ class IntegrationPluginError(Exception):
|
|||||||
"""Init a plugin error.
|
"""Init a plugin error.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
path: Path on which the error occured - used to find out which plugin it was
|
path: Path on which the error occurred - used to find out which plugin it was
|
||||||
message: The original error message
|
message: The original error message
|
||||||
"""
|
"""
|
||||||
self.path = path
|
self.path = path
|
||||||
|
@ -32,7 +32,7 @@ class MetaBase:
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
key (str): key for the value
|
key (str): key for the value
|
||||||
old_key (str, optional): depreceated key - will throw warning
|
old_key (str, optional): deprecated key - will throw warning
|
||||||
__default (optional): Value if nothing with key can be found. Defaults to None.
|
__default (optional): Value if nothing with key can be found. Defaults to None.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
|
@ -58,7 +58,7 @@ class PluginsRegistry:
|
|||||||
self.errors = {} # Holds discovering errors
|
self.errors = {} # Holds discovering errors
|
||||||
|
|
||||||
# flags
|
# flags
|
||||||
self.is_loading = False # Are plugins beeing loaded right now
|
self.is_loading = False # Are plugins being loaded right now
|
||||||
self.apps_loading = True # Marks if apps were reloaded yet
|
self.apps_loading = True # Marks if apps were reloaded yet
|
||||||
self.git_is_modern = True # Is a modern version of git available
|
self.git_is_modern = True # Is a modern version of git available
|
||||||
|
|
||||||
@ -152,7 +152,7 @@ class PluginsRegistry:
|
|||||||
print('[PLUGIN] Max retries, breaking loading')
|
print('[PLUGIN] Max retries, breaking loading')
|
||||||
break
|
break
|
||||||
if settings.PLUGIN_TESTING:
|
if settings.PLUGIN_TESTING:
|
||||||
print(f'[PLUGIN] Above error occured during testing - {retry_counter}/{settings.PLUGIN_RETRY} retries left')
|
print(f'[PLUGIN] Above error occurred during testing - {retry_counter}/{settings.PLUGIN_RETRY} retries left')
|
||||||
|
|
||||||
# now the loading will re-start up with init
|
# now the loading will re-start up with init
|
||||||
|
|
||||||
@ -199,7 +199,7 @@ class PluginsRegistry:
|
|||||||
full_reload (bool, optional): Reload everything - including plugin mechanism. Defaults to False.
|
full_reload (bool, optional): Reload everything - including plugin mechanism. Defaults to False.
|
||||||
force_reload (bool, optional): Also reload base apps. Defaults to False.
|
force_reload (bool, optional): Also reload base apps. Defaults to False.
|
||||||
"""
|
"""
|
||||||
# Do not reload whe currently loading
|
# Do not reload when currently loading
|
||||||
if self.is_loading:
|
if self.is_loading:
|
||||||
return # pragma: no cover
|
return # pragma: no cover
|
||||||
|
|
||||||
|
@ -10,6 +10,6 @@ class NoIntegrationPlugin(InvenTreePlugin):
|
|||||||
|
|
||||||
|
|
||||||
class WrongIntegrationPlugin(UrlsMixin, InvenTreePlugin):
|
class WrongIntegrationPlugin(UrlsMixin, InvenTreePlugin):
|
||||||
"""A basic wron plugin with urls."""
|
"""A basic wrong plugin with urls."""
|
||||||
|
|
||||||
NAME = "WrongIntegrationPlugin"
|
NAME = "WrongIntegrationPlugin"
|
||||||
|
@ -152,7 +152,7 @@ class PluginConfigInstallSerializer(serializers.Serializer):
|
|||||||
ret['result'] = str(error.output, 'utf-8')
|
ret['result'] = str(error.output, 'utf-8')
|
||||||
ret['error'] = True
|
ret['error'] = True
|
||||||
|
|
||||||
# save plugin to plugin_file if installed successfull
|
# save plugin to plugin_file if installed successful
|
||||||
if success:
|
if success:
|
||||||
# Read content of plugin file
|
# Read content of plugin file
|
||||||
plg_lines = open(settings.PLUGIN_FILE).readlines()
|
plg_lines = open(settings.PLUGIN_FILE).readlines()
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
"""This module provides template tags for handeling plugins."""
|
"""This module provides template tags for handling plugins."""
|
||||||
|
|
||||||
from django import template
|
from django import template
|
||||||
from django.conf import settings as djangosettings
|
from django.conf import settings as djangosettings
|
||||||
@ -40,7 +40,7 @@ def plugin_settings_content(context, plugin, *args, **kwargs):
|
|||||||
|
|
||||||
@register.simple_tag()
|
@register.simple_tag()
|
||||||
def mixin_enabled(plugin, key, *args, **kwargs):
|
def mixin_enabled(plugin, key, *args, **kwargs):
|
||||||
"""Is the mixin registerd and configured in the plugin?"""
|
"""Is the mixin registered and configured in the plugin?"""
|
||||||
return plugin.mixin_enabled(key)
|
return plugin.mixin_enabled(key)
|
||||||
|
|
||||||
|
|
||||||
|
@ -139,7 +139,7 @@ class PluginDetailAPITest(PluginMixin, InvenTreeAPITestCase):
|
|||||||
}, follow=True)
|
}, follow=True)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
# deactivate plugin - deactivate again -> nothing will hapen but the nothing 'changed' function is triggered
|
# deactivate plugin - deactivate again -> nothing will happen but the nothing 'changed' function is triggered
|
||||||
response = self.client.post(url, {
|
response = self.client.post(url, {
|
||||||
'action': 'plugin_deactivate',
|
'action': 'plugin_deactivate',
|
||||||
'index': 0,
|
'index': 0,
|
||||||
|
@ -202,7 +202,7 @@ class RegistryTests(TestCase):
|
|||||||
def run_package_test(self, directory):
|
def run_package_test(self, directory):
|
||||||
"""General runner for testing package based installs."""
|
"""General runner for testing package based installs."""
|
||||||
|
|
||||||
# Patch environment varible to add dir
|
# Patch environment variable to add dir
|
||||||
envs = {'INVENTREE_PLUGIN_TEST_DIR': directory}
|
envs = {'INVENTREE_PLUGIN_TEST_DIR': directory}
|
||||||
with mock.patch.dict(os.environ, envs):
|
with mock.patch.dict(os.environ, envs):
|
||||||
# Reload to redicsover plugins
|
# Reload to redicsover plugins
|
||||||
|
@ -34,7 +34,7 @@ class ReportConfig(AppConfig):
|
|||||||
self.create_default_return_order_reports()
|
self.create_default_return_order_reports()
|
||||||
|
|
||||||
def create_default_reports(self, model, reports):
|
def create_default_reports(self, model, reports):
|
||||||
"""Copy defualt report files across to the media directory."""
|
"""Copy default report files across to the media directory."""
|
||||||
# Source directory for report templates
|
# Source directory for report templates
|
||||||
src_dir = Path(__file__).parent.joinpath(
|
src_dir = Path(__file__).parent.joinpath(
|
||||||
'templates',
|
'templates',
|
||||||
|
@ -409,7 +409,7 @@ class BillOfMaterialsReport(ReportTemplateBase):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def getSubdir(cls):
|
def getSubdir(cls):
|
||||||
"""Retun the directory where BillOfMaterialsReport templates are located"""
|
"""Return the directory where BillOfMaterialsReport templates are located"""
|
||||||
return 'bom'
|
return 'bom'
|
||||||
|
|
||||||
filters = models.CharField(
|
filters = models.CharField(
|
||||||
@ -481,7 +481,7 @@ class SalesOrderReport(ReportTemplateBase):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def getSubdir(cls):
|
def getSubdir(cls):
|
||||||
"""Retun the subdirectory where SalesOrderReport templates are located"""
|
"""Return the subdirectory where SalesOrderReport templates are located"""
|
||||||
return 'salesorder'
|
return 'salesorder'
|
||||||
|
|
||||||
filters = models.CharField(
|
filters = models.CharField(
|
||||||
|
@ -418,7 +418,7 @@ class BuildReportTest(ReportTest):
|
|||||||
|
|
||||||
|
|
||||||
class BOMReportTest(ReportTest):
|
class BOMReportTest(ReportTest):
|
||||||
"""Unit test class fot the BillOfMaterialsReport model"""
|
"""Unit test class for the BillOfMaterialsReport model"""
|
||||||
model = report_models.BillOfMaterialsReport
|
model = report_models.BillOfMaterialsReport
|
||||||
|
|
||||||
list_url = 'api-bom-report-list'
|
list_url = 'api-bom-report-list'
|
||||||
|
@ -364,7 +364,7 @@ class StockFilter(rest_filters.FilterSet):
|
|||||||
]
|
]
|
||||||
|
|
||||||
# Relationship filters
|
# Relationship filters
|
||||||
manufactuer = rest_filters.ModelChoiceFilter(label='Manufacturer', queryset=Company.objects.filter(is_manufacturer=True), field_name='manufacturer_part__manufacturer')
|
manufacturer = rest_filters.ModelChoiceFilter(label='Manufacturer', queryset=Company.objects.filter(is_manufacturer=True), field_name='manufacturer_part__manufacturer')
|
||||||
supplier = rest_filters.ModelChoiceFilter(label='Supplier', queryset=Company.objects.filter(is_supplier=True), field_name='supplier_part__supplier')
|
supplier = rest_filters.ModelChoiceFilter(label='Supplier', queryset=Company.objects.filter(is_supplier=True), field_name='supplier_part__supplier')
|
||||||
|
|
||||||
# Part name filters
|
# Part name filters
|
||||||
@ -827,7 +827,7 @@ class StockList(APIDownloadMixin, ListCreateDestroyAPIView):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
Determine the response type based on the request.
|
Determine the response type based on the request.
|
||||||
a) For HTTP requests (e.g. via the browseable API) return a DRF response
|
a) For HTTP requests (e.g. via the browsable API) return a DRF response
|
||||||
b) For AJAX requests, simply return a JSON rendered response.
|
b) For AJAX requests, simply return a JSON rendered response.
|
||||||
|
|
||||||
Note: b) is about 100x quicker than a), because the DRF framework adds a lot of cruft
|
Note: b) is about 100x quicker than a), because the DRF framework adds a lot of cruft
|
||||||
|
@ -25,8 +25,8 @@ def delete_scheduled(apps, schema_editor):
|
|||||||
|
|
||||||
# Ensure any parent / child relationships are updated!
|
# Ensure any parent / child relationships are updated!
|
||||||
for item in items:
|
for item in items:
|
||||||
childs = StockItem.objects.filter(parent=item)
|
children = StockItem.objects.filter(parent=item)
|
||||||
childs.update(parent=item.parent)
|
children.update(parent=item.parent)
|
||||||
|
|
||||||
item.delete()
|
item.delete()
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@ def update_stock_history(apps, schema_editor):
|
|||||||
n = 0
|
n = 0
|
||||||
|
|
||||||
for item in items:
|
for item in items:
|
||||||
# Find newest relevent history
|
# Find newest relevant history
|
||||||
history = StockItemTracking.objects.filter(
|
history = StockItemTracking.objects.filter(
|
||||||
item=item,
|
item=item,
|
||||||
tracking_type__in=[StockHistoryCode.SENT_TO_CUSTOMER, StockHistoryCode.SHIPPED_AGAINST_SALES_ORDER]
|
tracking_type__in=[StockHistoryCode.SENT_TO_CUSTOMER, StockHistoryCode.SHIPPED_AGAINST_SALES_ORDER]
|
||||||
|
@ -256,7 +256,7 @@ def generate_batch_code():
|
|||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
|
|
||||||
# Pass context data through to the template randering.
|
# Pass context data through to the template randering.
|
||||||
# The folowing context variables are availble for custom batch code generation
|
# The following context variables are available for custom batch code generation
|
||||||
context = {
|
context = {
|
||||||
'date': now,
|
'date': now,
|
||||||
'year': now.year,
|
'year': now.year,
|
||||||
@ -624,7 +624,7 @@ class StockItem(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, commo
|
|||||||
|
|
||||||
except PartModels.Part.DoesNotExist:
|
except PartModels.Part.DoesNotExist:
|
||||||
# This gets thrown if self.supplier_part is null
|
# This gets thrown if self.supplier_part is null
|
||||||
# TODO - Find a test than can be perfomed...
|
# TODO - Find a test than can be performed...
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Ensure that the item cannot be assigned to itself
|
# Ensure that the item cannot be assigned to itself
|
||||||
@ -2109,7 +2109,7 @@ class StockItemTracking(models.Model):
|
|||||||
"""Stock tracking entry - used for tracking history of a particular StockItem.
|
"""Stock tracking entry - used for tracking history of a particular StockItem.
|
||||||
|
|
||||||
Note: 2021-05-11
|
Note: 2021-05-11
|
||||||
The legacy StockTrackingItem model contained very litle information about the "history" of the item.
|
The legacy StockTrackingItem model contained very little information about the "history" of the item.
|
||||||
In fact, only the "quantity" of the item was recorded at each interaction.
|
In fact, only the "quantity" of the item was recorded at each interaction.
|
||||||
Also, the "title" was translated at time of generation, and thus was not really translateable.
|
Also, the "title" was translated at time of generation, and thus was not really translateable.
|
||||||
The "new" system tracks all 'delta' changes to the model,
|
The "new" system tracks all 'delta' changes to the model,
|
||||||
|
@ -1241,7 +1241,7 @@ class StockTestResultTest(StockAPITestCase):
|
|||||||
"""Tests for StockTestResult APIs."""
|
"""Tests for StockTestResult APIs."""
|
||||||
|
|
||||||
def get_url(self):
|
def get_url(self):
|
||||||
"""Helper funtion to get test-result api url."""
|
"""Helper function to get test-result api url."""
|
||||||
return reverse('api-stock-test-result-list')
|
return reverse('api-stock-test-result-list')
|
||||||
|
|
||||||
def test_list(self):
|
def test_list(self):
|
||||||
|
@ -97,16 +97,16 @@ class StockOwnershipTest(StockViewTestCase):
|
|||||||
InvenTreeSetting.set_setting('STOCK_OWNERSHIP_CONTROL', True, self.user)
|
InvenTreeSetting.set_setting('STOCK_OWNERSHIP_CONTROL', True, self.user)
|
||||||
self.assertEqual(True, InvenTreeSetting.get_setting('STOCK_OWNERSHIP_CONTROL'))
|
self.assertEqual(True, InvenTreeSetting.get_setting('STOCK_OWNERSHIP_CONTROL'))
|
||||||
|
|
||||||
def assert_ownership(self, assertio: bool = True, user=None):
|
def assert_ownership(self, assertion: bool = True, user=None):
|
||||||
"""Helper function to check ownership control."""
|
"""Helper function to check ownership control."""
|
||||||
if user is None:
|
if user is None:
|
||||||
user = self.user
|
user = self.user
|
||||||
|
|
||||||
item = StockItem.objects.get(pk=self.test_item_id)
|
item = StockItem.objects.get(pk=self.test_item_id)
|
||||||
self.assertEqual(assertio, item.check_ownership(user))
|
self.assertEqual(assertion, item.check_ownership(user))
|
||||||
|
|
||||||
location = StockLocation.objects.get(pk=self.test_location_id)
|
location = StockLocation.objects.get(pk=self.test_location_id)
|
||||||
self.assertEqual(assertio, location.check_ownership(user))
|
self.assertEqual(assertion, location.check_ownership(user))
|
||||||
|
|
||||||
def assert_api_change(self):
|
def assert_api_change(self):
|
||||||
"""Helper function to get response to API change."""
|
"""Helper function to get response to API change."""
|
||||||
|
@ -51,7 +51,7 @@ class StockTestBase(InvenTreeTestCase):
|
|||||||
|
|
||||||
|
|
||||||
class StockTest(StockTestBase):
|
class StockTest(StockTestBase):
|
||||||
"""Tests to ensure that the stock location tree functions correcly."""
|
"""Tests to ensure that the stock location tree functions correctly."""
|
||||||
|
|
||||||
def test_pathstring(self):
|
def test_pathstring(self):
|
||||||
"""Check that pathstring updates occur as expected"""
|
"""Check that pathstring updates occur as expected"""
|
||||||
@ -145,7 +145,7 @@ class StockTest(StockTestBase):
|
|||||||
item.save()
|
item.save()
|
||||||
item.full_clean()
|
item.full_clean()
|
||||||
|
|
||||||
# Check that valid URLs pass - and check custon schemes
|
# Check that valid URLs pass - and check custom schemes
|
||||||
for good_url in [
|
for good_url in [
|
||||||
'https://test.com',
|
'https://test.com',
|
||||||
'https://digikey.com/datasheets?file=1010101010101.bin',
|
'https://digikey.com/datasheets?file=1010101010101.bin',
|
||||||
@ -309,12 +309,12 @@ class StockTest(StockTestBase):
|
|||||||
|
|
||||||
self.assertFalse(self.drawer2.has_children)
|
self.assertFalse(self.drawer2.has_children)
|
||||||
|
|
||||||
childs = [item.pk for item in self.office.getUniqueChildren()]
|
children = [item.pk for item in self.office.getUniqueChildren()]
|
||||||
|
|
||||||
self.assertIn(self.drawer1.id, childs)
|
self.assertIn(self.drawer1.id, children)
|
||||||
self.assertIn(self.drawer2.id, childs)
|
self.assertIn(self.drawer2.id, children)
|
||||||
|
|
||||||
self.assertNotIn(self.bathroom.id, childs)
|
self.assertNotIn(self.bathroom.id, children)
|
||||||
|
|
||||||
def test_items(self):
|
def test_items(self):
|
||||||
"""Test has_items."""
|
"""Test has_items."""
|
||||||
@ -646,7 +646,7 @@ class StockTest(StockTestBase):
|
|||||||
|
|
||||||
self.assertEqual(item.serial_int, 0)
|
self.assertEqual(item.serial_int, 0)
|
||||||
|
|
||||||
# Next, test for incremenet / decrement functionality
|
# Next, test for increment / decrement functionality
|
||||||
item.serial = 100
|
item.serial = 100
|
||||||
item.save()
|
item.save()
|
||||||
|
|
||||||
@ -754,7 +754,7 @@ class StockTest(StockTestBase):
|
|||||||
"""Unit tests for stock location tree structure (MPTT).
|
"""Unit tests for stock location tree structure (MPTT).
|
||||||
|
|
||||||
Ensure that the MPTT structure is rebuilt correctly,
|
Ensure that the MPTT structure is rebuilt correctly,
|
||||||
and the corrent ancestor tree is observed.
|
and the current ancestor tree is observed.
|
||||||
|
|
||||||
Ref: https://github.com/inventree/InvenTree/issues/2636
|
Ref: https://github.com/inventree/InvenTree/issues/2636
|
||||||
Ref: https://github.com/inventree/InvenTree/issues/2733
|
Ref: https://github.com/inventree/InvenTree/issues/2733
|
||||||
@ -1185,7 +1185,7 @@ class TestResultTest(StockTestBase):
|
|||||||
tests = item.testResultMap(include_installed=False)
|
tests = item.testResultMap(include_installed=False)
|
||||||
self.assertEqual(len(tests), 3)
|
self.assertEqual(len(tests), 3)
|
||||||
|
|
||||||
# There are no "sub items" intalled at this stage
|
# There are no "sub items" installed at this stage
|
||||||
tests = item.testResultMap(include_installed=False)
|
tests = item.testResultMap(include_installed=False)
|
||||||
self.assertEqual(len(tests), 3)
|
self.assertEqual(len(tests), 3)
|
||||||
|
|
||||||
|
@ -44,7 +44,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>{{ plugin_key }}</td>
|
<td>{{ plugin_key }}</td>
|
||||||
{% trans "Unvailable" as no_info %}
|
{% trans "Unavailable" as no_info %}
|
||||||
<td>
|
<td>
|
||||||
{% if plugin.author %}
|
{% if plugin.author %}
|
||||||
{{ plugin.author }}
|
{{ plugin.author }}
|
||||||
|
@ -94,7 +94,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<p>{% trans "Some languages are not complete" %}
|
<p>{% trans "Some languages are not complete" %}
|
||||||
{% if ALL_LANG %}
|
{% if ALL_LANG %}
|
||||||
. <a href="{% url 'settings' %}">{% trans "Show only sufficent" %}</a>
|
. <a href="{% url 'settings' %}">{% trans "Show only sufficient" %}</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
{% trans "and hidden." %} <a href="?alllang">{% trans "Show them too" %}</a>
|
{% trans "and hidden." %} <a href="?alllang">{% trans "Show them too" %}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -95,7 +95,7 @@ $(document).ready(function () {
|
|||||||
|
|
||||||
{% if messages %}
|
{% if messages %}
|
||||||
{% for message in messages %}
|
{% for message in messages %}
|
||||||
showMessage("{{ messsage }}");
|
showMessage("{{ message }}");
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
@ -226,7 +226,7 @@ function enableBreadcrumbTree(options) {
|
|||||||
$('#breadcrumb-tree-toggle').click(function() {
|
$('#breadcrumb-tree-toggle').click(function() {
|
||||||
// Add callback to "collapse" and "expand" the sidebar
|
// Add callback to "collapse" and "expand" the sidebar
|
||||||
|
|
||||||
// Toggle treeview visibilty
|
// Toggle treeview visibility
|
||||||
$('#breadcrumb-tree-collapse').toggle();
|
$('#breadcrumb-tree-collapse').toggle();
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@ -111,7 +111,7 @@ function onBarcodeScanClicked(e) {
|
|||||||
|
|
||||||
function onCameraAvailable(hasCamera, options) {
|
function onCameraAvailable(hasCamera, options) {
|
||||||
if (hasCamera && global_settings.BARCODE_WEBCAM_SUPPORT) {
|
if (hasCamera && global_settings.BARCODE_WEBCAM_SUPPORT) {
|
||||||
// Camera is only acccessible if page is served over secure connection
|
// Camera is only accessible if page is served over secure connection
|
||||||
if (window.isSecureContext == true) {
|
if (window.isSecureContext == true) {
|
||||||
qrScanner = new Html5Qrcode('barcode_scan_video', {
|
qrScanner = new Html5Qrcode('barcode_scan_video', {
|
||||||
useBarCodeDetectorIfSupported: true,
|
useBarCodeDetectorIfSupported: true,
|
||||||
|
@ -1076,7 +1076,7 @@ function loadBuildOutputTable(build_info, options={}) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Callack for the "unallocate" button
|
// Callback for the "unallocate" button
|
||||||
$(table).find('.button-output-unallocate').click(function() {
|
$(table).find('.button-output-unallocate').click(function() {
|
||||||
var pk = $(this).attr('pk');
|
var pk = $(this).attr('pk');
|
||||||
|
|
||||||
@ -1738,7 +1738,7 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
|
|||||||
// Reload table data
|
// Reload table data
|
||||||
$(table).bootstrapTable('load', bom_items);
|
$(table).bootstrapTable('load', bom_items);
|
||||||
|
|
||||||
// Find the top-level progess bar for this build output
|
// Find the top-level progress bar for this build output
|
||||||
var output_progress_bar = $(`#output-progress-${outputId}`);
|
var output_progress_bar = $(`#output-progress-${outputId}`);
|
||||||
|
|
||||||
if (output_progress_bar.exists()) {
|
if (output_progress_bar.exists()) {
|
||||||
@ -1789,7 +1789,7 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function requiredQuantity(row) {
|
function requiredQuantity(row) {
|
||||||
// Return the requied quantity for a given row
|
// Return the required quantity for a given row
|
||||||
|
|
||||||
var quantity = 0;
|
var quantity = 0;
|
||||||
|
|
||||||
@ -1946,7 +1946,7 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
|
|||||||
},
|
},
|
||||||
buttons: constructExpandCollapseButtons(table),
|
buttons: constructExpandCollapseButtons(table),
|
||||||
detailFormatter: function(index, row, element) {
|
detailFormatter: function(index, row, element) {
|
||||||
// Contruct an 'inner table' which shows which stock items have been allocated
|
// Construct an 'inner table' which shows which stock items have been allocated
|
||||||
|
|
||||||
var subTableId = `allocation-table-${outputId}-${row.pk}`;
|
var subTableId = `allocation-table-${outputId}-${row.pk}`;
|
||||||
|
|
||||||
|
@ -289,7 +289,7 @@ function editSupplierPart(part, options={}) {
|
|||||||
/*
|
/*
|
||||||
* Delete one or more SupplierPart objects from the database.
|
* Delete one or more SupplierPart objects from the database.
|
||||||
* - User will be provided with a modal form, showing all the parts to be deleted.
|
* - User will be provided with a modal form, showing all the parts to be deleted.
|
||||||
* - Delete operations are performed sequentialy, not simultaneously
|
* - Delete operations are performed sequentially, not simultaneously
|
||||||
*/
|
*/
|
||||||
function deleteSupplierParts(parts, options={}) {
|
function deleteSupplierParts(parts, options={}) {
|
||||||
|
|
||||||
@ -785,7 +785,7 @@ function loadContactTable(table, options={}) {
|
|||||||
|
|
||||||
/* Delete one or more ManufacturerPart objects from the database.
|
/* Delete one or more ManufacturerPart objects from the database.
|
||||||
* - User will be provided with a modal form, showing all the parts to be deleted.
|
* - User will be provided with a modal form, showing all the parts to be deleted.
|
||||||
* - Delete operations are performed sequentialy, not simultaneously
|
* - Delete operations are performed sequentially, not simultaneously
|
||||||
*/
|
*/
|
||||||
function deleteManufacturerParts(selections, options={}) {
|
function deleteManufacturerParts(selections, options={}) {
|
||||||
|
|
||||||
|
@ -1183,7 +1183,7 @@ function handleFormSuccess(response, options) {
|
|||||||
var msg_target = null;
|
var msg_target = null;
|
||||||
|
|
||||||
if (persist) {
|
if (persist) {
|
||||||
// If the modal is persistant, the target for any messages should be the modal!
|
// If the modal is persistent, the target for any messages should be the modal!
|
||||||
msg_target = $(options.modal).find('#pre-form-content');
|
msg_target = $(options.modal).find('#pre-form-content');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1944,7 +1944,7 @@ function initializeRelatedField(field, fields, options={}) {
|
|||||||
var html = renderModelData(name, field.model, data, field);
|
var html = renderModelData(name, field.model, data, field);
|
||||||
return $(html);
|
return $(html);
|
||||||
} else {
|
} else {
|
||||||
// Return a simple renderering
|
// Return a simple rendering
|
||||||
console.warn(`templateResult() missing 'field.model' for '${name}'`);
|
console.warn(`templateResult() missing 'field.model' for '${name}'`);
|
||||||
return `${name} - ${item.id}`;
|
return `${name} - ${item.id}`;
|
||||||
}
|
}
|
||||||
@ -1974,7 +1974,7 @@ function initializeRelatedField(field, fields, options={}) {
|
|||||||
var html = renderModelData(name, field.model, data, field);
|
var html = renderModelData(name, field.model, data, field);
|
||||||
return $(html);
|
return $(html);
|
||||||
} else {
|
} else {
|
||||||
// Return a simple renderering
|
// Return a simple rendering
|
||||||
console.warn(`templateSelection() missing 'field.model' for '${name}'`);
|
console.warn(`templateSelection() missing 'field.model' for '${name}'`);
|
||||||
return `${name} - ${item.id}`;
|
return `${name} - ${item.id}`;
|
||||||
}
|
}
|
||||||
@ -2026,7 +2026,7 @@ function initializeRelatedField(field, fields, options={}) {
|
|||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Set the value of a select2 instace for a "related field",
|
* Set the value of a select2 instance for a "related field",
|
||||||
* e.g. with data returned from a secondary modal
|
* e.g. with data returned from a secondary modal
|
||||||
*
|
*
|
||||||
* arguments:
|
* arguments:
|
||||||
@ -2431,7 +2431,7 @@ function constructInputOptions(name, classes, type, parameters, options={}) {
|
|||||||
opts.push(`value='${parameters.value}'`);
|
opts.push(`value='${parameters.value}'`);
|
||||||
}
|
}
|
||||||
} else if (parameters.default != null) {
|
} else if (parameters.default != null) {
|
||||||
// Otherwise, a defualt value?
|
// Otherwise, a default value?
|
||||||
opts.push(`value='${parameters.default}'`);
|
opts.push(`value='${parameters.default}'`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -322,7 +322,7 @@ function makeRemoveButton(cls, pk, title, options={}) {
|
|||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Render a progessbar!
|
* Render a progressbar!
|
||||||
*
|
*
|
||||||
* @param value is the current value of the progress bar
|
* @param value is the current value of the progress bar
|
||||||
* @param maximum is the maximum value of the progress bar
|
* @param maximum is the maximum value of the progress bar
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user