mirror of
https://github.com/inventree/InvenTree.git
synced 2025-06-15 11:35:41 +00:00
Merge remote-tracking branch 'inventree/master'
This commit is contained in:
16
.github/workflows/docker.yaml
vendored
16
.github/workflows/docker.yaml
vendored
@ -26,7 +26,8 @@ jobs:
|
||||
# Build the docker image
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@ -56,13 +57,22 @@ jobs:
|
||||
- name: Set up Docker Buildx
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/setup-buildx-action@v1
|
||||
- name: Set up cosign
|
||||
uses: sigstore/cosign-installer@48866aa521d8bf870604709cd43ec2f602d03ff2
|
||||
- name: Login to Dockerhub
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Extract Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@69f6fc9d46f2f8bf0d5491e4aabe0bb8c6a4678a
|
||||
with:
|
||||
images: |
|
||||
inventree/inventree
|
||||
- name: Build and Push
|
||||
id: build-and-push
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
@ -74,6 +84,10 @@ jobs:
|
||||
build-args: |
|
||||
commit_hash=${{ env.git_commit_hash }}
|
||||
commit_date=${{ env.git_commit_date }}
|
||||
- name: Sign the published image
|
||||
env:
|
||||
COSIGN_EXPERIMENTAL: "true"
|
||||
run: cosign sign ${{ steps.meta.outputs.tags }}@${{ steps.build-and-push.outputs.digest }}
|
||||
- name: Push to Stable Branch
|
||||
uses: ad-m/github-push-action@master
|
||||
if: env.stable_release == 'true'
|
||||
|
13
Dockerfile
13
Dockerfile
@ -127,20 +127,9 @@ FROM base as dev
|
||||
|
||||
ENV INVENTREE_DEBUG=True
|
||||
|
||||
ENV INVENTREE_DEV_DIR="${INVENTREE_HOME}/dev"
|
||||
|
||||
# Location for python virtual environment
|
||||
# If the INVENTREE_PY_ENV variable is set, the entrypoint script will use it!
|
||||
ENV INVENTREE_PY_ENV="${INVENTREE_DEV_DIR}/env"
|
||||
|
||||
# Override default path settings
|
||||
ENV INVENTREE_STATIC_ROOT="${INVENTREE_DEV_DIR}/static"
|
||||
ENV INVENTREE_MEDIA_ROOT="${INVENTREE_DEV_DIR}/media"
|
||||
ENV INVENTREE_PLUGIN_DIR="${INVENTREE_DEV_DIR}/plugins"
|
||||
|
||||
ENV INVENTREE_CONFIG_FILE="${INVENTREE_DEV_DIR}/config.yaml"
|
||||
ENV INVENTREE_SECRET_KEY_FILE="${INVENTREE_DEV_DIR}/secret_key.txt"
|
||||
ENV INVENTREE_PLUGIN_FILE="${INVENTREE_DEV_DIR}/plugins.txt"
|
||||
ENV INVENTREE_PY_ENV="${INVENTREE_DATA_DIR}/env"
|
||||
|
||||
WORKDIR ${INVENTREE_HOME}
|
||||
|
||||
|
@ -114,9 +114,9 @@ C) Look for default key file "secret_key.txt"
|
||||
d) Create "secret_key.txt" if it does not exist
|
||||
"""
|
||||
|
||||
if os.getenv("INVENTREE_SECRET_KEY"):
|
||||
if secret_key := os.getenv("INVENTREE_SECRET_KEY"):
|
||||
# Secret key passed in directly
|
||||
SECRET_KEY = os.getenv("INVENTREE_SECRET_KEY").strip() # pragma: no cover
|
||||
SECRET_KEY = secret_key.strip() # pragma: no cover
|
||||
logger.info("SECRET_KEY loaded by INVENTREE_SECRET_KEY") # pragma: no cover
|
||||
else:
|
||||
# Secret key passed in by file location
|
||||
|
@ -691,7 +691,7 @@ class TestSettings(helpers.InvenTreeTestCase):
|
||||
|
||||
valid = [
|
||||
'inventree/config.yaml',
|
||||
'inventree/dev/config.yaml',
|
||||
'inventree/data/config.yaml',
|
||||
]
|
||||
|
||||
self.assertTrue(any([opt in config.get_config_file().lower() for opt in valid]))
|
||||
@ -706,7 +706,7 @@ class TestSettings(helpers.InvenTreeTestCase):
|
||||
|
||||
valid = [
|
||||
'inventree/plugins.txt',
|
||||
'inventree/dev/plugins.txt',
|
||||
'inventree/data/plugins.txt',
|
||||
]
|
||||
|
||||
self.assertTrue(any([opt in config.get_plugin_file().lower() for opt in valid]))
|
||||
|
@ -813,6 +813,14 @@ class Build(MPTTModel, ReferenceIndexingMixin):
|
||||
interchangeable = kwargs.get('interchangeable', False)
|
||||
substitutes = kwargs.get('substitutes', True)
|
||||
|
||||
def stock_sort(item, bom_item, variant_parts):
|
||||
if item.part == bom_item.sub_part:
|
||||
return 1
|
||||
elif item.part in variant_parts:
|
||||
return 2
|
||||
else:
|
||||
return 3
|
||||
|
||||
# Get a list of all 'untracked' BOM items
|
||||
for bom_item in self.untracked_bom_items:
|
||||
|
||||
@ -859,15 +867,7 @@ class Build(MPTTModel, ReferenceIndexingMixin):
|
||||
|
||||
This ensures that allocation priority is first given to "direct" parts
|
||||
"""
|
||||
def stock_sort(item):
|
||||
if item.part == bom_item.sub_part:
|
||||
return 1
|
||||
elif item.part in variant_parts:
|
||||
return 2
|
||||
else:
|
||||
return 3
|
||||
|
||||
available_stock = sorted(available_stock, key=stock_sort)
|
||||
available_stock = sorted(available_stock, key=lambda item, b=bom_item, v=variant_parts: stock_sort(item, b, v))
|
||||
|
||||
if len(available_stock) == 0:
|
||||
# No stock items are available
|
||||
|
@ -495,7 +495,7 @@ class PurchaseOrderLineItemList(APIDownloadMixin, ListCreateAPI):
|
||||
search_fields = [
|
||||
'part__part__name',
|
||||
'part__part__description',
|
||||
'part__MPN',
|
||||
'part__manufacturer_part__MPN',
|
||||
'part__SKU',
|
||||
'reference',
|
||||
]
|
||||
|
@ -168,23 +168,16 @@
|
||||
{% if order.status == PurchaseOrderStatus.PENDING %}
|
||||
$('#new-po-line').click(function() {
|
||||
|
||||
var fields = poLineItemFields({
|
||||
order: {{ order.pk }},
|
||||
createPurchaseOrderLineItem({{ order.pk }}, {
|
||||
{% if order.supplier %}
|
||||
supplier: {{ order.supplier.pk }},
|
||||
{% if order.supplier.currency %}
|
||||
currency: '{{ order.supplier.currency }}',
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
});
|
||||
|
||||
constructForm('{% url "api-po-line-list" %}', {
|
||||
fields: fields,
|
||||
method: 'POST',
|
||||
title: '{% trans "Add Line Item" %}',
|
||||
onSuccess: function() {
|
||||
$('#po-line-table').bootstrapTable('refresh');
|
||||
},
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -252,18 +252,19 @@ class APICallMixinTest(BaseMixinDefinition, TestCase):
|
||||
self.assertEqual(result.reason, 'OK')
|
||||
|
||||
# api_call with full url
|
||||
result = self.mixin.api_call('https://api.github.com/orgs/inventree', endpoint_is_url=True)
|
||||
result = self.mixin.api_call('orgs/inventree')
|
||||
self.assertTrue(result)
|
||||
|
||||
# api_call with post and data
|
||||
result = self.mixin.api_call(
|
||||
'repos/inventree/InvenTree',
|
||||
method='GET'
|
||||
'https://reqres.in/api/users/',
|
||||
data={"name": "morpheus", "job": "leader"},
|
||||
method='POST',
|
||||
endpoint_is_url=True,
|
||||
)
|
||||
|
||||
self.assertTrue(result)
|
||||
self.assertEqual(result['name'], 'InvenTree')
|
||||
self.assertEqual(result['html_url'], 'https://github.com/inventree/InvenTree')
|
||||
self.assertEqual(result['name'], 'morpheus')
|
||||
|
||||
# api_call with filter
|
||||
result = self.mixin.api_call('repos/inventree/InvenTree/stargazers', url_args={'page': '2'})
|
||||
|
@ -102,10 +102,14 @@ function editManufacturerPart(part, options={}) {
|
||||
}
|
||||
|
||||
|
||||
function supplierPartFields() {
|
||||
function supplierPartFields(options={}) {
|
||||
|
||||
return {
|
||||
part: {},
|
||||
var fields = {
|
||||
part: {
|
||||
filters: {
|
||||
purchaseable: true,
|
||||
}
|
||||
},
|
||||
manufacturer_part: {
|
||||
filters: {
|
||||
part_detail: true,
|
||||
@ -128,6 +132,12 @@ function supplierPartFields() {
|
||||
icon: 'fa-box',
|
||||
}
|
||||
};
|
||||
|
||||
if (options.part) {
|
||||
fields.manufacturer_part.filters.part = options.part;
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
/*
|
||||
@ -135,10 +145,11 @@ function supplierPartFields() {
|
||||
*/
|
||||
function createSupplierPart(options={}) {
|
||||
|
||||
var fields = supplierPartFields();
|
||||
var fields = supplierPartFields({
|
||||
part: options.part,
|
||||
});
|
||||
|
||||
if (options.part) {
|
||||
fields.manufacturer_part.filters.part = options.part;
|
||||
fields.part.hidden = true;
|
||||
fields.part.value = options.part;
|
||||
}
|
||||
|
@ -255,15 +255,20 @@ function renderOwner(name, data, parameters={}, options={}) {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
function renderPurchaseOrder(name, data, parameters={}, options={}) {
|
||||
|
||||
var html = `<span>${data.reference}</span>`;
|
||||
|
||||
var thumbnail = null;
|
||||
var html = '';
|
||||
|
||||
if (data.supplier_detail) {
|
||||
thumbnail = data.supplier_detail.thumbnail || data.supplier_detail.image;
|
||||
|
||||
html += ' - ' + select2Thumbnail(thumbnail);
|
||||
html += `<span>${data.supplier_detail.name}</span>`;
|
||||
html += select2Thumbnail(thumbnail);
|
||||
}
|
||||
|
||||
html += `<span>${data.reference}</span>`;
|
||||
|
||||
var thumbnail = null;
|
||||
|
||||
if (data.supplier_detail) {
|
||||
html += ` - <span>${data.supplier_detail.name}</span>`;
|
||||
}
|
||||
|
||||
if (data.description) {
|
||||
|
@ -16,6 +16,7 @@
|
||||
renderLink,
|
||||
salesOrderStatusDisplay,
|
||||
setupFilterList,
|
||||
supplierPartFields,
|
||||
*/
|
||||
|
||||
/* exported
|
||||
@ -25,6 +26,8 @@
|
||||
completePurchaseOrder,
|
||||
completeShipment,
|
||||
completePendingShipments,
|
||||
createPurchaseOrder,
|
||||
createPurchaseOrderLineItem,
|
||||
createSalesOrder,
|
||||
createSalesOrderShipment,
|
||||
editPurchaseOrderLineItem,
|
||||
@ -539,6 +542,26 @@ function createPurchaseOrder(options={}) {
|
||||
}
|
||||
|
||||
|
||||
// Create a new PurchaseOrderLineItem
|
||||
function createPurchaseOrderLineItem(order, options={}) {
|
||||
|
||||
var fields = poLineItemFields({
|
||||
order: order,
|
||||
supplier: options.supplier,
|
||||
currency: options.currency,
|
||||
});
|
||||
|
||||
constructForm('{% url "api-po-line-list" %}', {
|
||||
fields: fields,
|
||||
method: 'POST',
|
||||
title: '{% trans "Add Line Item" %}',
|
||||
onSuccess: function(response) {
|
||||
handleFormSuccess(response, options);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/* Construct a set of fields for the SalesOrderLineItem form */
|
||||
function soLineItemFields(options={}) {
|
||||
|
||||
@ -590,13 +613,40 @@ function poLineItemFields(options={}) {
|
||||
|
||||
var fields = {
|
||||
order: {
|
||||
hidden: true,
|
||||
filters: {
|
||||
supplier_detail: true,
|
||||
}
|
||||
},
|
||||
part: {
|
||||
filters: {
|
||||
part_detail: true,
|
||||
supplier_detail: true,
|
||||
supplier: options.supplier,
|
||||
},
|
||||
secondary: {
|
||||
method: 'POST',
|
||||
title: '{% trans "Add Supplier Part" %}',
|
||||
fields: function(data) {
|
||||
var fields = supplierPartFields({
|
||||
part: data.part,
|
||||
});
|
||||
|
||||
fields.supplier.value = options.supplier;
|
||||
|
||||
// Adjust manufacturer part query based on selected part
|
||||
fields.manufacturer_part.adjustFilters = function(query, opts) {
|
||||
|
||||
var part = getFormFieldValue('part', {}, opts);
|
||||
|
||||
if (part) {
|
||||
query.part = part;
|
||||
}
|
||||
|
||||
return query;
|
||||
};
|
||||
|
||||
return fields;
|
||||
}
|
||||
}
|
||||
},
|
||||
quantity: {},
|
||||
@ -610,6 +660,7 @@ function poLineItemFields(options={}) {
|
||||
|
||||
if (options.order) {
|
||||
fields.order.value = options.order;
|
||||
fields.order.hidden = true;
|
||||
}
|
||||
|
||||
if (options.currency) {
|
||||
|
@ -22,7 +22,7 @@ services:
|
||||
inventree-dev-db:
|
||||
container_name: inventree-dev-db
|
||||
image: postgres:13
|
||||
ports:
|
||||
expose:
|
||||
- ${INVENTREE_DB_PORT:-5432}/tcp
|
||||
environment:
|
||||
- PGDATA=/var/lib/postgresql/data/dev/pgdb
|
||||
|
@ -42,4 +42,7 @@ INVENTREE_CACHE_PORT=6379
|
||||
# Enable plugins?
|
||||
INVENTREE_PLUGINS_ENABLED=False
|
||||
|
||||
# Image tag that should be used
|
||||
INVENTREE_TAG=stable
|
||||
|
||||
COMPOSE_PROJECT_NAME=inventree-production
|
||||
|
@ -45,7 +45,7 @@ services:
|
||||
inventree-db:
|
||||
container_name: inventree-db
|
||||
image: postgres:13
|
||||
ports:
|
||||
expose:
|
||||
- ${INVENTREE_DB_PORT:-5432}/tcp
|
||||
environment:
|
||||
- PGDATA=/var/lib/postgresql/data/pgdb
|
||||
@ -65,16 +65,16 @@ services:
|
||||
- inventree-db
|
||||
env_file:
|
||||
- .env
|
||||
ports:
|
||||
- ${INVENTREE_CACHE_PORT:-6379}:6379
|
||||
restart: unless-stopped
|
||||
expose:
|
||||
- ${INVENTREE_CACHE_PORT:-6379}
|
||||
restart: always
|
||||
|
||||
# InvenTree web server services
|
||||
# Uses gunicorn as the web server
|
||||
inventree-server:
|
||||
container_name: inventree-server
|
||||
# If you wish to specify a particular InvenTree version, do so here
|
||||
image: inventree/inventree:stable
|
||||
image: inventree/inventree:${INVENTREE_TAG:-stable}
|
||||
expose:
|
||||
- 8000
|
||||
depends_on:
|
||||
@ -85,13 +85,14 @@ services:
|
||||
volumes:
|
||||
# Data volume must map to /home/inventree/data
|
||||
- inventree_data:/home/inventree/data
|
||||
- inventree_plugins:/home/inventree/InvenTree/plugins
|
||||
restart: unless-stopped
|
||||
|
||||
# Background worker process handles long-running or periodic tasks
|
||||
inventree-worker:
|
||||
container_name: inventree-worker
|
||||
# If you wish to specify a particular InvenTree version, do so here
|
||||
image: inventree/inventree:stable
|
||||
image: inventree/inventree:${INVENTREE_TAG:-stable}
|
||||
command: invoke worker
|
||||
depends_on:
|
||||
- inventree-server
|
||||
@ -100,6 +101,7 @@ services:
|
||||
volumes:
|
||||
# Data volume must map to /home/inventree/data
|
||||
- inventree_data:/home/inventree/data
|
||||
- inventree_plugins:/home/inventree/InvenTree/plugins
|
||||
restart: unless-stopped
|
||||
|
||||
# nginx acts as a reverse proxy
|
||||
@ -126,7 +128,6 @@ services:
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
# NOTE: Change /path/to/data to a directory on your local machine
|
||||
# Persistent data, stored external to the container(s)
|
||||
inventree_data:
|
||||
driver: local
|
||||
@ -135,3 +136,10 @@ volumes:
|
||||
o: bind
|
||||
# This directory specified where InvenTree data are stored "outside" the docker containers
|
||||
device: ${INVENTREE_EXT_VOLUME:?You must specify the 'INVENTREE_EXT_VOLUME' variable in the .env file!}
|
||||
inventree_plugins:
|
||||
driver: local
|
||||
driver_opts:
|
||||
type: none
|
||||
o: bind
|
||||
# This directory specified where the optional local plugin directory is stored "outside" the docker containers
|
||||
device: ${INVENTREE_EXT_PLUGINS:-./}
|
||||
|
14
tasks.py
14
tasks.py
@ -406,7 +406,13 @@ def import_fixtures(c):
|
||||
|
||||
|
||||
# Execution tasks
|
||||
@task(help={'address': 'Server address:port (default=127.0.0.1:8000)'})
|
||||
@task
|
||||
def wait(c):
|
||||
"""Wait until the database connection is ready."""
|
||||
return manage(c, "wait_for_db")
|
||||
|
||||
|
||||
@task(pre=[wait], help={'address': 'Server address:port (default=127.0.0.1:8000)'})
|
||||
def server(c, address="127.0.0.1:8000"):
|
||||
"""Launch a (deveopment) server using Django's in-built webserver.
|
||||
|
||||
@ -415,12 +421,6 @@ def server(c, address="127.0.0.1:8000"):
|
||||
manage(c, "runserver {address}".format(address=address), pty=True)
|
||||
|
||||
|
||||
@task
|
||||
def wait(c):
|
||||
"""Wait until the database connection is ready."""
|
||||
return manage(c, "wait_for_db")
|
||||
|
||||
|
||||
@task(pre=[wait])
|
||||
def worker(c):
|
||||
"""Run the InvenTree background worker process."""
|
||||
|
Reference in New Issue
Block a user