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 = `<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) { 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."""