diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml
index 2ce55f03c1..d81e200dce 100644
--- a/.github/workflows/docker.yaml
+++ b/.github/workflows/docker.yaml
@@ -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'
diff --git a/Dockerfile b/Dockerfile
index f9d34ec1c1..bc2dc9d9d7 100644
--- a/Dockerfile
+++ b/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}
diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py
index 133474571e..447e07b09c 100644
--- a/InvenTree/InvenTree/settings.py
+++ b/InvenTree/InvenTree/settings.py
@@ -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
diff --git a/InvenTree/InvenTree/tests.py b/InvenTree/InvenTree/tests.py
index 2a3377c408..1f9c11e7fc 100644
--- a/InvenTree/InvenTree/tests.py
+++ b/InvenTree/InvenTree/tests.py
@@ -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]))
diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py
index 620b4f343b..c71defc40c 100644
--- a/InvenTree/build/models.py
+++ b/InvenTree/build/models.py
@@ -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
diff --git a/InvenTree/order/api.py b/InvenTree/order/api.py
index 634a07c580..25b974c94e 100644
--- a/InvenTree/order/api.py
+++ b/InvenTree/order/api.py
@@ -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',
]
diff --git a/InvenTree/order/templates/order/purchase_order_detail.html b/InvenTree/order/templates/order/purchase_order_detail.html
index 1c4b36d3aa..8e301acf39 100644
--- a/InvenTree/order/templates/order/purchase_order_detail.html
+++ b/InvenTree/order/templates/order/purchase_order_detail.html
@@ -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');
- },
+ }
});
});
diff --git a/InvenTree/plugin/base/integration/test_mixins.py b/InvenTree/plugin/base/integration/test_mixins.py
index 13cfb47d79..3f95d56694 100644
--- a/InvenTree/plugin/base/integration/test_mixins.py
+++ b/InvenTree/plugin/base/integration/test_mixins.py
@@ -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'})
diff --git a/InvenTree/templates/js/translated/company.js b/InvenTree/templates/js/translated/company.js
index 5af324d32f..681735db6a 100644
--- a/InvenTree/templates/js/translated/company.js
+++ b/InvenTree/templates/js/translated/company.js
@@ -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;
}
diff --git a/InvenTree/templates/js/translated/model_renderers.js b/InvenTree/templates/js/translated/model_renderers.js
index f3527bcdee..97970f67d4 100644
--- a/InvenTree/templates/js/translated/model_renderers.js
+++ b/InvenTree/templates/js/translated/model_renderers.js
@@ -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 = `${data.reference}`;
-
- var thumbnail = null;
+ var html = '';
if (data.supplier_detail) {
thumbnail = data.supplier_detail.thumbnail || data.supplier_detail.image;
- html += ' - ' + select2Thumbnail(thumbnail);
- html += `${data.supplier_detail.name}`;
+ html += select2Thumbnail(thumbnail);
+ }
+
+ html += `${data.reference}`;
+
+ var thumbnail = null;
+
+ if (data.supplier_detail) {
+ html += ` - ${data.supplier_detail.name}`;
}
if (data.description) {
diff --git a/InvenTree/templates/js/translated/order.js b/InvenTree/templates/js/translated/order.js
index f8166fc693..e6300b7326 100644
--- a/InvenTree/templates/js/translated/order.js
+++ b/InvenTree/templates/js/translated/order.js
@@ -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) {
diff --git a/docker-compose.yml b/docker-compose.yml
index baba646883..8ab381a5bd 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -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
diff --git a/docker/production/.env b/docker/production/.env
index 055ecff57d..171f2053e6 100644
--- a/docker/production/.env
+++ b/docker/production/.env
@@ -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
diff --git a/docker/production/docker-compose.yml b/docker/production/docker-compose.yml
index 092e62f510..45f0c01c7b 100644
--- a/docker/production/docker-compose.yml
+++ b/docker/production/docker-compose.yml
@@ -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:-./}
diff --git a/tasks.py b/tasks.py
index 5161676321..80a8180b3b 100644
--- a/tasks.py
+++ b/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."""