mirror of
https://github.com/inventree/InvenTree.git
synced 2025-12-19 13:20:37 -06:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b0b05e479a | ||
|
|
003ea5de71 | ||
|
|
4e8c59cf2a | ||
|
|
f42146bc0a | ||
|
|
065aca46cd | ||
|
|
89be7f2699 | ||
|
|
d46505671f |
@@ -13,6 +13,7 @@ function detect_docker() {
|
||||
else
|
||||
DOCKER="no"
|
||||
fi
|
||||
echo "# POI04| Running in docker: ${DOCKER}"
|
||||
}
|
||||
|
||||
function detect_initcmd() {
|
||||
@@ -30,6 +31,7 @@ function detect_initcmd() {
|
||||
if [ "${DOCKER}" == "yes" ]; then
|
||||
INIT_CMD="initctl"
|
||||
fi
|
||||
echo "# POI05| Using init command: ${INIT_CMD}"
|
||||
}
|
||||
|
||||
function detect_ip() {
|
||||
@@ -37,36 +39,36 @@ function detect_ip() {
|
||||
|
||||
if [ "${SETUP_NO_CALLS}" == "true" ]; then
|
||||
# Use local IP address
|
||||
echo "# Getting the IP address of the first local IP address"
|
||||
echo "# POI06| Getting the IP address of the first local IP address"
|
||||
export INVENTREE_IP=$(hostname -I | awk '{print $1}')
|
||||
else
|
||||
# Use web service to get the IP address
|
||||
echo "# Getting the IP address of the server via web service"
|
||||
echo "# POI06| Getting the IP address of the server via web service"
|
||||
export INVENTREE_IP=$(curl -s https://checkip.amazonaws.com)
|
||||
fi
|
||||
|
||||
echo "IP address is ${INVENTREE_IP}"
|
||||
echo "# POI06| IP address is ${INVENTREE_IP}"
|
||||
}
|
||||
|
||||
function detect_python() {
|
||||
# Detect if there is already a python version installed in /opt/inventree/env/lib
|
||||
if test -f "${APP_HOME}/env/bin/python"; then
|
||||
echo "# Python environment already present"
|
||||
echo "# POI07| Python environment already present"
|
||||
# Extract earliest python version initialised from /opt/inventree/env/lib
|
||||
SETUP_PYTHON=$(ls -1 ${APP_HOME}/env/bin/python* | sort | head -n 1)
|
||||
echo "# Found earlier used version: ${SETUP_PYTHON}"
|
||||
echo "# POI07| Found earlier used version: ${SETUP_PYTHON}"
|
||||
else
|
||||
echo "# No python environment found - using environment variable: ${SETUP_PYTHON}"
|
||||
echo "# POI07| No python environment found - using environment variable: ${SETUP_PYTHON}"
|
||||
fi
|
||||
|
||||
# Try to detect a python between 3.9 and 3.12 in reverse order
|
||||
if [ -z "$(which ${SETUP_PYTHON})" ]; then
|
||||
echo "# Trying to detecting python3.${PYTHON_FROM} to python3.${PYTHON_TO} - using newest version"
|
||||
echo "# POI07| Trying to detecting python3.${PYTHON_FROM} to python3.${PYTHON_TO} - using newest version"
|
||||
for i in $(seq $PYTHON_TO -1 $PYTHON_FROM); do
|
||||
echo "# Checking for python3.${i}"
|
||||
echo "# POI07| Checking for python3.${i}"
|
||||
if [ -n "$(which python3.${i})" ]; then
|
||||
SETUP_PYTHON="python3.${i}"
|
||||
echo "# Found python3.${i} installed - using for setup ${SETUP_PYTHON}"
|
||||
echo "# POI07| Found python3.${i} installed - using for setup ${SETUP_PYTHON}"
|
||||
break
|
||||
fi
|
||||
done
|
||||
@@ -75,12 +77,14 @@ function detect_python() {
|
||||
# Ensure python can be executed - abort if not
|
||||
if [ -z "$(which ${SETUP_PYTHON})" ]; then
|
||||
echo "${On_Red}"
|
||||
echo "# Python ${SETUP_PYTHON} not found - aborting!"
|
||||
echo "# Please ensure python can be executed with the command '$SETUP_PYTHON' by the current user '$USER'."
|
||||
echo "# If you are using a different python version, please set the environment variable SETUP_PYTHON to the correct command - eg. 'python3.10'."
|
||||
echo "# POI07| Python ${SETUP_PYTHON} not found - aborting!"
|
||||
echo "# POI07| Please ensure python can be executed with the command '$SETUP_PYTHON' by the current user '$USER'."
|
||||
echo "# POI07| If you are using a different python version, please set the environment variable SETUP_PYTHON to the correct command - eg. 'python3.10'."
|
||||
echo "${Color_Off}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "# POI07| Using python command: ${SETUP_PYTHON}"
|
||||
}
|
||||
|
||||
function get_env() {
|
||||
@@ -95,7 +99,7 @@ function get_env() {
|
||||
done
|
||||
|
||||
if [ -n "${SETUP_DEBUG}" ]; then
|
||||
echo "Done getting env $envname: ${!envname}"
|
||||
echo "# POI02| Done getting env $envname: ${!envname}"
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -103,7 +107,7 @@ function detect_local_env() {
|
||||
# Get all possible envs for the install
|
||||
|
||||
if [ -n "${SETUP_DEBUG}" ]; then
|
||||
echo "# Printing local envs - before #++#"
|
||||
echo "# POI02| Printing local envs - before #++#"
|
||||
printenv
|
||||
fi
|
||||
|
||||
@@ -113,7 +117,7 @@ function detect_local_env() {
|
||||
done
|
||||
|
||||
if [ -n "${SETUP_DEBUG}" ]; then
|
||||
echo "# Printing local envs - after #++#"
|
||||
echo "# POI02| Printing local envs - after #++#"
|
||||
printenv
|
||||
fi
|
||||
}
|
||||
@@ -121,15 +125,17 @@ function detect_local_env() {
|
||||
function detect_envs() {
|
||||
# Detect all envs that should be passed to setup commands
|
||||
|
||||
echo "# Setting base environment variables"
|
||||
echo "# POI03| Setting base environment variables"
|
||||
|
||||
export INVENTREE_CONFIG_FILE=${INVENTREE_CONFIG_FILE:-${CONF_DIR}/config.yaml}
|
||||
|
||||
if test -f "${INVENTREE_CONFIG_FILE}"; then
|
||||
echo "# Using existing config file: ${INVENTREE_CONFIG_FILE}"
|
||||
echo "# POI03| Using existing config file: ${INVENTREE_CONFIG_FILE}"
|
||||
|
||||
# Install parser
|
||||
echo "# POI03| Installing requirements"
|
||||
pip install --require-hashes -r ${APP_HOME}/contrib/dev_reqs/requirements.txt -q
|
||||
echo "# POI03| Installed requirements"
|
||||
|
||||
# Load config
|
||||
export INVENTREE_CONF_DATA=$(cat ${INVENTREE_CONFIG_FILE} | jc --yaml)
|
||||
@@ -149,10 +155,10 @@ function detect_envs() {
|
||||
export INVENTREE_DB_HOST=$(jq -r '.[].database.HOST' <<< ${INVENTREE_CONF_DATA})
|
||||
export INVENTREE_DB_PORT=$(jq -r '.[].database.PORT' <<< ${INVENTREE_CONF_DATA})
|
||||
else
|
||||
echo "# No config file found: ${INVENTREE_CONFIG_FILE}, using envs or defaults"
|
||||
echo "# POI03| No config file found: ${INVENTREE_CONFIG_FILE}, using envs or defaults"
|
||||
|
||||
if [ -n "${SETUP_DEBUG}" ]; then
|
||||
echo "# Print current envs"
|
||||
echo "# POI03| Print current envs"
|
||||
printenv | grep INVENTREE_
|
||||
printenv | grep SETUP_
|
||||
fi
|
||||
@@ -175,43 +181,43 @@ function detect_envs() {
|
||||
fi
|
||||
|
||||
# For debugging pass out the envs
|
||||
echo "# Collected environment variables:"
|
||||
echo "# INVENTREE_MEDIA_ROOT=${INVENTREE_MEDIA_ROOT}"
|
||||
echo "# INVENTREE_STATIC_ROOT=${INVENTREE_STATIC_ROOT}"
|
||||
echo "# INVENTREE_BACKUP_DIR=${INVENTREE_BACKUP_DIR}"
|
||||
echo "# INVENTREE_PLUGINS_ENABLED=${INVENTREE_PLUGINS_ENABLED}"
|
||||
echo "# INVENTREE_PLUGIN_FILE=${INVENTREE_PLUGIN_FILE}"
|
||||
echo "# INVENTREE_SECRET_KEY_FILE=${INVENTREE_SECRET_KEY_FILE}"
|
||||
echo "# INVENTREE_DB_ENGINE=${INVENTREE_DB_ENGINE}"
|
||||
echo "# INVENTREE_DB_NAME=${INVENTREE_DB_NAME}"
|
||||
echo "# INVENTREE_DB_USER=${INVENTREE_DB_USER}"
|
||||
echo "# POI03| Collected environment variables:"
|
||||
echo "# POI03| INVENTREE_MEDIA_ROOT=${INVENTREE_MEDIA_ROOT}"
|
||||
echo "# POI03| INVENTREE_STATIC_ROOT=${INVENTREE_STATIC_ROOT}"
|
||||
echo "# POI03| INVENTREE_BACKUP_DIR=${INVENTREE_BACKUP_DIR}"
|
||||
echo "# POI03| INVENTREE_PLUGINS_ENABLED=${INVENTREE_PLUGINS_ENABLED}"
|
||||
echo "# POI03| INVENTREE_PLUGIN_FILE=${INVENTREE_PLUGIN_FILE}"
|
||||
echo "# POI03| INVENTREE_SECRET_KEY_FILE=${INVENTREE_SECRET_KEY_FILE}"
|
||||
echo "# POI03| INVENTREE_DB_ENGINE=${INVENTREE_DB_ENGINE}"
|
||||
echo "# POI03| INVENTREE_DB_NAME=${INVENTREE_DB_NAME}"
|
||||
echo "# POI03| INVENTREE_DB_USER=${INVENTREE_DB_USER}"
|
||||
if [ -n "${SETUP_DEBUG}" ]; then
|
||||
echo "# INVENTREE_DB_PASSWORD=${INVENTREE_DB_PASSWORD}"
|
||||
echo "# POI03| INVENTREE_DB_PASSWORD=${INVENTREE_DB_PASSWORD}"
|
||||
fi
|
||||
echo "# INVENTREE_DB_HOST=${INVENTREE_DB_HOST}"
|
||||
echo "# INVENTREE_DB_PORT=${INVENTREE_DB_PORT}"
|
||||
echo "# POI03| INVENTREE_DB_HOST=${INVENTREE_DB_HOST}"
|
||||
echo "# POI03| INVENTREE_DB_PORT=${INVENTREE_DB_PORT}"
|
||||
}
|
||||
|
||||
function create_initscripts() {
|
||||
|
||||
# Make sure python env exists
|
||||
if test -f "${APP_HOME}/env"; then
|
||||
echo "# python environment already present - skipping"
|
||||
echo "# POI09| python environment already present - skipping"
|
||||
else
|
||||
echo "# Setting up python environment"
|
||||
echo "# POI09| Setting up python environment"
|
||||
sudo -u ${APP_USER} --preserve-env=$SETUP_ENVS bash -c "cd ${APP_HOME} && ${SETUP_PYTHON} -m venv env"
|
||||
sudo -u ${APP_USER} --preserve-env=$SETUP_ENVS bash -c "cd ${APP_HOME} && env/bin/pip install invoke wheel"
|
||||
|
||||
# Check INSTALLER_EXTRA exists and load it
|
||||
if test -f "${APP_HOME}/INSTALLER_EXTRA"; then
|
||||
echo "# Loading extra packages from INSTALLER_EXTRA"
|
||||
echo "# POI09| Loading extra packages from INSTALLER_EXTRA"
|
||||
source ${APP_HOME}/INSTALLER_EXTRA
|
||||
fi
|
||||
|
||||
if [ -n "${SETUP_EXTRA_PIP}" ]; then
|
||||
echo "# Installing extra pip packages"
|
||||
echo "# POI09| Installing extra pip packages"
|
||||
if [ -n "${SETUP_DEBUG}" ]; then
|
||||
echo "# Extra pip packages: ${SETUP_EXTRA_PIP}"
|
||||
echo "# POI09| Extra pip packages: ${SETUP_EXTRA_PIP}"
|
||||
fi
|
||||
sudo -u ${APP_USER} --preserve-env=$SETUP_ENVS bash -c "cd ${APP_HOME} && env/bin/pip install ${SETUP_EXTRA_PIP}"
|
||||
# Write extra packages to INSTALLER_EXTRA
|
||||
@@ -221,41 +227,45 @@ function create_initscripts() {
|
||||
|
||||
# Unlink default config if it exists
|
||||
if test -f "/etc/nginx/sites-enabled/default"; then
|
||||
echo "# Unlinking default nginx config\n# Old file still in /etc/nginx/sites-available/default"
|
||||
echo "# POI09| Unlinking default nginx config\n# POI09| Old file still in /etc/nginx/sites-available/default"
|
||||
sudo unlink /etc/nginx/sites-enabled/default
|
||||
echo "# POI09| Unlinked default nginx config"
|
||||
fi
|
||||
|
||||
# Create InvenTree specific nginx config
|
||||
echo "# Stopping nginx"
|
||||
echo "# POI09| Stopping nginx"
|
||||
${INIT_CMD} stop nginx
|
||||
echo "# Setting up nginx to ${SETUP_NGINX_FILE}"
|
||||
echo "# POI09| Stopped nginx"
|
||||
echo "# POI09| Setting up nginx to ${SETUP_NGINX_FILE}"
|
||||
# Always use the latest nginx config; important if new headers are added / needed for security
|
||||
cp ${APP_HOME}/contrib/packager.io/nginx.prod.conf ${SETUP_NGINX_FILE}
|
||||
sed -i s/inventree-server:8000/localhost:6000/g ${SETUP_NGINX_FILE}
|
||||
sed -i s=var/www=opt/inventree/data=g ${SETUP_NGINX_FILE}
|
||||
# Start nginx
|
||||
echo "# Starting nginx"
|
||||
echo "# POI09| Starting nginx"
|
||||
${INIT_CMD} start nginx
|
||||
echo "# POI09| Started nginx"
|
||||
|
||||
echo "# (Re)creating init scripts"
|
||||
echo "# POI09| (Re)creating init scripts"
|
||||
# This resets scale parameters to a known state
|
||||
inventree scale web="1" worker="1"
|
||||
|
||||
echo "# Enabling InvenTree on boot"
|
||||
echo "# POI09| Enabling InvenTree on boot"
|
||||
${INIT_CMD} enable inventree
|
||||
echo "# POI09| Enabled InvenTree on boot"
|
||||
}
|
||||
|
||||
function create_admin() {
|
||||
# Create data for admin users - stop with setting SETUP_ADMIN_NOCREATION to true
|
||||
if [ "${SETUP_ADMIN_NOCREATION}" == "true" ]; then
|
||||
echo "# Admin creation is disabled - skipping"
|
||||
echo "# POI10| Admin creation is disabled - skipping"
|
||||
return
|
||||
fi
|
||||
|
||||
if test -f "${SETUP_ADMIN_PASSWORD_FILE}"; then
|
||||
echo "# Admin data already exists - skipping"
|
||||
echo "# POI10| Admin data already exists - skipping"
|
||||
else
|
||||
echo "# Creating admin user data"
|
||||
echo "# POI10| Creating admin user data"
|
||||
|
||||
# Static admin data
|
||||
export INVENTREE_ADMIN_USER=${INVENTREE_ADMIN_USER:-admin}
|
||||
@@ -270,13 +280,15 @@ function create_admin() {
|
||||
}
|
||||
|
||||
function start_inventree() {
|
||||
echo "# Starting InvenTree"
|
||||
echo "# POI15| Starting InvenTree"
|
||||
${INIT_CMD} start inventree
|
||||
echo "# POI15| Started InvenTree"
|
||||
}
|
||||
|
||||
function stop_inventree() {
|
||||
echo "# Stopping InvenTree"
|
||||
echo "# POI11| Stopping InvenTree"
|
||||
${INIT_CMD} stop inventree
|
||||
echo "# POI11| Stopped InvenTree"
|
||||
}
|
||||
|
||||
function update_or_install() {
|
||||
@@ -285,23 +297,23 @@ function update_or_install() {
|
||||
chown ${APP_USER}:${APP_GROUP} ${APP_HOME} -R
|
||||
|
||||
# Run update as app user
|
||||
echo "# Updating InvenTree"
|
||||
sudo -u ${APP_USER} --preserve-env=$SETUP_ENVS bash -c "cd ${APP_HOME} && pip install wheel"
|
||||
sudo -u ${APP_USER} --preserve-env=$SETUP_ENVS bash -c "cd ${APP_HOME} && invoke update | sed -e 's/^/# inv update| /;'"
|
||||
echo "# POI12| Updating InvenTree"
|
||||
sudo -u ${APP_USER} --preserve-env=$SETUP_ENVS bash -c "cd ${APP_HOME} && pip install uv wheel"
|
||||
sudo -u ${APP_USER} --preserve-env=$SETUP_ENVS bash -c "cd ${APP_HOME} && invoke update --uv | sed -e 's/^/# POI12| u | /;'"
|
||||
|
||||
# Make sure permissions are correct again
|
||||
echo "# Set permissions for data dir and media: ${DATA_DIR}"
|
||||
echo "# POI12| Set permissions for data dir and media: ${DATA_DIR}"
|
||||
chown ${APP_USER}:${APP_GROUP} ${DATA_DIR} -R
|
||||
chown ${APP_USER}:${APP_GROUP} ${CONF_DIR} -R
|
||||
}
|
||||
|
||||
function set_env() {
|
||||
echo "# Setting up InvenTree config values"
|
||||
echo "# POI13| Setting up InvenTree config values"
|
||||
|
||||
inventree config:set INVENTREE_CONFIG_FILE=${INVENTREE_CONFIG_FILE}
|
||||
|
||||
# Changing the config file
|
||||
echo "# Writing the settings to the config file ${INVENTREE_CONFIG_FILE}"
|
||||
echo "# POI13| Writing the settings to the config file ${INVENTREE_CONFIG_FILE}"
|
||||
# Media Root
|
||||
sed -i s=#media_root:\ \'/home/inventree/data/media\'=media_root:\ \'${INVENTREE_MEDIA_ROOT}\'=g ${INVENTREE_CONFIG_FILE}
|
||||
# Static Root
|
||||
@@ -332,23 +344,28 @@ function set_env() {
|
||||
|
||||
# Fixing the permissions
|
||||
chown ${APP_USER}:${APP_GROUP} ${DATA_DIR} ${INVENTREE_CONFIG_FILE}
|
||||
|
||||
echo "# POI13| Done setting up InvenTree config values"
|
||||
}
|
||||
|
||||
function set_site() {
|
||||
# Ensure IP is known
|
||||
if [ -z "${INVENTREE_IP}" ]; then
|
||||
echo "# No IP address found - skipping"
|
||||
echo "# POI14| No IP address found - skipping"
|
||||
return
|
||||
fi
|
||||
|
||||
# Check if INVENTREE_SITE_URL in inventree config
|
||||
if [ -z "$(inventree config:get INVENTREE_SITE_URL)" ]; then
|
||||
echo "# Setting up InvenTree site URL"
|
||||
echo "# POI14| Setting up InvenTree site URL"
|
||||
inventree config:set INVENTREE_SITE_URL=http://${INVENTREE_IP}
|
||||
else
|
||||
echo "# POI14| Site URL already set - skipping"
|
||||
fi
|
||||
}
|
||||
|
||||
function final_message() {
|
||||
echo "# POI16| Printing Final message"
|
||||
echo -e "####################################################################################"
|
||||
echo -e "This InvenTree install uses nginx, the settings for the webserver can be found in"
|
||||
echo -e "${SETUP_NGINX_FILE}"
|
||||
@@ -362,10 +379,12 @@ function final_message() {
|
||||
|
||||
|
||||
function update_checks() {
|
||||
echo "# Running upgrade"
|
||||
echo "# POI08| Running upgrade"
|
||||
local old_version=$1
|
||||
local old_version_rev=$(echo ${old_version} | cut -d'-' -f1 | cut -d'.' -f2)
|
||||
echo "# Old version is: ${old_version} - release: ${old_version_rev}"
|
||||
local new_version=$(dpkg-query --show --showformat='${Version}' inventree)
|
||||
local new_version_rev=$(echo ${new_version} | cut -d'-' -f1 | cut -d'.' -f2)
|
||||
echo "# POI08| Old version is: ${old_version} | ${old_version_rev} - updating to ${new_version} | ${old_version_rev}"
|
||||
|
||||
local ABORT=false
|
||||
function check_config_value() {
|
||||
@@ -378,25 +397,25 @@ function update_checks() {
|
||||
value=$(jq -r ".[].${config_key}" <<< ${INVENTREE_CONF_DATA})
|
||||
fi
|
||||
if [ -z "${value}" ] || [ "$value" == "null" ]; then
|
||||
echo "# No setting for ${name} found - please set it manually either in ${INVENTREE_CONFIG_FILE} under '${config_key}' or with 'inventree config:set ${env_key}=value'"
|
||||
echo "# POI08| No setting for ${name} found - please set it manually either in ${INVENTREE_CONFIG_FILE} under '${config_key}' or with 'inventree config:set ${env_key}=value'"
|
||||
ABORT=true
|
||||
else
|
||||
echo "# Found setting for ${name} - ${value}"
|
||||
echo "# POI08| Found setting for ${name} - ${value}"
|
||||
fi
|
||||
}
|
||||
|
||||
# Custom checks if old version is below 0.8.0
|
||||
if [ "${old_version_rev}" -lt "9" ]; then
|
||||
echo "# Old version is below 0.9.0 - You might be missing some configs"
|
||||
echo "# POI08| Old version is below 0.9.0 - You might be missing some configs"
|
||||
|
||||
# Check for BACKUP_DIR and SITE_URL in INVENTREE_CONF_DATA and config
|
||||
check_config_value "INVENTREE_SITE_URL" "site_url" "site URL"
|
||||
check_config_value "INVENTREE_BACKUP_DIR" "backup_dir" "backup dir"
|
||||
|
||||
if [ "${ABORT}" = true ]; then
|
||||
echo "# Aborting - please set the missing values and run the update again"
|
||||
echo "# POI08| Aborting - please set the missing values and run the update again"
|
||||
exit 1
|
||||
fi
|
||||
echo "# All checks passed - continuing with the update"
|
||||
echo "# POI08| All checks passed - continuing with the update"
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -3,12 +3,15 @@
|
||||
# packager.io postinstall script
|
||||
#
|
||||
|
||||
echo "# POI01| Running postinstall script - start - $(date)"
|
||||
exec > >(tee ${APP_HOME}/log/setup_$(date +"%F_%H_%M_%S").log) 2>&1
|
||||
|
||||
PATH=${APP_HOME}/env/bin:${APP_HOME}/:/sbin:/bin:/usr/sbin:/usr/bin:
|
||||
|
||||
# import functions
|
||||
echo "# POI01| Importing functions"
|
||||
. ${APP_HOME}/contrib/packager.io/functions.sh
|
||||
echo "# POI01| Functions imported"
|
||||
|
||||
# Envs that should be passed to setup commands
|
||||
export SETUP_ENVS=PATH,APP_HOME,INVENTREE_MEDIA_ROOT,INVENTREE_STATIC_ROOT,INVENTREE_BACKUP_DIR,INVENTREE_SITE_URL,INVENTREE_PLUGINS_ENABLED,INVENTREE_PLUGIN_FILE,INVENTREE_CONFIG_FILE,INVENTREE_SECRET_KEY_FILE,INVENTREE_DB_ENGINE,INVENTREE_DB_NAME,INVENTREE_DB_USER,INVENTREE_DB_PASSWORD,INVENTREE_DB_HOST,INVENTREE_DB_PORT,INVENTREE_ADMIN_USER,INVENTREE_ADMIN_EMAIL,INVENTREE_ADMIN_PASSWORD,SETUP_NGINX_FILE,SETUP_ADMIN_PASSWORD_FILE,SETUP_NO_CALLS,SETUP_DEBUG,SETUP_EXTRA_PIP,SETUP_PYTHON,SETUP_ADMIN_NOCREATION
|
||||
@@ -37,9 +40,9 @@ detect_ip
|
||||
detect_python
|
||||
|
||||
# Check if we are updating and need to alert
|
||||
echo "# Checking if update checks are needed"
|
||||
echo "# POI08| Checking if update checks are needed"
|
||||
if [ -z "$2" ]; then
|
||||
echo "# Normal install - no need for checks"
|
||||
echo "# POI08| Normal install - no need for checks"
|
||||
else
|
||||
update_checks $2
|
||||
fi
|
||||
@@ -60,3 +63,4 @@ start_inventree
|
||||
|
||||
# show info
|
||||
final_message
|
||||
echo "# POI17| Running postinstall script - done - $(date)"
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#
|
||||
# packager.io preinstall/preremove script
|
||||
#
|
||||
echo "# PRI01| Running preinstall script - start - $(date)"
|
||||
PATH=${APP_HOME}/env/bin:${APP_HOME}/:/sbin:/bin:/usr/sbin:/usr/bin:
|
||||
|
||||
# Envs that should be passed to setup commands
|
||||
@@ -9,12 +10,14 @@ export SETUP_ENVS=PATH,APP_HOME,INVENTREE_MEDIA_ROOT,INVENTREE_STATIC_ROOT,INVEN
|
||||
|
||||
if test -f "${APP_HOME}/env/bin/pip"; then
|
||||
# Check if clear-generated is available
|
||||
if sudo -u ${APP_USER} --preserve-env=$SETUP_ENVS bash -c "cd ${APP_HOME} && invoke clear-generated --help" > /dev/null 2>&1; then
|
||||
echo "# Clearing precompiled files"
|
||||
sudo -u ${APP_USER} --preserve-env=$SETUP_ENVS bash -c "cd ${APP_HOME} && invoke clear-generated"
|
||||
if sudo -u ${APP_USER} --preserve-env=$SETUP_ENVS bash -c "cd ${APP_HOME} && invoke int.clear-generated --help" > /dev/null 2>&1; then
|
||||
echo "# PRI02| Clearing precompiled files"
|
||||
sudo -u ${APP_USER} --preserve-env=$SETUP_ENVS bash -c "cd ${APP_HOME} && invoke int.clear-generated"
|
||||
else
|
||||
echo "# Clearing precompiled files - skipping"
|
||||
echo "# PRI02| Clearing precompiled files - skipping"
|
||||
fi
|
||||
else
|
||||
echo "# No python environment found - skipping"
|
||||
echo "# PRI02| No python environment found - skipping"
|
||||
fi
|
||||
|
||||
echo "# PRI03| Running preinstall script - done - $(date)"
|
||||
|
||||
@@ -52,7 +52,7 @@ invoke setup-dev
|
||||
|
||||
## Branches and Versioning
|
||||
|
||||
InvenTree roughly follow the [GitLab flow](https://docs.gitlab.com/ee/topics/gitlab_flow.html) branching style, to allow simple management of multiple tagged releases, short-lived branches, and development on the main branch.
|
||||
InvenTree roughly follow the [GitLab flow](https://about.gitlab.com/topics/version-control/what-are-gitlab-flow-best-practices/) branching style, to allow simple management of multiple tagged releases, short-lived branches, and development on the main branch.
|
||||
|
||||
There are nominally 5 active branches:
|
||||
- `master` - The main development branch
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
{
|
||||
"pattern": "http://localhost"
|
||||
},
|
||||
{
|
||||
"pattern": "https://localhost:5173/"
|
||||
},
|
||||
{
|
||||
"pattern": "http://127.0.0.1"
|
||||
},
|
||||
|
||||
@@ -170,10 +170,11 @@ DBBACKUP_STORAGE = get_setting(
|
||||
|
||||
# Default backup configuration
|
||||
DBBACKUP_STORAGE_OPTIONS = get_setting(
|
||||
'INVENTREE_BACKUP_OPTIONS', 'backup_options', None
|
||||
'INVENTREE_BACKUP_OPTIONS',
|
||||
'backup_options',
|
||||
default_value={'location': config.get_backup_dir()},
|
||||
typecast=dict,
|
||||
)
|
||||
if DBBACKUP_STORAGE_OPTIONS is None:
|
||||
DBBACKUP_STORAGE_OPTIONS = {'location': config.get_backup_dir()}
|
||||
|
||||
INVENTREE_ADMIN_ENABLED = get_boolean_setting(
|
||||
'INVENTREE_ADMIN_ENABLED', config_key='admin_enabled', default_value=True
|
||||
@@ -1284,6 +1285,9 @@ PLUGIN_FILE_CHECKED = False # Was the plugin file checked?
|
||||
# Flag to allow table events during testing
|
||||
TESTING_TABLE_EVENTS = False
|
||||
|
||||
# Flag to allow pricing recalculations during testing
|
||||
TESTING_PRICING = False
|
||||
|
||||
# User interface customization values
|
||||
CUSTOM_LOGO = get_custom_file(
|
||||
'INVENTREE_CUSTOM_LOGO', 'customize.logo', 'custom logo', lookup_media=True
|
||||
|
||||
@@ -264,7 +264,7 @@ class InvenTreeAPITestCase(ExchangeRateMixin, UserMixin, APITestCase):
|
||||
MAX_QUERY_TIME = 7.5
|
||||
|
||||
@contextmanager
|
||||
def assertNumQueriesLessThan(self, value, using='default', verbose=None, url=None):
|
||||
def assertNumQueriesLessThan(self, value, using='default', verbose=False, url=None):
|
||||
"""Context manager to check that the number of queries is less than a certain value.
|
||||
|
||||
Example:
|
||||
@@ -282,10 +282,8 @@ class InvenTreeAPITestCase(ExchangeRateMixin, UserMixin, APITestCase):
|
||||
f'Query count exceeded at {url}: Expected < {value} queries, got {n}'
|
||||
) # pragma: no cover
|
||||
|
||||
if verbose or n >= value:
|
||||
msg = '\r\n%s' % json.dumps(
|
||||
context.captured_queries, indent=4
|
||||
) # pragma: no cover
|
||||
if verbose and n >= value:
|
||||
msg = f'\r\n{json.dumps(context.captured_queries, indent=4)}' # pragma: no cover
|
||||
else:
|
||||
msg = None
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ from django.conf import settings
|
||||
from .api_version import INVENTREE_API_TEXT, INVENTREE_API_VERSION
|
||||
|
||||
# InvenTree software version
|
||||
INVENTREE_SW_VERSION = '0.16.1'
|
||||
INVENTREE_SW_VERSION = '0.16.3'
|
||||
|
||||
|
||||
logger = logging.getLogger('inventree')
|
||||
|
||||
@@ -966,6 +966,12 @@ class BuildOverallocationTest(BuildAPITest):
|
||||
outputs = cls.build.build_outputs.all()
|
||||
cls.build.complete_build_output(outputs[0], cls.user)
|
||||
|
||||
def setUp(self):
|
||||
"""Basic operation as part of test suite setup"""
|
||||
super().setUp()
|
||||
|
||||
self.generate_exchange_rates()
|
||||
|
||||
def test_setup(self):
|
||||
"""Validate expected state after set-up."""
|
||||
self.assertEqual(self.build.incomplete_outputs.count(), 0)
|
||||
@@ -994,7 +1000,7 @@ class BuildOverallocationTest(BuildAPITest):
|
||||
'accept_overallocated': 'accept',
|
||||
},
|
||||
expected_code=201,
|
||||
max_query_count=550, # TODO: Come back and refactor this
|
||||
max_query_count=1000, # TODO: Come back and refactor this
|
||||
)
|
||||
|
||||
self.build.refresh_from_db()
|
||||
@@ -1015,9 +1021,11 @@ class BuildOverallocationTest(BuildAPITest):
|
||||
'accept_overallocated': 'trim',
|
||||
},
|
||||
expected_code=201,
|
||||
max_query_count=555, # TODO: Come back and refactor this
|
||||
max_query_count=1000, # TODO: Come back and refactor this
|
||||
)
|
||||
|
||||
# Note: Large number of queries is due to pricing recalculation for each stock item
|
||||
|
||||
self.build.refresh_from_db()
|
||||
|
||||
# Build should have been marked as complete
|
||||
|
||||
@@ -1052,7 +1052,10 @@ class SupplierPriceBreak(common.models.PriceBreak):
|
||||
)
|
||||
def after_save_supplier_price(sender, instance, created, **kwargs):
|
||||
"""Callback function when a SupplierPriceBreak is created or updated."""
|
||||
if InvenTree.ready.canAppAccessDatabase() and not InvenTree.ready.isImportingData():
|
||||
if (
|
||||
InvenTree.ready.canAppAccessDatabase(allow_test=settings.TESTING_PRICING)
|
||||
and not InvenTree.ready.isImportingData()
|
||||
):
|
||||
if instance.part and instance.part.part:
|
||||
instance.part.part.schedule_pricing_update(create=True)
|
||||
|
||||
@@ -1064,6 +1067,9 @@ def after_save_supplier_price(sender, instance, created, **kwargs):
|
||||
)
|
||||
def after_delete_supplier_price(sender, instance, **kwargs):
|
||||
"""Callback function when a SupplierPriceBreak is deleted."""
|
||||
if InvenTree.ready.canAppAccessDatabase() and not InvenTree.ready.isImportingData():
|
||||
if (
|
||||
InvenTree.ready.canAppAccessDatabase(allow_test=settings.TESTING_PRICING)
|
||||
and not InvenTree.ready.isImportingData()
|
||||
):
|
||||
if instance.part and instance.part.part:
|
||||
instance.part.part.schedule_pricing_update(create=False)
|
||||
|
||||
@@ -1931,7 +1931,7 @@ class Part(
|
||||
|
||||
return pricing
|
||||
|
||||
def schedule_pricing_update(self, create: bool = False, test: bool = False):
|
||||
def schedule_pricing_update(self, create: bool = False):
|
||||
"""Helper function to schedule a pricing update.
|
||||
|
||||
Importantly, catches any errors which may occur during deletion of related objects,
|
||||
@@ -1941,7 +1941,6 @@ class Part(
|
||||
|
||||
Arguments:
|
||||
create: Whether or not a new PartPricing object should be created if it does not already exist
|
||||
test: Whether or not the pricing update is allowed during unit tests
|
||||
"""
|
||||
try:
|
||||
self.refresh_from_db()
|
||||
@@ -1952,7 +1951,7 @@ class Part(
|
||||
pricing = self.pricing
|
||||
|
||||
if create or pricing.pk:
|
||||
pricing.schedule_for_update(test=test)
|
||||
pricing.schedule_for_update()
|
||||
except IntegrityError:
|
||||
# If this part instance has been deleted,
|
||||
# some post-delete or post-save signals may still be fired
|
||||
@@ -2532,7 +2531,8 @@ class PartPricing(common.models.MetaMixin):
|
||||
- Detailed pricing information is very context specific in any case
|
||||
"""
|
||||
|
||||
price_modified = False
|
||||
# When calculating assembly pricing, we limit the depth of the calculation
|
||||
MAX_PRICING_DEPTH = 50
|
||||
|
||||
@property
|
||||
def is_valid(self):
|
||||
@@ -2561,14 +2561,10 @@ class PartPricing(common.models.MetaMixin):
|
||||
|
||||
return result
|
||||
|
||||
def schedule_for_update(self, counter: int = 0, test: bool = False):
|
||||
def schedule_for_update(self, counter: int = 0):
|
||||
"""Schedule this pricing to be updated."""
|
||||
import InvenTree.ready
|
||||
|
||||
# If we are running within CI, only schedule the update if the test flag is set
|
||||
if settings.TESTING and not test:
|
||||
return
|
||||
|
||||
# If importing data, skip pricing update
|
||||
if InvenTree.ready.isImportingData():
|
||||
return
|
||||
@@ -2612,7 +2608,7 @@ class PartPricing(common.models.MetaMixin):
|
||||
logger.debug('Pricing for %s already scheduled for update - skipping', p)
|
||||
return
|
||||
|
||||
if counter > 25:
|
||||
if counter > self.MAX_PRICING_DEPTH:
|
||||
# Prevent infinite recursion / stack depth issues
|
||||
logger.debug(
|
||||
counter, f'Skipping pricing update for {p} - maximum depth exceeded'
|
||||
@@ -2631,16 +2627,36 @@ class PartPricing(common.models.MetaMixin):
|
||||
|
||||
import part.tasks as part_tasks
|
||||
|
||||
# Pricing calculations are performed in the background,
|
||||
# unless the TESTING_PRICING flag is set
|
||||
background = not settings.TESTING or not settings.TESTING_PRICING
|
||||
|
||||
# Offload task to update the pricing
|
||||
# Force async, to prevent running in the foreground
|
||||
# Force async, to prevent running in the foreground (unless in testing mode)
|
||||
InvenTree.tasks.offload_task(
|
||||
part_tasks.update_part_pricing, self, counter=counter, force_async=True
|
||||
part_tasks.update_part_pricing,
|
||||
self,
|
||||
counter=counter,
|
||||
force_async=background,
|
||||
)
|
||||
|
||||
def update_pricing(self, counter: int = 0, cascade: bool = True):
|
||||
"""Recalculate all cost data for the referenced Part instance."""
|
||||
# If importing data, skip pricing update
|
||||
def update_pricing(
|
||||
self,
|
||||
counter: int = 0,
|
||||
cascade: bool = True,
|
||||
previous_min=None,
|
||||
previous_max=None,
|
||||
):
|
||||
"""Recalculate all cost data for the referenced Part instance.
|
||||
|
||||
Arguments:
|
||||
counter: Recursion counter (used to prevent infinite recursion)
|
||||
cascade: If True, update pricing for all assemblies and templates which use this part
|
||||
previous_min: Previous minimum price (used to prevent further updates if unchanged)
|
||||
previous_max: Previous maximum price (used to prevent further updates if unchanged)
|
||||
|
||||
"""
|
||||
# If importing data, skip pricing update
|
||||
if InvenTree.ready.isImportingData():
|
||||
return
|
||||
|
||||
@@ -2671,18 +2687,25 @@ class PartPricing(common.models.MetaMixin):
|
||||
# Background worker processes may try to concurrently update
|
||||
pass
|
||||
|
||||
pricing_changed = False
|
||||
|
||||
# Without previous pricing data, we assume that the pricing has changed
|
||||
if previous_min != self.overall_min or previous_max != self.overall_max:
|
||||
pricing_changed = True
|
||||
|
||||
# Update parent assemblies and templates
|
||||
if cascade and self.price_modified:
|
||||
if pricing_changed and cascade:
|
||||
self.update_assemblies(counter)
|
||||
self.update_templates(counter)
|
||||
|
||||
def update_assemblies(self, counter: int = 0):
|
||||
"""Schedule updates for any assemblies which use this part."""
|
||||
# If the linked Part is used in any assemblies, schedule a pricing update for those assemblies
|
||||
|
||||
used_in_parts = self.part.get_used_in()
|
||||
|
||||
for p in used_in_parts:
|
||||
p.pricing.schedule_for_update(counter + 1)
|
||||
p.pricing.schedule_for_update(counter=counter + 1)
|
||||
|
||||
def update_templates(self, counter: int = 0):
|
||||
"""Schedule updates for any template parts above this part."""
|
||||
@@ -2698,13 +2721,13 @@ class PartPricing(common.models.MetaMixin):
|
||||
|
||||
try:
|
||||
self.update_overall_cost()
|
||||
except IntegrityError:
|
||||
except Exception:
|
||||
# If something has happened to the Part model, might throw an error
|
||||
pass
|
||||
|
||||
try:
|
||||
super().save(*args, **kwargs)
|
||||
except IntegrityError:
|
||||
except Exception:
|
||||
# This error may be thrown if there is already duplicate pricing data
|
||||
pass
|
||||
|
||||
@@ -2772,9 +2795,6 @@ class PartPricing(common.models.MetaMixin):
|
||||
|
||||
any_max_elements = True
|
||||
|
||||
old_bom_cost_min = self.bom_cost_min
|
||||
old_bom_cost_max = self.bom_cost_max
|
||||
|
||||
if any_min_elements:
|
||||
self.bom_cost_min = cumulative_min
|
||||
else:
|
||||
@@ -2785,12 +2805,6 @@ class PartPricing(common.models.MetaMixin):
|
||||
else:
|
||||
self.bom_cost_max = None
|
||||
|
||||
if (
|
||||
old_bom_cost_min != self.bom_cost_min
|
||||
or old_bom_cost_max != self.bom_cost_max
|
||||
):
|
||||
self.price_modified = True
|
||||
|
||||
if save:
|
||||
self.save()
|
||||
|
||||
@@ -2854,12 +2868,6 @@ class PartPricing(common.models.MetaMixin):
|
||||
if purchase_max is None or cost > purchase_max:
|
||||
purchase_max = cost
|
||||
|
||||
if (
|
||||
self.purchase_cost_min != purchase_min
|
||||
or self.purchase_cost_max != purchase_max
|
||||
):
|
||||
self.price_modified = True
|
||||
|
||||
self.purchase_cost_min = purchase_min
|
||||
self.purchase_cost_max = purchase_max
|
||||
|
||||
@@ -2886,12 +2894,6 @@ class PartPricing(common.models.MetaMixin):
|
||||
if max_int_cost is None or cost > max_int_cost:
|
||||
max_int_cost = cost
|
||||
|
||||
if (
|
||||
self.internal_cost_min != min_int_cost
|
||||
or self.internal_cost_max != max_int_cost
|
||||
):
|
||||
self.price_modified = True
|
||||
|
||||
self.internal_cost_min = min_int_cost
|
||||
self.internal_cost_max = max_int_cost
|
||||
|
||||
@@ -2927,12 +2929,6 @@ class PartPricing(common.models.MetaMixin):
|
||||
if max_sup_cost is None or cost > max_sup_cost:
|
||||
max_sup_cost = cost
|
||||
|
||||
if (
|
||||
self.supplier_price_min != min_sup_cost
|
||||
or self.supplier_price_max != max_sup_cost
|
||||
):
|
||||
self.price_modified = True
|
||||
|
||||
self.supplier_price_min = min_sup_cost
|
||||
self.supplier_price_max = max_sup_cost
|
||||
|
||||
@@ -2968,9 +2964,6 @@ class PartPricing(common.models.MetaMixin):
|
||||
if variant_max is None or v_max > variant_max:
|
||||
variant_max = v_max
|
||||
|
||||
if self.variant_cost_min != variant_min or self.variant_cost_max != variant_max:
|
||||
self.price_modified = True
|
||||
|
||||
self.variant_cost_min = variant_min
|
||||
self.variant_cost_max = variant_max
|
||||
|
||||
@@ -3091,12 +3084,6 @@ class PartPricing(common.models.MetaMixin):
|
||||
if max_sell_history is None or cost > max_sell_history:
|
||||
max_sell_history = cost
|
||||
|
||||
if (
|
||||
self.sale_history_min != min_sell_history
|
||||
or self.sale_history_max != max_sell_history
|
||||
):
|
||||
self.price_modified = True
|
||||
|
||||
self.sale_history_min = min_sell_history
|
||||
self.sale_history_max = max_sell_history
|
||||
|
||||
@@ -4509,7 +4496,10 @@ def update_bom_build_lines(sender, instance, created, **kwargs):
|
||||
def update_pricing_after_edit(sender, instance, created, **kwargs):
|
||||
"""Callback function when a part price break is created or updated."""
|
||||
# Update part pricing *unless* we are importing data
|
||||
if InvenTree.ready.canAppAccessDatabase() and not InvenTree.ready.isImportingData():
|
||||
if (
|
||||
InvenTree.ready.canAppAccessDatabase(allow_test=settings.TESTING_PRICING)
|
||||
and not InvenTree.ready.isImportingData()
|
||||
):
|
||||
if instance.part:
|
||||
instance.part.schedule_pricing_update(create=True)
|
||||
|
||||
@@ -4526,7 +4516,10 @@ def update_pricing_after_edit(sender, instance, created, **kwargs):
|
||||
def update_pricing_after_delete(sender, instance, **kwargs):
|
||||
"""Callback function when a part price break is deleted."""
|
||||
# Update part pricing *unless* we are importing data
|
||||
if InvenTree.ready.canAppAccessDatabase() and not InvenTree.ready.isImportingData():
|
||||
if (
|
||||
InvenTree.ready.canAppAccessDatabase(allow_test=settings.TESTING_PRICING)
|
||||
and not InvenTree.ready.isImportingData()
|
||||
):
|
||||
if instance.part:
|
||||
instance.part.schedule_pricing_update(create=False)
|
||||
|
||||
|
||||
@@ -73,7 +73,11 @@ def update_part_pricing(pricing: part.models.PartPricing, counter: int = 0):
|
||||
"""
|
||||
logger.info('Updating part pricing for %s', pricing.part)
|
||||
|
||||
pricing.update_pricing(counter=counter)
|
||||
pricing.update_pricing(
|
||||
counter=counter,
|
||||
previous_min=pricing.overall_min,
|
||||
previous_max=pricing.overall_max,
|
||||
)
|
||||
|
||||
|
||||
@scheduled_task(ScheduledTask.DAILY)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Unit tests for Part pricing calculations."""
|
||||
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.test.utils import override_settings
|
||||
|
||||
from djmoney.contrib.exchange.models import convert_money
|
||||
from djmoney.money import Money
|
||||
@@ -88,6 +89,7 @@ class PartPricingTests(InvenTreeTestCase):
|
||||
part=self.sp_2, quantity=10, price=4.55, price_currency='GBP'
|
||||
)
|
||||
|
||||
@override_settings(TESTING_PRICING=True)
|
||||
def test_pricing_data(self):
|
||||
"""Test link between Part and PartPricing model."""
|
||||
# Initially there is no associated Pricing data
|
||||
@@ -113,6 +115,7 @@ class PartPricingTests(InvenTreeTestCase):
|
||||
"""Ensure that conversion behaves properly with missing rates."""
|
||||
...
|
||||
|
||||
@override_settings(TESTING_PRICING=True)
|
||||
def test_simple(self):
|
||||
"""Tests for hard-coded values."""
|
||||
pricing = self.part.pricing
|
||||
@@ -144,6 +147,7 @@ class PartPricingTests(InvenTreeTestCase):
|
||||
self.assertEqual(pricing.overall_min, Money('0.111111', 'USD'))
|
||||
self.assertEqual(pricing.overall_max, Money('25', 'USD'))
|
||||
|
||||
@override_settings(TESTING_PRICING=True)
|
||||
def test_supplier_part_pricing(self):
|
||||
"""Test for supplier part pricing."""
|
||||
pricing = self.part.pricing
|
||||
@@ -157,19 +161,22 @@ class PartPricingTests(InvenTreeTestCase):
|
||||
# Creating price breaks will cause the pricing to be updated
|
||||
self.create_price_breaks()
|
||||
|
||||
pricing.update_pricing()
|
||||
pricing = self.part.pricing
|
||||
pricing.refresh_from_db()
|
||||
|
||||
self.assertAlmostEqual(float(pricing.overall_min.amount), 2.015, places=2)
|
||||
self.assertAlmostEqual(float(pricing.overall_max.amount), 3.06, places=2)
|
||||
|
||||
# Delete all supplier parts and re-calculate
|
||||
self.part.supplier_parts.all().delete()
|
||||
pricing.update_pricing()
|
||||
|
||||
pricing = self.part.pricing
|
||||
pricing.refresh_from_db()
|
||||
|
||||
self.assertIsNone(pricing.supplier_price_min)
|
||||
self.assertIsNone(pricing.supplier_price_max)
|
||||
|
||||
@override_settings(TESTING_PRICING=True)
|
||||
def test_internal_pricing(self):
|
||||
"""Tests for internal price breaks."""
|
||||
# Ensure internal pricing is enabled
|
||||
@@ -189,7 +196,8 @@ class PartPricingTests(InvenTreeTestCase):
|
||||
part=self.part, quantity=ii + 1, price=10 - ii, price_currency=currency
|
||||
)
|
||||
|
||||
pricing.update_internal_cost()
|
||||
pricing = self.part.pricing
|
||||
pricing.refresh_from_db()
|
||||
|
||||
# Expected money value
|
||||
m_expected = Money(10 - ii, currency)
|
||||
@@ -202,6 +210,7 @@ class PartPricingTests(InvenTreeTestCase):
|
||||
self.assertEqual(pricing.internal_cost_max, Money(10, currency))
|
||||
self.assertEqual(pricing.overall_max, Money(10, currency))
|
||||
|
||||
@override_settings(TESTING_PRICING=True)
|
||||
def test_stock_item_pricing(self):
|
||||
"""Test for stock item pricing data."""
|
||||
# Create a part
|
||||
@@ -244,6 +253,7 @@ class PartPricingTests(InvenTreeTestCase):
|
||||
self.assertEqual(pricing.overall_min, Money(1.176471, 'USD'))
|
||||
self.assertEqual(pricing.overall_max, Money(6.666667, 'USD'))
|
||||
|
||||
@override_settings(TESTING_PRICING=True)
|
||||
def test_bom_pricing(self):
|
||||
"""Unit test for BOM pricing calculations."""
|
||||
pricing = self.part.pricing
|
||||
@@ -253,7 +263,8 @@ class PartPricingTests(InvenTreeTestCase):
|
||||
|
||||
currency = 'AUD'
|
||||
|
||||
for ii in range(10):
|
||||
# Create pricing out of order, to ensure min/max values are calculated correctly
|
||||
for ii in range(5):
|
||||
# Create a new part for the BOM
|
||||
sub_part = part.models.Part.objects.create(
|
||||
name=f'Sub Part {ii}',
|
||||
@@ -274,15 +285,21 @@ class PartPricingTests(InvenTreeTestCase):
|
||||
part=self.part, sub_part=sub_part, quantity=5
|
||||
)
|
||||
|
||||
pricing.update_bom_cost()
|
||||
|
||||
# Check that the values have been updated correctly
|
||||
self.assertEqual(pricing.currency, 'USD')
|
||||
|
||||
# Final overall pricing checks
|
||||
self.assertEqual(pricing.overall_min, Money('366.666665', 'USD'))
|
||||
self.assertEqual(pricing.overall_max, Money('550', 'USD'))
|
||||
# Price range should have been automatically updated
|
||||
self.part.refresh_from_db()
|
||||
pricing = self.part.pricing
|
||||
|
||||
expected_min = 100
|
||||
expected_max = 150
|
||||
|
||||
# Final overall pricing checks
|
||||
self.assertEqual(pricing.overall_min, Money(expected_min, 'USD'))
|
||||
self.assertEqual(pricing.overall_max, Money(expected_max, 'USD'))
|
||||
|
||||
@override_settings(TESTING_PRICING=True)
|
||||
def test_purchase_pricing(self):
|
||||
"""Unit tests for historical purchase pricing."""
|
||||
self.create_price_breaks()
|
||||
@@ -350,6 +367,7 @@ class PartPricingTests(InvenTreeTestCase):
|
||||
# Max cost in USD
|
||||
self.assertAlmostEqual(float(pricing.purchase_cost_max.amount), 6.95, places=2)
|
||||
|
||||
@override_settings(TESTING_PRICING=True)
|
||||
def test_delete_with_pricing(self):
|
||||
"""Test for deleting a part which has pricing information."""
|
||||
# Create some pricing data
|
||||
@@ -374,6 +392,7 @@ class PartPricingTests(InvenTreeTestCase):
|
||||
with self.assertRaises(part.models.PartPricing.DoesNotExist):
|
||||
pricing.refresh_from_db()
|
||||
|
||||
@override_settings(TESTING_PRICING=True)
|
||||
def test_delete_without_pricing(self):
|
||||
"""Test that we can delete a part which does not have pricing information."""
|
||||
pricing = self.part.pricing
|
||||
@@ -389,6 +408,7 @@ class PartPricingTests(InvenTreeTestCase):
|
||||
with self.assertRaises(part.models.Part.DoesNotExist):
|
||||
self.part.refresh_from_db()
|
||||
|
||||
@override_settings(TESTING_PRICING=True)
|
||||
def test_check_missing_pricing(self):
|
||||
"""Tests for check_missing_pricing background task.
|
||||
|
||||
@@ -412,6 +432,7 @@ class PartPricingTests(InvenTreeTestCase):
|
||||
# Check that PartPricing objects have been created
|
||||
self.assertEqual(part.models.PartPricing.objects.count(), 101)
|
||||
|
||||
@override_settings(TESTING_PRICING=True)
|
||||
def test_delete_part_with_stock_items(self):
|
||||
"""Test deleting a part instance with stock items.
|
||||
|
||||
@@ -432,7 +453,7 @@ class PartPricingTests(InvenTreeTestCase):
|
||||
)
|
||||
|
||||
# Manually schedule a pricing update (does not happen automatically in testing)
|
||||
p.schedule_pricing_update(create=True, test=True)
|
||||
p.schedule_pricing_update(create=True)
|
||||
|
||||
# Check that a PartPricing object exists
|
||||
self.assertTrue(part.models.PartPricing.objects.filter(part=p).exists())
|
||||
@@ -444,5 +465,84 @@ class PartPricingTests(InvenTreeTestCase):
|
||||
self.assertFalse(part.models.PartPricing.objects.filter(part=p).exists())
|
||||
|
||||
# Try to update pricing (should fail gracefully as the Part has been deleted)
|
||||
p.schedule_pricing_update(create=False, test=True)
|
||||
p.schedule_pricing_update(create=False)
|
||||
self.assertFalse(part.models.PartPricing.objects.filter(part=p).exists())
|
||||
|
||||
@override_settings(TESTING_PRICING=True)
|
||||
def test_multi_level_bom(self):
|
||||
"""Test that pricing for multi-level BOMs is calculated correctly."""
|
||||
# Create some parts
|
||||
A1 = part.models.Part.objects.create(
|
||||
name='A1', description='A1', assembly=True, component=True
|
||||
)
|
||||
B1 = part.models.Part.objects.create(
|
||||
name='B1', description='B1', assembly=True, component=True
|
||||
)
|
||||
C1 = part.models.Part.objects.create(
|
||||
name='C1', description='C1', assembly=True, component=True
|
||||
)
|
||||
D1 = part.models.Part.objects.create(
|
||||
name='D1', description='D1', assembly=True, component=True
|
||||
)
|
||||
D2 = part.models.Part.objects.create(
|
||||
name='D2', description='D2', assembly=True, component=True
|
||||
)
|
||||
D3 = part.models.Part.objects.create(
|
||||
name='D3', description='D3', assembly=True, component=True
|
||||
)
|
||||
|
||||
# BOM Items
|
||||
part.models.BomItem.objects.create(part=A1, sub_part=B1, quantity=10)
|
||||
part.models.BomItem.objects.create(part=B1, sub_part=C1, quantity=2)
|
||||
part.models.BomItem.objects.create(part=C1, sub_part=D1, quantity=3)
|
||||
part.models.BomItem.objects.create(part=C1, sub_part=D2, quantity=4)
|
||||
part.models.BomItem.objects.create(part=C1, sub_part=D3, quantity=5)
|
||||
|
||||
# Pricing data (only for low-level D parts)
|
||||
P1 = D1.pricing
|
||||
P1.override_min = 4.50
|
||||
P1.override_max = 5.50
|
||||
P1.save()
|
||||
P1.update_pricing()
|
||||
|
||||
P2 = D2.pricing
|
||||
P2.override_min = 6.50
|
||||
P2.override_max = 7.50
|
||||
P2.save()
|
||||
P2.update_pricing()
|
||||
|
||||
P3 = D3.pricing
|
||||
P3.override_min = 8.50
|
||||
P3.override_max = 9.50
|
||||
P3.save()
|
||||
P3.update_pricing()
|
||||
|
||||
# Simple checks for low-level BOM items
|
||||
self.assertEqual(D1.pricing.overall_min, Money(4.50, 'USD'))
|
||||
self.assertEqual(D1.pricing.overall_max, Money(5.50, 'USD'))
|
||||
|
||||
self.assertEqual(D2.pricing.overall_min, Money(6.50, 'USD'))
|
||||
self.assertEqual(D2.pricing.overall_max, Money(7.50, 'USD'))
|
||||
|
||||
self.assertEqual(D3.pricing.overall_min, Money(8.50, 'USD'))
|
||||
self.assertEqual(D3.pricing.overall_max, Money(9.50, 'USD'))
|
||||
|
||||
# Calculate pricing for "C" level part
|
||||
c_min = 3 * 4.50 + 4 * 6.50 + 5 * 8.50
|
||||
c_max = 3 * 5.50 + 4 * 7.50 + 5 * 9.50
|
||||
|
||||
self.assertEqual(C1.pricing.overall_min, Money(c_min, 'USD'))
|
||||
self.assertEqual(C1.pricing.overall_max, Money(c_max, 'USD'))
|
||||
|
||||
# Calculate pricing for "A" and "B" level parts
|
||||
b_min = 2 * c_min
|
||||
b_max = 2 * c_max
|
||||
|
||||
a_min = 10 * b_min
|
||||
a_max = 10 * b_max
|
||||
|
||||
self.assertEqual(B1.pricing.overall_min, Money(b_min, 'USD'))
|
||||
self.assertEqual(B1.pricing.overall_max, Money(b_max, 'USD'))
|
||||
|
||||
self.assertEqual(A1.pricing.overall_min, Money(a_min, 'USD'))
|
||||
self.assertEqual(A1.pricing.overall_max, Money(a_max, 'USD'))
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
import os
|
||||
|
||||
from django.db import connection, migrations
|
||||
from django.core.files.base import ContentFile
|
||||
from django.core.files.storage import default_storage
|
||||
from django.db import connection, migrations
|
||||
|
||||
import InvenTree.ready
|
||||
|
||||
@@ -48,7 +48,7 @@ def convert_legacy_labels(table_name, model_name, template_model):
|
||||
except Exception:
|
||||
# Table likely does not exist
|
||||
if not InvenTree.ready.isInTestMode():
|
||||
print(f"Legacy label table {table_name} not found - skipping migration")
|
||||
print(f"\nLegacy label table {table_name} not found - skipping migration")
|
||||
return 0
|
||||
|
||||
rows = cursor.fetchall()
|
||||
|
||||
@@ -2284,14 +2284,16 @@ def after_delete_stock_item(sender, instance: StockItem, **kwargs):
|
||||
"""Function to be executed after a StockItem object is deleted."""
|
||||
from part import tasks as part_tasks
|
||||
|
||||
if not InvenTree.ready.isImportingData() and InvenTree.ready.canAppAccessDatabase(
|
||||
allow_test=True
|
||||
):
|
||||
if InvenTree.ready.isImportingData():
|
||||
return
|
||||
|
||||
if InvenTree.ready.canAppAccessDatabase(allow_test=True):
|
||||
# Run this check in the background
|
||||
InvenTree.tasks.offload_task(
|
||||
part_tasks.notify_low_stock_if_required, instance.part
|
||||
)
|
||||
|
||||
if InvenTree.ready.canAppAccessDatabase(allow_test=settings.TESTING_PRICING):
|
||||
# Schedule an update on parent part pricing
|
||||
if instance.part:
|
||||
instance.part.schedule_pricing_update(create=False)
|
||||
@@ -2302,19 +2304,15 @@ def after_save_stock_item(sender, instance: StockItem, created, **kwargs):
|
||||
"""Hook function to be executed after StockItem object is saved/updated."""
|
||||
from part import tasks as part_tasks
|
||||
|
||||
if (
|
||||
created
|
||||
and not InvenTree.ready.isImportingData()
|
||||
and InvenTree.ready.canAppAccessDatabase(allow_test=True)
|
||||
):
|
||||
# Run this check in the background
|
||||
InvenTree.tasks.offload_task(
|
||||
part_tasks.notify_low_stock_if_required, instance.part
|
||||
)
|
||||
if created and not InvenTree.ready.isImportingData():
|
||||
if InvenTree.ready.canAppAccessDatabase(allow_test=True):
|
||||
InvenTree.tasks.offload_task(
|
||||
part_tasks.notify_low_stock_if_required, instance.part
|
||||
)
|
||||
|
||||
# Schedule an update on parent part pricing
|
||||
if instance.part:
|
||||
instance.part.schedule_pricing_update(create=True)
|
||||
if InvenTree.ready.canAppAccessDatabase(allow_test=settings.TESTING_PRICING):
|
||||
if instance.part:
|
||||
instance.part.schedule_pricing_update(create=True)
|
||||
|
||||
|
||||
class StockItemTracking(InvenTree.models.InvenTreeModel):
|
||||
|
||||
@@ -16,7 +16,7 @@ def clear_sessions(apps, schema_editor): # pragma: no cover
|
||||
try:
|
||||
engine = import_module(settings.SESSION_ENGINE)
|
||||
engine.SessionStore.clear_expired()
|
||||
print('Cleared all user sessions to deal with GHSA-2crp-q9pc-457j')
|
||||
print('\nCleared all user sessions to deal with GHSA-2crp-q9pc-457j')
|
||||
except Exception:
|
||||
# Database may not be ready yet, so this does not matter anyhow
|
||||
pass
|
||||
|
||||
@@ -39,6 +39,7 @@ import { UserRoles } from '../../enums/Roles';
|
||||
import { companyFields } from '../../forms/CompanyForms';
|
||||
import { useEditApiFormModal } from '../../hooks/UseForm';
|
||||
import { useInstance } from '../../hooks/UseInstance';
|
||||
import { apiUrl } from '../../states/ApiState';
|
||||
import { useUserState } from '../../states/UserState';
|
||||
import { AddressTable } from '../../tables/company/AddressTable';
|
||||
import { ContactTable } from '../../tables/company/ContactTable';
|
||||
@@ -143,7 +144,7 @@ export default function CompanyDetail(props: Readonly<CompanyDetailProps>) {
|
||||
<Grid.Col span={4}>
|
||||
<DetailsImage
|
||||
appRole={UserRoles.purchase_order}
|
||||
apiPath={ApiEndpoints.company_list}
|
||||
apiPath={apiUrl(ApiEndpoints.company_list, company.pk)}
|
||||
src={company.image}
|
||||
pk={company.pk}
|
||||
refresh={refreshInstance}
|
||||
|
||||
Reference in New Issue
Block a user