Merge branch 'inventree:master' into automatic-shipment-creation

This commit is contained in:
Maksim Stojkovic 2022-05-19 00:28:41 +10:00 committed by GitHub
commit 5ece98ed39
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 460 additions and 100 deletions

View File

@ -18,6 +18,18 @@ jobs:
- name: Check version number
run: |
python3 ci/check_version_number.py --dev
- name: Build Docker Image
run: |
cd docker
docker-compose build
docker-compose run inventree-dev-server invoke update
- name: Run unit tests
run: |
cd docker
docker-compose up -d
docker-compose run inventree-dev-server invoke wait
docker-compose run inventree-dev-server invoke test
docker-compose down
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx

View File

@ -1,37 +0,0 @@
# Test that the InvenTree docker image compiles correctly
# This CI action runs on pushes to either the master or stable branches
# 1. Build the development docker image (as per the documentation)
# 2. Install requied python libs into the docker container
# 3. Launch the container
# 4. Check that the API endpoint is available
name: Docker Test
on:
push:
branches:
- 'master'
- 'stable'
jobs:
docker:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v2
- name: Build Docker Image
run: |
cd docker
docker-compose -f docker-compose.sqlite.yml build
docker-compose -f docker-compose.sqlite.yml run inventree-dev-server invoke update
docker-compose -f docker-compose.sqlite.yml up -d
- name: Sleepy Time
run: sleep 60
- name: Test API
run: |
pip install requests
python3 ci/check_api_endpoint.py

View File

@ -4,11 +4,14 @@ InvenTree API version information
# InvenTree API version
INVENTREE_API_VERSION = 49
INVENTREE_API_VERSION = 50
"""
Increment this API version number whenever there is a significant change to the API that any clients need to know about
v50 -> 2022-05-18 : https://github.com/inventree/InvenTree/pull/2912
- Implement Attachments for manufacturer parts
v49 -> 2022-05-09 : https://github.com/inventree/InvenTree/pull/2957
- Allows filtering of plugin list by 'active' status
- Allows filtering of plugin list by 'mixin' support

View File

@ -1,5 +1,7 @@
import json
from test.support import EnvironmentVarGuard
import os
from unittest import mock
from django.test import TestCase, override_settings
import django.core.exceptions as django_exceptions
@ -449,17 +451,20 @@ class TestSettings(TestCase):
def setUp(self) -> None:
self.user_mdl = get_user_model()
self.env = EnvironmentVarGuard()
# Create a user for auth
user = get_user_model()
self.user = user.objects.create_superuser('testuser1', 'test1@testing.com', 'password1')
self.client.login(username='testuser1', password='password1')
def run_reload(self):
def in_env_context(self, envs={}):
"""Patch the env to include the given dict"""
return mock.patch.dict(os.environ, envs)
def run_reload(self, envs={}):
from plugin import registry
with self.env:
with self.in_env_context(envs):
settings.USER_ADDED = False
registry.reload_plugins()
@ -475,25 +480,28 @@ class TestSettings(TestCase):
self.assertEqual(user_count(), 1)
# not enough set
self.env.set('INVENTREE_ADMIN_USER', 'admin') # set username
self.run_reload()
self.run_reload({
'INVENTREE_ADMIN_USER': 'admin'
})
self.assertEqual(user_count(), 1)
# enough set
self.env.set('INVENTREE_ADMIN_USER', 'admin') # set username
self.env.set('INVENTREE_ADMIN_EMAIL', 'info@example.com') # set email
self.env.set('INVENTREE_ADMIN_PASSWORD', 'password123') # set password
self.run_reload()
self.run_reload({
'INVENTREE_ADMIN_USER': 'admin', # set username
'INVENTREE_ADMIN_EMAIL': 'info@example.com', # set email
'INVENTREE_ADMIN_PASSWORD': 'password123' # set password
})
self.assertEqual(user_count(), 2)
# create user manually
self.user_mdl.objects.create_user('testuser', 'test@testing.com', 'password')
self.assertEqual(user_count(), 3)
# check it will not be created again
self.env.set('INVENTREE_ADMIN_USER', 'testuser')
self.env.set('INVENTREE_ADMIN_EMAIL', 'test@testing.com')
self.env.set('INVENTREE_ADMIN_PASSWORD', 'password')
self.run_reload()
self.run_reload({
'INVENTREE_ADMIN_USER': 'testuser',
'INVENTREE_ADMIN_EMAIL': 'test@testing.com',
'INVENTREE_ADMIN_PASSWORD': 'password',
})
self.assertEqual(user_count(), 3)
# make sure to clean up
@ -517,20 +525,30 @@ class TestSettings(TestCase):
def test_helpers_cfg_file(self):
# normal run - not configured
self.assertIn('InvenTree/InvenTree/config.yaml', config.get_config_file())
valid = [
'inventree/config.yaml',
'inventree/dev/config.yaml',
]
self.assertTrue(any([opt in config.get_config_file().lower() for opt in valid]))
# with env set
with self.env:
self.env.set('INVENTREE_CONFIG_FILE', 'my_special_conf.yaml')
self.assertIn('InvenTree/InvenTree/my_special_conf.yaml', config.get_config_file())
with self.in_env_context({'INVENTREE_CONFIG_FILE': 'my_special_conf.yaml'}):
self.assertIn('inventree/inventree/my_special_conf.yaml', config.get_config_file().lower())
def test_helpers_plugin_file(self):
# normal run - not configured
self.assertIn('InvenTree/InvenTree/plugins.txt', config.get_plugin_file())
valid = [
'inventree/plugins.txt',
'inventree/dev/plugins.txt',
]
self.assertTrue(any([opt in config.get_plugin_file().lower() for opt in valid]))
# with env set
with self.env:
self.env.set('INVENTREE_PLUGIN_FILE', 'my_special_plugins.txt')
with self.in_env_context({'INVENTREE_PLUGIN_FILE': 'my_special_plugins.txt'}):
self.assertIn('my_special_plugins.txt', config.get_plugin_file())
def test_helpers_setting(self):
@ -539,8 +557,7 @@ class TestSettings(TestCase):
self.assertEqual(config.get_setting(TEST_ENV_NAME, None, '123!'), '123!')
# with env set
with self.env:
self.env.set(TEST_ENV_NAME, '321')
with self.in_env_context({TEST_ENV_NAME: '321'}):
self.assertEqual(config.get_setting(TEST_ENV_NAME, None), '321')

View File

@ -8,7 +8,7 @@ import import_export.widgets as widgets
from .models import Company
from .models import SupplierPart
from .models import SupplierPriceBreak
from .models import ManufacturerPart, ManufacturerPartParameter
from .models import ManufacturerPart, ManufacturerPartAttachment, ManufacturerPartParameter
from part.models import Part
@ -109,6 +109,16 @@ class ManufacturerPartAdmin(ImportExportModelAdmin):
autocomplete_fields = ('part', 'manufacturer',)
class ManufacturerPartAttachmentAdmin(ImportExportModelAdmin):
"""
Admin class for ManufacturerPartAttachment model
"""
list_display = ('manufacturer_part', 'attachment', 'comment')
autocomplete_fields = ('manufacturer_part',)
class ManufacturerPartParameterResource(ModelResource):
"""
Class for managing ManufacturerPartParameter data import/export
@ -175,4 +185,5 @@ admin.site.register(SupplierPart, SupplierPartAdmin)
admin.site.register(SupplierPriceBreak, SupplierPriceBreakAdmin)
admin.site.register(ManufacturerPart, ManufacturerPartAdmin)
admin.site.register(ManufacturerPartAttachment, ManufacturerPartAttachmentAdmin)
admin.site.register(ManufacturerPartParameter, ManufacturerPartParameterAdmin)

View File

@ -12,13 +12,14 @@ from django.urls import include, re_path
from django.db.models import Q
from InvenTree.helpers import str2bool
from InvenTree.api import AttachmentMixin
from .models import Company
from .models import ManufacturerPart, ManufacturerPartParameter
from .models import ManufacturerPart, ManufacturerPartAttachment, ManufacturerPartParameter
from .models import SupplierPart, SupplierPriceBreak
from .serializers import CompanySerializer
from .serializers import ManufacturerPartSerializer, ManufacturerPartParameterSerializer
from .serializers import ManufacturerPartSerializer, ManufacturerPartAttachmentSerializer, ManufacturerPartParameterSerializer
from .serializers import SupplierPartSerializer, SupplierPriceBreakSerializer
@ -160,6 +161,32 @@ class ManufacturerPartDetail(generics.RetrieveUpdateDestroyAPIView):
serializer_class = ManufacturerPartSerializer
class ManufacturerPartAttachmentList(AttachmentMixin, generics.ListCreateAPIView):
"""
API endpoint for listing (and creating) a ManufacturerPartAttachment (file upload).
"""
queryset = ManufacturerPartAttachment.objects.all()
serializer_class = ManufacturerPartAttachmentSerializer
filter_backends = [
DjangoFilterBackend,
]
filter_fields = [
'manufacturer_part',
]
class ManufacturerPartAttachmentDetail(AttachmentMixin, generics.RetrieveUpdateDestroyAPIView):
"""
Detail endpooint for ManufacturerPartAttachment model
"""
queryset = ManufacturerPartAttachment.objects.all()
serializer_class = ManufacturerPartAttachmentSerializer
class ManufacturerPartParameterList(generics.ListCreateAPIView):
"""
API endpoint for list view of ManufacturerPartParamater model.
@ -387,6 +414,12 @@ class SupplierPriceBreakDetail(generics.RetrieveUpdateDestroyAPIView):
manufacturer_part_api_urls = [
# Base URL for ManufacturerPartAttachment API endpoints
re_path(r'^attachment/', include([
re_path(r'^(?P<pk>\d+)/', ManufacturerPartAttachmentDetail.as_view(), name='api-manufacturer-part-attachment-detail'),
re_path(r'^$', ManufacturerPartAttachmentList.as_view(), name='api-manufacturer-part-attachment-list'),
])),
re_path(r'^parameter/', include([
re_path(r'^(?P<pk>\d+)/', ManufacturerPartParameterDetail.as_view(), name='api-manufacturer-part-parameter-detail'),

View File

@ -0,0 +1,33 @@
# Generated by Django 3.2.13 on 2022-05-01 12:57
import InvenTree.fields
import InvenTree.models
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('company', '0042_supplierpricebreak_updated'),
]
operations = [
migrations.CreateModel(
name='ManufacturerPartAttachment',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('attachment', models.FileField(blank=True, help_text='Select file to attach', null=True, upload_to=InvenTree.models.rename_attachment, verbose_name='Attachment')),
('link', InvenTree.fields.InvenTreeURLField(blank=True, help_text='Link to external URL', null=True, verbose_name='Link')),
('comment', models.CharField(blank=True, help_text='File comment', max_length=100, verbose_name='Comment')),
('upload_date', models.DateField(auto_now_add=True, null=True, verbose_name='upload date')),
('manufacturer_part', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='attachments', to='company.manufacturerpart', verbose_name='Manufacturer Part')),
('user', models.ForeignKey(blank=True, help_text='User', null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='User')),
],
options={
'abstract': False,
},
),
]

View File

@ -22,6 +22,7 @@ from stdimage.models import StdImageField
from InvenTree.helpers import getMediaUrl, getBlankImage, getBlankThumbnail
from InvenTree.fields import InvenTreeURLField
from InvenTree.models import InvenTreeAttachment
from InvenTree.status_codes import PurchaseOrderStatus
import InvenTree.validators
@ -380,6 +381,22 @@ class ManufacturerPart(models.Model):
return s
class ManufacturerPartAttachment(InvenTreeAttachment):
"""
Model for storing file attachments against a ManufacturerPart object
"""
@staticmethod
def get_api_url():
return reverse('api-manufacturer-part-attachment-list')
def getSubdir(self):
return os.path.join("manufacturer_part_files", str(self.manufacturer_part.id))
manufacturer_part = models.ForeignKey(ManufacturerPart, on_delete=models.CASCADE,
verbose_name=_('Manufacturer Part'), related_name='attachments')
class ManufacturerPartParameter(models.Model):
"""
A ManufacturerPartParameter represents a key:value parameter for a MnaufacturerPart.

View File

@ -8,6 +8,7 @@ from rest_framework import serializers
from sql_util.utils import SubqueryCount
from InvenTree.serializers import InvenTreeAttachmentSerializer
from InvenTree.serializers import InvenTreeDecimalField
from InvenTree.serializers import InvenTreeImageSerializerField
from InvenTree.serializers import InvenTreeModelSerializer
@ -16,7 +17,7 @@ from InvenTree.serializers import InvenTreeMoneySerializer
from part.serializers import PartBriefSerializer
from .models import Company
from .models import ManufacturerPart, ManufacturerPartParameter
from .models import ManufacturerPart, ManufacturerPartAttachment, ManufacturerPartParameter
from .models import SupplierPart, SupplierPriceBreak
from common.settings import currency_code_default, currency_code_mappings
@ -142,6 +143,29 @@ class ManufacturerPartSerializer(InvenTreeModelSerializer):
]
class ManufacturerPartAttachmentSerializer(InvenTreeAttachmentSerializer):
"""
Serializer for the ManufacturerPartAttachment class
"""
class Meta:
model = ManufacturerPartAttachment
fields = [
'pk',
'manufacturer_part',
'attachment',
'filename',
'link',
'comment',
'upload_date',
]
read_only_fields = [
'upload_date',
]
class ManufacturerPartParameterSerializer(InvenTreeModelSerializer):
"""
Serializer for the ManufacturerPartParameter model

View File

@ -144,6 +144,21 @@ src="{% static 'img/blank_image.png' %}"
</div>
</div>
<div class='panel panel-hidden' id='panel-attachments'>
<div class='panel-heading'>
<div class='d-flex flex-wrap'>
<h4>{% trans "Attachments" %}</h4>
{% include "spacer.html" %}
<div class='btn-group' role='group'>
{% include "attachment_button.html" %}
</div>
</div>
</div>
<div class='panel-content'>
{% include "attachment_table.html" %}
</div>
</div>
<div class='panel panel-hidden' id='panel-parameters'>
<div class='panel-heading'>
<div class='d-flex flex-wrap'>
@ -178,6 +193,34 @@ src="{% static 'img/blank_image.png' %}"
{% block js_ready %}
{{ block.super }}
onPanelLoad("attachments", function() {
loadAttachmentTable('{% url "api-manufacturer-part-attachment-list" %}', {
filters: {
manufacturer_part: {{ part.pk }},
},
fields: {
manufacturer_part: {
value: {{ part.pk }},
hidden: true
}
}
});
enableDragAndDrop(
'#attachment-dropzone',
'{% url "api-manufacturer-part-attachment-list" %}',
{
data: {
manufacturer_part: {{ part.id }},
},
label: 'attachment',
success: function(data, status, xhr) {
reloadAttachmentTable();
}
}
);
});
function reloadParameters() {
$("#parameter-table").bootstrapTable("refresh");
}

View File

@ -4,5 +4,7 @@
{% trans "Parameters" as text %}
{% include "sidebar_item.html" with label='parameters' text=text icon="fa-th-list" %}
{% trans "Attachments" as text %}
{% include "sidebar_item.html" with label='attachments' text=text icon="fa-paperclip" %}
{% trans "Supplier Parts" as text %}
{% include "sidebar_item.html" with label='supplier-parts' text=text icon="fa-building" %}

View File

@ -1046,24 +1046,29 @@ class PartDetailTests(InvenTreeAPITestCase):
)
self.assertEqual(response.status_code, 400)
self.assertIn('Upload a valid image', str(response.data))
# Now try to upload a valid image file
img = PIL.Image.new('RGB', (128, 128), color='red')
img.save('dummy_image.jpg')
# Now try to upload a valid image file, in multiple formats
for fmt in ['jpg', 'png', 'bmp', 'webp']:
fn = f'dummy_image.{fmt}'
with open('dummy_image.jpg', 'rb') as dummy_image:
response = upload_client.patch(
url,
{
'image': dummy_image,
},
format='multipart',
)
img = PIL.Image.new('RGB', (128, 128), color='red')
img.save(fn)
self.assertEqual(response.status_code, 200)
with open(fn, 'rb') as dummy_image:
response = upload_client.patch(
url,
{
'image': dummy_image,
},
format='multipart',
)
# And now check that the image has been set
p = Part.objects.get(pk=pk)
self.assertEqual(response.status_code, 200)
# And now check that the image has been set
p = Part.objects.get(pk=pk)
self.assertIsNotNone(p.image)
def test_details(self):
"""

View File

@ -2,6 +2,7 @@
from allauth.account.models import EmailAddress
from django.conf import settings
from django.contrib.auth import get_user_model
from django.test import TestCase
@ -64,11 +65,21 @@ class TemplateTagTest(TestCase):
def test_hash(self):
result_hash = inventree_extras.inventree_commit_hash()
self.assertGreater(len(result_hash), 5)
if settings.DOCKER:
# Testing inside docker environment *may* return an empty git commit hash
# In such a case, skip this check
pass
else:
self.assertGreater(len(result_hash), 5)
def test_date(self):
d = inventree_extras.inventree_commit_date()
self.assertEqual(len(d.split('-')), 3)
if settings.DOCKER:
# Testing inside docker environment *may* return an empty git commit hash
# In such a case, skip this check
pass
else:
self.assertEqual(len(d.split('-')), 3)
def test_github(self):
self.assertIn('github.com', inventree_extras.inventree_github_url())

View File

@ -13,6 +13,7 @@ import InvenTree.helpers
from plugin.helpers import MixinImplementationError, MixinNotImplementedError, render_template
from plugin.models import PluginConfig, PluginSetting
from plugin.registry import registry
from plugin.urls import PLUGIN_BASE
@ -204,7 +205,7 @@ class ScheduleMixin:
Schedule.objects.create(
name=task_name,
func='plugin.registry.call_function',
func=registry.call_plugin_function,
args=f"'{slug}', '{func_name}'",
schedule_type=task['schedule'],
minutes=task.get('minutes', None),

View File

@ -7,7 +7,7 @@ from rest_framework.views import APIView
from InvenTree.tasks import offload_task
from plugin import registry
from plugin.registry import registry
from stock.models import StockItem, StockLocation
@ -40,9 +40,6 @@ class LocatePluginView(APIView):
# StockLocation to identify
location_pk = request.data.get('location', None)
if not item_pk and not location_pk:
raise ParseError("Must supply either 'item' or 'location' parameter")
data = {
"success": "Identification plugin activated",
"plugin": plugin,
@ -53,27 +50,27 @@ class LocatePluginView(APIView):
try:
StockItem.objects.get(pk=item_pk)
offload_task(registry.call_function, plugin, 'locate_stock_item', item_pk)
offload_task(registry.call_plugin_function, plugin, 'locate_stock_item', item_pk)
data['item'] = item_pk
return Response(data)
except StockItem.DoesNotExist:
raise NotFound("StockItem matching PK '{item}' not found")
except (ValueError, StockItem.DoesNotExist):
raise NotFound(f"StockItem matching PK '{item_pk}' not found")
elif location_pk:
try:
StockLocation.objects.get(pk=location_pk)
offload_task(registry.call_function, plugin, 'locate_stock_location', location_pk)
offload_task(registry.call_plugin_function, plugin, 'locate_stock_location', location_pk)
data['location'] = location_pk
return Response(data)
except StockLocation.DoesNotExist:
raise NotFound("StockLocation matching PK {'location'} not found")
except (ValueError, StockLocation.DoesNotExist):
raise NotFound(f"StockLocation matching PK '{location_pk}' not found")
else:
raise NotFound()
raise ParseError("Must supply either 'item' or 'location' parameter")

View File

@ -0,0 +1,148 @@
"""
Unit tests for the 'locate' plugin mixin class
"""
from django.urls import reverse
from InvenTree.api_tester import InvenTreeAPITestCase
from plugin.registry import registry
from stock.models import StockItem, StockLocation
class LocatePluginTests(InvenTreeAPITestCase):
fixtures = [
'category',
'part',
'location',
'stock',
]
def test_installed(self):
"""Test that a locate plugin is actually installed"""
plugins = registry.with_mixin('locate')
self.assertTrue(len(plugins) > 0)
self.assertTrue('samplelocate' in [p.slug for p in plugins])
def test_locate_fail(self):
"""Test various API failure modes"""
url = reverse('api-locate-plugin')
# Post without a plugin
response = self.post(
url,
{},
expected_code=400
)
self.assertIn("'plugin' field must be supplied", str(response.data))
# Post with a plugin that does not exist, or is invalid
for slug in ['xyz', 'event', 'plugin']:
response = self.post(
url,
{
'plugin': slug,
},
expected_code=400,
)
self.assertIn(f"Plugin '{slug}' is not installed, or does not support the location mixin", str(response.data))
# Post with a valid plugin, but no other data
response = self.post(
url,
{
'plugin': 'samplelocate',
},
expected_code=400
)
self.assertIn("Must supply either 'item' or 'location' parameter", str(response.data))
# Post with valid plugin, invalid item or location
for pk in ['qq', 99999, -42]:
response = self.post(
url,
{
'plugin': 'samplelocate',
'item': pk,
},
expected_code=404
)
self.assertIn(f"StockItem matching PK '{pk}' not found", str(response.data))
response = self.post(
url,
{
'plugin': 'samplelocate',
'location': pk,
},
expected_code=404,
)
self.assertIn(f"StockLocation matching PK '{pk}' not found", str(response.data))
def test_locate_item(self):
"""
Test that the plugin correctly 'locates' a StockItem
As the background worker is not running during unit testing,
the sample 'locate' function will be called 'inline'
"""
url = reverse('api-locate-plugin')
item = StockItem.objects.get(pk=1)
# The sample plugin will set the 'located' metadata tag
item.set_metadata('located', False)
response = self.post(
url,
{
'plugin': 'samplelocate',
'item': 1,
},
expected_code=200
)
self.assertEqual(response.data['item'], 1)
item.refresh_from_db()
# Item metadata should have been altered!
self.assertTrue(item.metadata['located'])
def test_locate_location(self):
"""
Test that the plugin correctly 'locates' a StockLocation
"""
url = reverse('api-locate-plugin')
for location in StockLocation.objects.all():
location.set_metadata('located', False)
response = self.post(
url,
{
'plugin': 'samplelocate',
'location': location.pk,
},
expected_code=200
)
self.assertEqual(response.data['location'], location.pk)
location.refresh_from_db()
# Item metadata should have been altered!
self.assertTrue(location.metadata['located'])

View File

@ -23,7 +23,23 @@ class SampleLocatePlugin(LocateMixin, InvenTreePlugin):
SLUG = "samplelocate"
TITLE = "Sample plugin for locating items"
VERSION = "0.1"
VERSION = "0.2"
def locate_stock_item(self, item_pk):
from stock.models import StockItem
logger.info(f"SampleLocatePlugin attempting to locate item ID {item_pk}")
try:
item = StockItem.objects.get(pk=item_pk)
logger.info(f"StockItem {item_pk} located!")
# Tag metadata
item.set_metadata('located', True)
except (ValueError, StockItem.DoesNotExist):
logger.error(f"StockItem ID {item_pk} does not exist!")
def locate_stock_location(self, location_pk):
@ -34,5 +50,9 @@ class SampleLocatePlugin(LocateMixin, InvenTreePlugin):
try:
location = StockLocation.objects.get(pk=location_pk)
logger.info(f"Location exists at '{location.pathstring}'")
except StockLocation.DoesNotExist:
# Tag metadata
location.set_metadata('located', True)
except (ValueError, StockLocation.DoesNotExist):
logger.error(f"Location ID {location_pk} does not exist!")

View File

@ -1,5 +1,10 @@
import sys
import traceback
from django.conf import settings
from django.views.debug import ExceptionReporter
from error_report.models import Error
from plugin.registry import registry
@ -21,7 +26,21 @@ class InvenTreePluginViewMixin:
panels = []
for plug in registry.with_mixin('panel'):
panels += plug.render_panels(self, self.request, ctx)
try:
panels += plug.render_panels(self, self.request, ctx)
except Exception:
# Prevent any plugin error from crashing the page render
kind, info, data = sys.exc_info()
# Log the error to the database
Error.objects.create(
kind=kind.__name__,
info=info,
data='\n'.join(traceback.format_exception(kind, info, data)),
path=self.request.path,
html=ExceptionReporter(self.request, kind, info, data).get_traceback_html(),
)
return panels

View File

@ -101,6 +101,7 @@ class RuleSet(models.Model):
'company_supplierpart',
'company_manufacturerpart',
'company_manufacturerpartparameter',
'company_manufacturerpartattachment',
'label_partlabel',
],
'stock_location': [

View File

@ -1,4 +1,4 @@
FROM alpine:3.13 as base
FROM alpine:3.14 as base
# GitHub source
ARG repository="https://github.com/inventree/InvenTree.git"
@ -62,13 +62,13 @@ RUN apk -U upgrade
RUN apk add --no-cache git make bash \
gcc libgcc g++ libstdc++ \
gnupg \
libjpeg-turbo libjpeg-turbo-dev jpeg jpeg-dev \
libjpeg-turbo libjpeg-turbo-dev jpeg jpeg-dev libwebp-dev \
libffi libffi-dev \
zlib zlib-dev \
# Special deps for WeasyPrint (these will be deprecated once WeasyPrint drops cairo requirement)
cairo cairo-dev pango pango-dev gdk-pixbuf \
# Fonts
fontconfig ttf-droid ttf-liberation ttf-dejavu ttf-opensans ttf-ubuntu-font-family font-croscore font-noto \
fontconfig ttf-droid ttf-liberation ttf-dejavu ttf-opensans font-croscore font-noto \
# Core python
python3 python3-dev py3-pip \
# SQLite support

View File

@ -39,7 +39,7 @@ inventree # Install the latest version of the Inve
isort==5.10.1 # DEV: python import sorting
markdown==3.3.4 # Force particular version of markdown
pep8-naming==0.11.1 # PEP naming convention extension
pillow==9.0.1 # Image manipulation
pillow==9.1.0 # Image manipulation
py-moneyed==0.8.0 # Specific version requirement for py-moneyed
pygments==2.7.4 # Syntax highlighting
python-barcode[images]==0.13.1 # Barcode generator