Merge remote-tracking branch 'inventree/master'

This commit is contained in:
Oliver Walters 2022-06-01 20:19:14 +10:00
commit 9c0d060bf2
43 changed files with 20473 additions and 19718 deletions

26
.github/release.yml vendored Normal file
View File

@ -0,0 +1,26 @@
# .github/release.yml
changelog:
categories:
- title: Breaking Changes
labels:
- Semver-Major
- breaking
- title: New Features
labels:
- Semver-Minor
- enhancement
- title: Bug Fixes
labels:
- Semver-Patch
- bug
- title: Devops / Setup Changes
labels:
- docker
- setup
- demo
- CI
- security
- title: Other Changes
labels:
- "*"

View File

@ -2,7 +2,6 @@
# This workflow runs under any of the following conditions: # This workflow runs under any of the following conditions:
# #
# - Push to the master branch # - Push to the master branch
# - Push to the stable branch
# - Publish release # - Publish release
# #
# The following actions are performed: # The following actions are performed:
@ -21,7 +20,6 @@ on:
push: push:
branches: branches:
- 'master' - 'master'
- 'stable'
jobs: jobs:
@ -29,12 +27,15 @@ jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps: steps:
- name: Check out repo - name: Check out repo
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Version Check - name: Version Check
run: | run: |
python3 ci/check_version_number.py python3 ci/version_check.py
echo "git_commit_hash=$(git rev-parse --short HEAD)" >> $GITHUB_ENV echo "git_commit_hash=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
echo "git_commit_date=$(git show -s --format=%ci)" >> $GITHUB_ENV echo "git_commit_date=$(git show -s --format=%ci)" >> $GITHUB_ENV
- name: Run Unit Tests - name: Run Unit Tests
@ -65,5 +66,14 @@ jobs:
platforms: linux/amd64,linux/arm64,linux/arm/v7 platforms: linux/amd64,linux/arm64,linux/arm/v7
push: true push: true
target: production target: production
tags: inventree/inventree:${{ env.docker_tag }} tags: ${{ env.docker_tags }}
build-args: commit_hash=${{ env.git_commit_hash }},commit_date=${{ env.git_commit_date }},commit_tag=${{ env.docker_tag }} build-args: |
commit_hash=${{ env.git_commit_hash }}
commit_date=${{ env.git_commit_date }}
- name: Push to Stable Branch
uses: ad-m/github-push-action@master
if: env.stable_release == 'true'
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
branch: stable
force: true

View File

@ -91,9 +91,6 @@ jobs:
cache: 'pip' cache: 'pip'
- name: Run pre-commit Checks - name: Run pre-commit Checks
uses: pre-commit/action@v2.0.3 uses: pre-commit/action@v2.0.3
- name: Check version number
run: |
python3 ci/check_version_number.py
python: python:
name: Tests - inventree-python name: Tests - inventree-python

View File

@ -137,6 +137,7 @@ The tags describe issues and PRs in multiple areas:
| Area | Name | Description | | Area | Name | Description |
|---|---|---| |---|---|---|
| Type Labels | | | | Type Labels | | |
| | breaking | Indicates a major update or change which breaks compatibility |
| | bug | Identifies a bug which needs to be addressed | | | bug | Identifies a bug which needs to be addressed |
| | dependency | Relates to a project dependency | | | dependency | Relates to a project dependency |
| | duplicate | Duplicate of another issue or PR | | | duplicate | Duplicate of another issue or PR |

View File

@ -105,10 +105,9 @@ COPY docker/init.sh ${INVENTREE_MNG_DIR}/init.sh
WORKDIR ${INVENTREE_MNG_DIR} WORKDIR ${INVENTREE_MNG_DIR}
# Drop to the inventree user for the production image # Drop to the inventree user for the production image
RUN adduser inventree #RUN adduser inventree
RUN chown -R inventree:inventree ${INVENTREE_HOME} #RUN chown -R inventree:inventree ${INVENTREE_HOME}
#USER inventree
USER inventree
# Install InvenTree packages # Install InvenTree packages
RUN pip3 install --user --disable-pip-version-check -r ${INVENTREE_HOME}/requirements.txt RUN pip3 install --user --disable-pip-version-check -r ${INVENTREE_HOME}/requirements.txt

View File

@ -4,11 +4,17 @@ InvenTree API version information
# InvenTree API version # InvenTree API version
INVENTREE_API_VERSION = 51 INVENTREE_API_VERSION = 53
""" """
Increment this API version number whenever there is a significant change to the API that any clients need to know about Increment this API version number whenever there is a significant change to the API that any clients need to know about
v52 -> 2022-06-01 : https://github.com/inventree/InvenTree/pull/3110
- Adds extra search fields to the BuildOrder list API endpoint
v52 -> 2022-05-31 : https://github.com/inventree/InvenTree/pull/3103
- Allow part list API to be searched by supplier SKU
v51 -> 2022-05-24 : https://github.com/inventree/InvenTree/pull/3058 v51 -> 2022-05-24 : https://github.com/inventree/InvenTree/pull/3058
- Adds new fields to the SalesOrderShipment model - Adds new fields to the SalesOrderShipment model

View File

@ -106,8 +106,10 @@ class BuildList(APIDownloadMixin, generics.ListCreateAPIView):
search_fields = [ search_fields = [
'reference', 'reference',
'part__name',
'title', 'title',
'part__name',
'part__IPN',
'part__description',
] ]
def get_queryset(self): def get_queryset(self):

View File

@ -375,7 +375,7 @@ onPanelLoad('attachments', function() {
}, },
label: 'attachment', label: 'attachment',
success: function(data, status, xhr) { success: function(data, status, xhr) {
location.reload(); $('#attachment-table').bootstrapTable('refresh');
} }
} }
); );

View File

@ -3,7 +3,6 @@
from django.test import TestCase from django.test import TestCase
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db.utils import IntegrityError
from InvenTree import status_codes as status from InvenTree import status_codes as status
@ -194,14 +193,13 @@ class BuildTest(BuildTestBase):
b.save() b.save()
def test_duplicate_bom_line(self): def test_duplicate_bom_line(self):
# Try to add a duplicate BOM item - it should fail! # Try to add a duplicate BOM item - it should be allowed
with self.assertRaises(IntegrityError): BomItem.objects.create(
BomItem.objects.create( part=self.assembly,
part=self.assembly, sub_part=self.sub_part_1,
sub_part=self.sub_part_1, quantity=99
quantity=99 )
)
def allocate_stock(self, output, allocations): def allocate_stock(self, output, allocations):
""" """

View File

@ -1425,6 +1425,20 @@ class InvenTreeUserSetting(BaseInvenTreeSetting):
'validator': bool, 'validator': bool,
}, },
'SEARCH_PREVIEW_SHOW_SUPPLIER_PARTS': {
'name': _('Seach Supplier Parts'),
'description': _('Display supplier parts in search preview window'),
'default': True,
'validator': bool,
},
'SEARCH_PREVIEW_SHOW_MANUFACTURER_PARTS': {
'name': _('Search Manufacturer Parts'),
'description': _('Display manufacturer parts in search preview window'),
'default': True,
'validator': bool,
},
'SEARCH_HIDE_INACTIVE_PARTS': { 'SEARCH_HIDE_INACTIVE_PARTS': {
'name': _("Hide Inactive Parts"), 'name': _("Hide Inactive Parts"),
'description': _('Excluded inactive parts from search preview window'), 'description': _('Excluded inactive parts from search preview window'),

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1387,6 +1387,7 @@ class PartList(APIDownloadMixin, generics.ListCreateAPIView):
'keywords', 'keywords',
'category__name', 'category__name',
'manufacturer_parts__MPN', 'manufacturer_parts__MPN',
'supplier_parts__SKU',
] ]

View File

@ -0,0 +1,17 @@
# Generated by Django 3.2.13 on 2022-05-31 01:42
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('part', '0076_auto_20220516_0819'),
]
operations = [
migrations.AlterUniqueTogether(
name='bomitem',
unique_together=set(),
),
]

View File

@ -2899,9 +2899,6 @@ class BomItem(models.Model, DataImportMixin):
class Meta: class Meta:
verbose_name = _("BOM Item") verbose_name = _("BOM Item")
# Prevent duplication of parent/child rows
unique_together = ('part', 'sub_part')
def __str__(self): def __str__(self):
return "{n} x {child} to make {parent}".format( return "{n} x {child} to make {parent}".format(
parent=self.part.full_name, parent=self.part.full_name,

View File

@ -16,6 +16,8 @@
<tbody> <tbody>
{% include "InvenTree/settings/setting.html" with key="SEARCH_PREVIEW_SHOW_PARTS" user_setting=True icon='fa-shapes' %} {% include "InvenTree/settings/setting.html" with key="SEARCH_PREVIEW_SHOW_PARTS" user_setting=True icon='fa-shapes' %}
{% include "InvenTree/settings/setting.html" with key="SEARCH_HIDE_INACTIVE_PARTS" user_setting=True icon='fa-eye-slash' %} {% include "InvenTree/settings/setting.html" with key="SEARCH_HIDE_INACTIVE_PARTS" user_setting=True icon='fa-eye-slash' %}
{% include "InvenTree/settings/setting.html" with key="SEARCH_PREVIEW_SHOW_SUPPLIER_PARTS" user_setting=True icon='fa-building' %}
{% include "InvenTree/settings/setting.html" with key="SEARCH_PREVIEW_SHOW_MANUFACTURER_PARTS" user_setting=True icon='fa-industry' %}
{% include "InvenTree/settings/setting.html" with key="SEARCH_PREVIEW_SHOW_CATEGORIES" user_setting=True icon='fa-sitemap' %} {% include "InvenTree/settings/setting.html" with key="SEARCH_PREVIEW_SHOW_CATEGORIES" user_setting=True icon='fa-sitemap' %}
{% include "InvenTree/settings/setting.html" with key="SEARCH_PREVIEW_SHOW_STOCK" user_setting=True icon='fa-boxes' %} {% include "InvenTree/settings/setting.html" with key="SEARCH_PREVIEW_SHOW_STOCK" user_setting=True icon='fa-boxes' %}
{% include "InvenTree/settings/setting.html" with key="SEARCH_PREVIEW_HIDE_UNAVAILABLE_STOCK" user_setting=True icon='fa-eye-slash' %} {% include "InvenTree/settings/setting.html" with key="SEARCH_PREVIEW_HIDE_UNAVAILABLE_STOCK" user_setting=True icon='fa-eye-slash' %}

View File

@ -2,6 +2,18 @@
<div id='attachment-buttons'> <div id='attachment-buttons'>
<div class='btn-group' role='group'> <div class='btn-group' role='group'>
<div class='btn-group'>
<button class='btn btn-primary dropdown-toggle' type='buton' data-bs-toggle='dropdown' title='{% trans "Actions" %}'>
<span class='fas fa-tools'></span> <span class='caret'></span>
</button>
<ul class='dropdown-menu'>
<li>
<a class='dropdown-item' href='#' id='multi-attachment-delete' title='{% trans "Delete selected attachments" %}'>
<span class='fas fa-trash-alt icon-red'></span> {% trans "Delete Attachments" %}
</a>
</li>
</ul>
</div>
{% include "filter_list.html" with id="attachments" %} {% include "filter_list.html" with id="attachments" %}
</div> </div>
</div> </div>

View File

@ -57,6 +57,75 @@ function addAttachmentButtonCallbacks(url, fields={}) {
} }
/*
* Construct a form to delete attachment files
*/
function deleteAttachments(attachments, url, options={}) {
if (attachments.length == 0) {
console.warn('deleteAttachments function called with zero attachments provided');
return;
}
function renderAttachment(attachment, opts={}) {
var icon = '';
if (attachment.filename) {
icon = `<span class='fas fa-file-alt'></span>`;
} else if (attachment.link) {
icon = `<span class='fas fa-link'></span>`;
}
return `
<tr>
<td>${icon}</td>
<td>${attachment.filename || attachment.link}</td>
<td>${attachment.comment}</td>
</tr>`;
}
var rows = '';
attachments.forEach(function(att) {
rows += renderAttachment(att);
});
var html = `
<div class='alert alert-block alert-danger'>
{% trans "All selected attachments will be deleted" %}
</div>
<table class='table table-striped table-condensed'>
<tr>
<th></th>
<th>{% trans "Attachment" %}</th>
<th>{% trans "Comment" %}</th>
</tr>
${rows}
</table>
`;
constructFormBody({}, {
method: 'DELETE',
title: '{% trans "Delete Attachments" %}',
preFormContent: html,
onSubmit: function(fields, opts) {
inventreeMultiDelete(
url,
attachments,
{
modal: opts.modal,
success: function() {
// Refresh the table once all attachments are deleted
$('#attachment-table').bootstrapTable('refresh');
}
}
);
}
});
}
function reloadAttachmentTable() { function reloadAttachmentTable() {
$('#attachment-table').bootstrapTable('refresh'); $('#attachment-table').bootstrapTable('refresh');
@ -71,6 +140,15 @@ function loadAttachmentTable(url, options) {
addAttachmentButtonCallbacks(url, options.fields || {}); addAttachmentButtonCallbacks(url, options.fields || {});
// Add callback for the 'multi delete' button
$('#multi-attachment-delete').click(function() {
var attachments = getTableData(table);
if (attachments.length > 0) {
deleteAttachments(attachments, url);
}
});
$(table).inventreeTable({ $(table).inventreeTable({
url: url, url: url,
name: options.name || 'attachments', name: options.name || 'attachments',
@ -80,7 +158,9 @@ function loadAttachmentTable(url, options) {
sortable: true, sortable: true,
search: true, search: true,
queryParams: options.filters || {}, queryParams: options.filters || {},
uniqueId: 'pk',
onPostBody: function() { onPostBody: function() {
// Add callback for 'edit' button // Add callback for 'edit' button
$(table).find('.button-attachment-edit').click(function() { $(table).find('.button-attachment-edit').click(function() {
var pk = $(this).attr('pk'); var pk = $(this).attr('pk');
@ -105,15 +185,14 @@ function loadAttachmentTable(url, options) {
$(table).find('.button-attachment-delete').click(function() { $(table).find('.button-attachment-delete').click(function() {
var pk = $(this).attr('pk'); var pk = $(this).attr('pk');
constructForm(`${url}${pk}/`, { var attachment = $(table).bootstrapTable('getRowByUniqueId', pk);
method: 'DELETE', deleteAttachments([attachment], url);
confirmMessage: '{% trans "Confirm Delete" %}',
title: '{% trans "Delete Attachment" %}',
onSuccess: reloadAttachmentTable,
});
}); });
}, },
columns: [ columns: [
{
checkbox: true,
},
{ {
field: 'attachment', field: 'attachment',
title: '{% trans "Attachment" %}', title: '{% trans "Attachment" %}',

View File

@ -151,6 +151,46 @@ function updateSearch() {
); );
} }
if (checkPermission('part') && checkPermission('purchase_order')) {
var params = {
part_detail: true,
supplier_detail: true,
manufacturer_detail: true,
};
if (user_settings.SEARCH_HIDE_INACTIVE_PARTS) {
// Return *only* active parts
params.active = true;
}
if (user_settings.SEARCH_PREVIEW_SHOW_SUPPLIER_PARTS) {
addSearchQuery(
'supplierpart',
'{% trans "Supplier Parts" %}',
'{% url "api-supplier-part-list" %}',
params,
renderSupplierPart,
{
url: '/supplier-part',
}
);
}
if (user_settings.SEARCH_PREVIEW_SHOW_MANUFACTURER_PARTS) {
addSearchQuery(
'manufacturerpart',
'{% trans "Manufacturer Parts" %}',
'{% url "api-manufacturer-part-list" %}',
params,
renderManufacturerPart,
{
url: '/manufacturer-part',
}
);
}
}
if (checkPermission('part_category') && user_settings.SEARCH_PREVIEW_SHOW_CATEGORIES) { if (checkPermission('part_category') && user_settings.SEARCH_PREVIEW_SHOW_CATEGORIES) {
// Search for matching part categories // Search for matching part categories
addSearchQuery( addSearchQuery(

View File

@ -4,20 +4,79 @@ Ensure that the release tag matches the InvenTree version number:
master / main branch: master / main branch:
- version number must end with 'dev' - version number must end with 'dev'
stable branch:
- version number must *not* end with 'dev'
- version number cannot already exist as a release tag
tagged branch: tagged branch:
- version number must match tag being built - version number must match tag being built
- version number cannot already exist as a release tag - version number cannot already exist as a release tag
""" """
import json
import os import os
import re import re
import sys import sys
import requests
def get_existing_release_tags():
"""Request information on existing releases via the GitHub API"""
response = requests.get('https://api.github.com/repos/inventree/inventree/releases')
if response.status_code != 200:
raise ValueError(f'Unexpected status code from GitHub API: {response.status_code}')
data = json.loads(response.text)
# Return a list of all tags
tags = []
for release in data:
tag = release['tag_name'].strip()
match = re.match(r"^.*(\d+)\.(\d+)\.(\d+).*$", tag)
if len(match.groups()) != 3:
print(f"Version '{tag}' did not match expected pattern")
continue
tags.append([int(x) for x in match.groups()])
return tags
def check_version_number(version_string):
"""Check the provided version number.
Returns True if the provided version is the 'newest' InvenTree release
"""
print(f"Checking version '{version_string}'")
# Check that the version string matches the required format
match = re.match(r"^(\d+)\.(\d+)\.(\d+)(?: dev)?$", version_string)
if not match or len(match.groups()) != 3:
raise ValueError(f"Version string '{version_string}' did not match required pattern")
version_tuple = [int(x) for x in match.groups()]
# Look through the existing releases
existing = get_existing_release_tags()
# Assume that this is the highest release, unless told otherwise
highest_release = True
for release in existing:
if release == version_tuple:
raise ValueError(f"Duplicate release '{version_string}' exists!")
if release > version_tuple:
highest_release = False
print(f"Found newer release: {str(release)}")
return highest_release
if __name__ == '__main__': if __name__ == '__main__':
here = os.path.abspath(os.path.dirname(__file__)) here = os.path.abspath(os.path.dirname(__file__))
@ -49,24 +108,12 @@ if __name__ == '__main__':
print(f"InvenTree Version: '{version}'") print(f"InvenTree Version: '{version}'")
highest_release = check_version_number(version)
# Determine which docker tag we are going to use # Determine which docker tag we are going to use
docker_tag = None docker_tags = None
if GITHUB_REF_TYPE == 'branch' and ('stable' in GITHUB_REF or 'stable' in GITHUB_BASE_REF): if GITHUB_REF_TYPE == 'tag':
print("Checking requirements for 'stable' release branch:")
pattern = r"^\d+(\.\d+)+$"
result = re.match(pattern, version)
if result is None:
print(f"Version number '{version}' does not match required pattern for stable branch")
sys.exit(1)
else:
print(f"Version number '{version}' matches stable branch")
docker_tag = 'stable'
elif GITHUB_REF_TYPE == 'tag':
# GITHUB_REF should be of th eform /refs/heads/<tag> # GITHUB_REF should be of th eform /refs/heads/<tag>
version_tag = GITHUB_REF.split('/')[-1] version_tag = GITHUB_REF.split('/')[-1]
print(f"Checking requirements for tagged release - '{version_tag}':") print(f"Checking requirements for tagged release - '{version_tag}':")
@ -77,7 +124,10 @@ if __name__ == '__main__':
# TODO: Check if there is already a release with this tag! # TODO: Check if there is already a release with this tag!
docker_tag = version_tag if highest_release:
docker_tags = [version_tag, 'stable']
else:
docker_tags = [version_tag]
elif GITHUB_REF_TYPE == 'branch': elif GITHUB_REF_TYPE == 'branch':
# Otherwise we know we are targetting the 'master' branch # Otherwise we know we are targetting the 'master' branch
@ -92,7 +142,7 @@ if __name__ == '__main__':
else: else:
print(f"Version number '{version}' matches development branch") print(f"Version number '{version}' matches development branch")
docker_tag = 'latest' docker_tags = ['latest']
else: else:
print("Unsupported branch / version combination:") print("Unsupported branch / version combination:")
@ -102,13 +152,20 @@ if __name__ == '__main__':
print("GITHUB_REF:", GITHUB_REF) print("GITHUB_REF:", GITHUB_REF)
sys.exit(1) sys.exit(1)
if docker_tag is None: if docker_tags is None:
print("Docker tag could not be determined") print("Docker tag could not be determined")
sys.exit(1) sys.exit(1)
print(f"Version check passed for '{version}'!") print(f"Version check passed for '{version}'!")
print(f"Docker tag: '{docker_tag}'") print(f"Docker tags: '{docker_tags}'")
# Ref: https://getridbug.com/python/how-to-set-environment-variables-in-github-actions-using-python/ # Ref: https://getridbug.com/python/how-to-set-environment-variables-in-github-actions-using-python/
with open(os.getenv('GITHUB_ENV'), 'a') as env_file: with open(os.getenv('GITHUB_ENV'), 'a') as env_file:
env_file.write(f"docker_tag={docker_tag}\n")
# Construct tag string
tags = ",".join([f"inventree/inventree:{tag}" for tag in docker_tags])
env_file.write(f"docker_tags={tags}\n")
if GITHUB_REF_TYPE == 'tag' and highest_release:
env_file.write("stable_release=true\n")