Compare commits

...

15 Commits
1.0.7 ... 0.8.2

Author SHA1 Message Date
Oliver
62f44d06bf Add missing 'remove stock' action (#3610) (#3611)
* Add missing 'remove stock' action

* Add some unit tests for the stock item view

(cherry picked from commit 993f36c98f)
2022-08-25 21:31:23 +10:00
Oliver
ee5238b13e StockItem page template fix (#3601) (#3604)
- Allow damaged / quarantined items to be adjusted
- Fix template rendering logic

(cherry picked from commit 6adfc91c5c)
2022-08-24 17:17:46 +10:00
Oliver
2db9b28063 Bug fix for loading asset files in reports (#3596) (#3598)
- Pathlib does not play well with SafeString
- Enforce string type' when loading an asset file
- Add unit tests

(cherry picked from commit 12509203d6)
2022-08-24 12:01:37 +10:00
Oliver
30beb12c81 Fix dimensions for label templates (#3578) (#3580)
- Disable localization in certain areas
- Different localization settings could mess with label generation

(cherry picked from commit c8de2efd9d)
2022-08-19 12:35:24 +10:00
Oliver
644bcb263f fix: invalid chas in cache key (#3574) (#3577)
(cherry picked from commit efafa3960b)

Co-authored-by: wolflu05 <76838159+wolflu05@users.noreply.github.com>
2022-08-19 11:35:49 +10:00
Oliver
2ae1d1c663 fix typo in variable name (#3541) (#3543)
this fixes broken e-mail from configuration.

(cherry picked from commit d102a87eff)

Co-authored-by: Jacob Siverskog <jacob@teenage.engineering>
2022-08-16 13:08:33 +10:00
Oliver
951225b420 Fix bug in exporting records (#3545) (#3552)
Introduced in #3392

(cherry picked from commit 858d48afe7)

Co-authored-by: miggland <miggland@users.noreply.github.com>
2022-08-16 13:08:11 +10:00
Oliver
836ec3289d Fix weasyprint version (#3539) (#3540)
* Pin weasyprint version

- Revert to 54.3
- Fixes https://github.com/inventree/InvenTree/issues/3528

* Simplify label printing for multiple pages

* Simplify PDF generation for multiple report outputs

* Add content wrapper div for base label template

- Allows more extensibility

(cherry picked from commit 87e7112326)
2022-08-15 12:22:10 +10:00
Oliver
3482b81e8a Bump version to 0.8.2 (#3504) 2022-08-09 13:38:33 +10:00
Oliver
fb97385b23 load admin first (#3484) (#3492)
(cherry picked from commit 9182f62bc4)
2022-08-08 07:47:27 +10:00
Oliver
276ad572c5 Url field fix (#3488) (#3491)
* Updates for automated metadata extraction

* Update link field for StockItem model

- Increase max_length to 200 characters
- Custom migration
- Updates for InvenTreeUrlField model

* Adding unit tests

* Bug fix for metadata.py

(cherry picked from commit 63d221854b)
2022-08-08 07:47:17 +10:00
Oliver
1e7f4dcb4b Improved loading for custom logo (#3489) (#3490)
- First check the 'static' directory
- Second check the 'media' directory (backwards compatibility)
- Third use the default logo

(cherry picked from commit 83b471b4f7)
2022-08-07 23:16:57 +10:00
Oliver
72bec42c0f Depenency updates (#3472) (#3481)
* update requirements

* automate everything

(cherry picked from commit 12a321ed4f)

Co-authored-by: Matthias Mair <code@mjmair.com>
2022-08-06 10:02:44 +10:00
Oliver
8e5202b39a Improvements to version check CI script (#3455) (#3457)
* Improvements to version check CI script

* Fix typo

* Allow duplicate version tags for 'stable' branch

(cherry picked from commit 08d93e0727)
2022-08-02 13:34:08 +10:00
Oliver
63664714ca Return early if themes dir does not exist (#3453)
* Return early if themes dir does not exist

(cherry picked from commit 3e1cdcdb07)

* Bump version number to 0.8.1
2022-08-02 10:38:36 +10:00
27 changed files with 408 additions and 174 deletions

22
.github/workflows/update.yml vendored Normal file
View File

@@ -0,0 +1,22 @@
name: Update dependency files regularly
on:
workflow_dispatch:
schedule:
- cron: "0 0 * * *"
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup
run: pip install -r requirements-dev.txt
- name: Update requirements.txt
run: pip-compile --output-file=requirements.txt requirements.in -U
- name: Update requirements-dev.txt
run: pip-compile --generate-hashes --output-file=requirements-dev.txt requirements-dev.in -U
- uses: stefanzweifel/git-auto-commit-action@v4
with:
commit_message: "[Bot] Updated dependency"
branch: dep-update

View File

@@ -6,7 +6,6 @@ from decimal import Decimal
from django import forms
from django.core import validators
from django.db import models as models
from django.forms.fields import URLField as FormURLField
from django.utils.translation import gettext_lazy as _
from djmoney.forms.fields import MoneyField
@@ -23,26 +22,28 @@ class InvenTreeRestURLField(RestURLField):
"""Custom field for DRF with custom scheme vaildators."""
def __init__(self, **kwargs):
"""Update schemes."""
# Enforce 'max length' parameter in form validation
if 'max_length' not in kwargs:
kwargs['max_length'] = 200
super().__init__(**kwargs)
self.validators[-1].schemes = allowable_url_schemes()
class InvenTreeURLFormField(FormURLField):
"""Custom URL form field with custom scheme validators."""
default_validators = [validators.URLValidator(schemes=allowable_url_schemes())]
class InvenTreeURLField(models.URLField):
"""Custom URL field which has custom scheme validators."""
validators = [validators.URLValidator(schemes=allowable_url_schemes())]
default_validators = [validators.URLValidator(schemes=allowable_url_schemes())]
def formfield(self, **kwargs):
"""Return a Field instance for this field."""
return super().formfield(**{
'form_class': InvenTreeURLFormField
})
def __init__(self, **kwargs):
"""Initialization method for InvenTreeURLField"""
# Max length for InvenTreeURLField defaults to 200
if 'max_length' not in kwargs:
kwargs['max_length'] = 200
super().__init__(**kwargs)
def money_kwargs():

View File

@@ -12,6 +12,7 @@ from wsgiref.util import FileWrapper
from django.conf import settings
from django.contrib.auth.models import Permission
from django.contrib.staticfiles.storage import StaticFilesStorage
from django.core.exceptions import FieldError, ValidationError
from django.core.files.storage import default_storage
from django.core.validators import URLValidator
@@ -241,17 +242,27 @@ def getLogoImage(as_file=False, custom=True):
"""Return the path to the logo-file."""
if custom and settings.CUSTOM_LOGO:
if as_file:
return f"file://{default_storage.path(settings.CUSTOM_LOGO)}"
else:
return default_storage.url(settings.CUSTOM_LOGO)
static_storage = StaticFilesStorage()
else:
if as_file:
path = settings.STATIC_ROOT.joinpath('img/inventree.png')
return f"file://{path}"
if static_storage.exists(settings.CUSTOM_LOGO):
storage = static_storage
elif default_storage.exists(settings.CUSTOM_LOGO):
storage = default_storage
else:
return getStaticUrl('img/inventree.png')
storage = None
if storage is not None:
if as_file:
return f"file://{storage.path(settings.CUSTOM_LOGO)}"
else:
return storage.url(settings.CUSTOM_LOGO)
# If we have got to this point, return the default logo
if as_file:
path = settings.STATIC_ROOT.joinpath('img/inventree.png')
return f"file://{path}"
else:
return getStaticUrl('img/inventree.png')
def TestIfImageURL(url):

View File

@@ -116,6 +116,12 @@ class InvenTreeMetadata(SimpleMetadata):
model_class = None
# Attributes to copy extra attributes from the model to the field (if they don't exist)
extra_attributes = [
'help_text',
'max_length',
]
try:
model_class = serializer.Meta.model
@@ -148,10 +154,7 @@ class InvenTreeMetadata(SimpleMetadata):
elif name in model_default_values:
serializer_info[name]['default'] = model_default_values[name]
# Attributes to copy from the model to the field (if they don't exist)
attributes = ['help_text']
for attr in attributes:
for attr in extra_attributes:
if attr not in serializer_info[name]:
if hasattr(field, attr):
@@ -172,8 +175,9 @@ class InvenTreeMetadata(SimpleMetadata):
# This is used to automatically filter AJAX requests
serializer_info[name]['filters'] = relation.model_field.get_limit_choices_to()
if 'help_text' not in serializer_info[name] and hasattr(relation.model_field, 'help_text'):
serializer_info[name]['help_text'] = relation.model_field.help_text
for attr in extra_attributes:
if attr not in serializer_info[name] and hasattr(relation.model_field, attr):
serializer_info[name][attr] = getattr(relation.model_field, attr)
if name in model_default_values:
serializer_info[name]['default'] = model_default_values[name]

View File

@@ -16,6 +16,7 @@ import sys
from pathlib import Path
import django.conf.locale
from django.contrib.staticfiles.storage import StaticFilesStorage
from django.core.files.storage import default_storage
from django.http import Http404
from django.utils.translation import gettext_lazy as _
@@ -135,6 +136,8 @@ MEDIA_URL = '/media/'
# Application definition
INSTALLED_APPS = [
# Admin site integration
'django.contrib.admin',
# InvenTree apps
'build.apps.BuildConfig',
@@ -150,7 +153,6 @@ INSTALLED_APPS = [
'InvenTree.apps.InvenTreeConfig', # InvenTree app runs last
# Core django modules
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'user_sessions', # db user sessions
@@ -676,7 +678,7 @@ EMAIL_SUBJECT_PREFIX = get_setting('INVENTREE_EMAIL_PREFIX', 'email.prefix', '[I
EMAIL_USE_TLS = get_boolean_setting('INVENTREE_EMAIL_TLS', 'email.tls', False)
EMAIL_USE_SSL = get_boolean_setting('INVENTREE_EMAIL_SSL', 'email.ssl', False)
DEFUALT_FROM_EMAIL = get_setting('INVENTREE_EMAIL_SENDER', 'email.sender', '')
DEFAULT_FROM_EMAIL = get_setting('INVENTREE_EMAIL_SENDER', 'email.sender', '')
EMAIL_USE_LOCALTIME = False
EMAIL_TIMEOUT = 60
@@ -820,10 +822,22 @@ CUSTOMIZE = get_setting('INVENTREE_CUSTOMIZE', 'customize', {})
CUSTOM_LOGO = get_setting('INVENTREE_CUSTOM_LOGO', 'customize.logo', None)
# check that the logo-file exsists in media
if CUSTOM_LOGO and not default_storage.exists(CUSTOM_LOGO): # pragma: no cover
logger.warning(f"The custom logo file '{CUSTOM_LOGO}' could not be found in the default media storage")
CUSTOM_LOGO = False
"""
Check for the existence of a 'custom logo' file:
- Check the 'static' directory
- Check the 'media' directory (legacy)
"""
if CUSTOM_LOGO:
static_storage = StaticFilesStorage()
if static_storage.exists(CUSTOM_LOGO):
logger.info(f"Loading custom logo from static directory: {CUSTOM_LOGO}")
elif default_storage.exists(CUSTOM_LOGO):
logger.info(f"Loading custom logo from media directory: {CUSTOM_LOGO}")
else:
logger.warning(f"The custom logo file '{CUSTOM_LOGO}' could not be found in the static or media directories")
CUSTOM_LOGO = False
if DEBUG:
logger.info("InvenTree running with DEBUG enabled")

View File

@@ -13,7 +13,7 @@ import common.models
from InvenTree.api_version import INVENTREE_API_VERSION
# InvenTree software version
INVENTREE_SW_VERSION = "0.8.0"
INVENTREE_SW_VERSION = "0.8.2"
def inventreeInstanceName():

View File

@@ -132,7 +132,7 @@ class BaseInvenTreeSetting(models.Model):
for k, v in kwargs.items():
key += f"_{k}:{v}"
return key
return key.replace(" ", "")
@classmethod
def allValues(cls, user=None, exclude_hidden=False):
@@ -1775,12 +1775,13 @@ class ColorTheme(models.Model):
@classmethod
def get_color_themes_choices(cls):
"""Get all color themes from static folder."""
if settings.TESTING and not os.path.exists(settings.STATIC_COLOR_THEMES_DIR):
if not os.path.exists(settings.STATIC_COLOR_THEMES_DIR):
logger.error('Theme directory does not exsist')
return []
# Get files list from css/color-themes/ folder
files_list = []
for file in os.listdir(settings.STATIC_COLOR_THEMES_DIR):
files_list.append(os.path.splitext(file))

View File

@@ -158,16 +158,12 @@ class LabelPrintMixin:
pages = []
if len(outputs) > 1:
# If more than one output is generated, merge them into a single file
for output in outputs:
doc = output.get_document()
for page in doc.pages:
pages.append(page)
for output in outputs:
doc = output.get_document()
for page in doc.pages:
pages.append(page)
pdf = outputs[0].get_document().copy(pages).write_pdf()
else:
pdf = outputs[0].get_document().write_pdf()
pdf = outputs[0].get_document().copy(pages).write_pdf()
inline = common.models.InvenTreeUserSetting.get_setting('LABEL_INLINE', user=request.user)

View File

@@ -1,10 +1,13 @@
{% load l10n %}
{% load report %}
{% load barcode %}
<head>
<style>
@page {
{% localize off %}
size: {{ width }}mm {{ height }}mm;
{% endlocalize %}
{% block margin %}
margin: 0mm;
{% endblock %}
@@ -15,6 +18,8 @@
margin: 0mm;
color: #000;
background-color: #FFF;
page-break-before: always;
page-break-after: always;
}
img {
@@ -22,14 +27,23 @@
image-rendering: pixelated;
}
.content {
width: 100%;
break-after: always;
position: relative;
}
{% block style %}
/* User-defined styles can go here */
{% endblock %}
</style>
</head>
<body>
{% block content %}
<!-- Label data rendered here! -->
{% endblock %}
<div class='content'>
{% block content %}
<!-- Label data rendered here! -->
{% endblock %}
</div>
</body>

View File

@@ -1,5 +1,6 @@
{% extends "label/label_base.html" %}
{% load l10n %}
{% load barcode %}
{% block style %}
@@ -8,15 +9,19 @@
position: fixed;
left: 0mm;
top: 0mm;
{% localize off %}
height: {{ height }}mm;
width: {{ height }}mm;
{% endlocalize %}
}
.part {
font-family: Arial, Helvetica, sans-serif;
display: inline;
position: absolute;
{% localize off %}
left: {{ height }}mm;
{% endlocalize %}
top: 2mm;
}

View File

@@ -1,5 +1,6 @@
{% extends "label/label_base.html" %}
{% load l10n %}
{% load barcode %}
{% block style %}
@@ -8,15 +9,19 @@
position: fixed;
left: 0mm;
top: 0mm;
{% localize off %}
height: {{ height }}mm;
width: {{ height }}mm;
{% endlocalize %}
}
.part {
font-family: Arial, Helvetica, sans-serif;
display: inline;
position: absolute;
{% localize off %}
left: {{ height }}mm;
{% endlocalize %}
top: 2mm;
}

View File

@@ -1,5 +1,6 @@
{% extends "label/label_base.html" %}
{% load l10n %}
{% load barcode %}
{% block style %}
@@ -8,8 +9,10 @@
position: fixed;
left: 0mm;
top: 0mm;
{% localize off %}
height: {{ height }}mm;
width: {{ height }}mm;
{% endlocalize %}
}
{% endblock %}

View File

@@ -1,5 +1,6 @@
{% extends "label/label_base.html" %}
{% load l10n %}
{% load barcode %}
{% block style %}
@@ -8,8 +9,10 @@
position: fixed;
left: 0mm;
top: 0mm;
{% localize off %}
height: {{ height }}mm;
width: {{ height }}mm;
{% endlocalize %}
}
{% endblock %}

View File

@@ -1,5 +1,6 @@
{% extends "label/label_base.html" %}
{% load l10n %}
{% load barcode %}
{% block style %}
@@ -8,15 +9,19 @@
position: fixed;
left: 0mm;
top: 0mm;
{% localize off %}
height: {{ height }}mm;
width: {{ height }}mm;
{% endlocalize %}
}
.loc {
font-family: Arial, Helvetica, sans-serif;
display: inline;
position: absolute;
{% localize off %}
left: {{ height }}mm;
{% endlocalize %}
top: 2mm;
}

View File

@@ -233,17 +233,12 @@ class ReportPrintMixin:
pages = []
try:
for output in outputs:
doc = output.get_document()
for page in doc.pages:
pages.append(page)
if len(outputs) > 1:
# If more than one output is generated, merge them into a single file
for output in outputs:
doc = output.get_document()
for page in doc.pages:
pages.append(page)
pdf = outputs[0].get_document().copy(pages).write_pdf()
else:
pdf = outputs[0].get_document().write_pdf()
pdf = outputs[0].get_document().copy(pages).write_pdf()
except TemplateDoesNotExist as e:

View File

@@ -5,7 +5,7 @@ import os
from django import template
from django.conf import settings
from django.utils.safestring import mark_safe
from django.utils.safestring import SafeString, mark_safe
import InvenTree.helpers
from common.models import InvenTreeSetting
@@ -28,11 +28,15 @@ def asset(filename):
Raises:
FileNotFoundError if file does not exist
"""
if type(filename) is SafeString:
# Prepend an empty string to enforce 'stringiness'
filename = '' + filename
# If in debug mode, return URL to the image, not a local file
debug_mode = InvenTreeSetting.get_setting('REPORT_DEBUG_MODE')
# Test if the file actually exists
full_path = settings.MEDIA_ROOT.joinpath('report', 'assets', filename)
full_path = settings.MEDIA_ROOT.joinpath('report', 'assets', filename).resolve()
if not full_path.exists() or not full_path.is_file():
raise FileNotFoundError(f"Asset file '{filename}' does not exist")
@@ -55,6 +59,10 @@ def uploaded_image(filename, replace_missing=True, replacement_file='blank_image
A fully qualified path to the image
"""
if type(filename) is SafeString:
# Prepend an empty string to enforce 'stringiness'
filename = '' + filename
# If in debug mode, return URL to the image, not a local file
debug_mode = InvenTreeSetting.get_setting('REPORT_DEBUG_MODE')

View File

@@ -9,6 +9,7 @@ from django.core.cache import cache
from django.http.response import StreamingHttpResponse
from django.test import TestCase
from django.urls import reverse
from django.utils.safestring import SafeString
from PIL import Image
@@ -49,6 +50,10 @@ class ReportTagTest(TestCase):
asset = report_tags.asset('test.txt')
self.assertEqual(asset, '/media/report/assets/test.txt')
# Ensure that a 'safe string' also works
asset = report_tags.asset(SafeString('test.txt'))
self.assertEqual(asset, '/media/report/assets/test.txt')
self.debug_mode(False)
asset = report_tags.asset('test.txt')
self.assertEqual(asset, f'file://{asset_dir}/test.txt')
@@ -87,10 +92,17 @@ class ReportTagTest(TestCase):
img = report_tags.uploaded_image('part/images/test.jpg')
self.assertEqual(img, '/media/part/images/test.jpg')
# Ensure that a 'safe string' also works
img = report_tags.uploaded_image(SafeString('part/images/test.jpg'))
self.assertEqual(img, '/media/part/images/test.jpg')
self.debug_mode(False)
img = report_tags.uploaded_image('part/images/test.jpg')
self.assertEqual(img, f'file://{img_path.joinpath("test.jpg")}')
img = report_tags.uploaded_image(SafeString('part/images/test.jpg'))
self.assertEqual(img, f'file://{img_path.joinpath("test.jpg")}')
def test_part_image(self):
"""Unit tests for the 'part_image' tag"""

View File

@@ -0,0 +1,19 @@
# Generated by Django 3.2.15 on 2022-08-07 02:38
import InvenTree.fields
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('stock', '0081_auto_20220801_0044'),
]
operations = [
migrations.AlterField(
model_name='stockitem',
name='link',
field=InvenTree.fields.InvenTreeURLField(blank=True, help_text='Link to external URL', max_length=200, verbose_name='External Link'),
),
]

View File

@@ -647,7 +647,7 @@ class StockItem(MetadataMixin, MPTTModel):
link = InvenTreeURLField(
verbose_name=_('External Link'),
max_length=125, blank=True,
blank=True, max_length=200,
help_text=_("Link to external URL")
)

View File

@@ -75,24 +75,20 @@
<div class='btn-group'>
<button id='stock-actions' title='{% trans "Stock adjustment actions" %}' class='btn btn-outline-secondary dropdown-toggle' type='button' data-bs-toggle='dropdown'><span class='fas fa-boxes'></span> <span class='caret'></span></button>
<ul class='dropdown-menu' role='menu'>
{% if item.can_adjust_location %}
{% if not item.serialized %}
{% if item.in_stock %}
<li><a class='dropdown-item' href='#' id='stock-count' title='{% trans "Count stock" %}'><span class='fas fa-check-circle icon-green'></span> {% trans "Count stock" %}</a></li>
{% endif %}
{% if not item.customer %}
<li><a class='dropdown-item' href='#' id='stock-add' title='{% trans "Add stock" %}'><span class='fas fa-plus-circle icon-green'></span> {% trans "Add stock" %}</a></li>
{% endif %}
{% if item.in_stock %}
<li><a class='dropdown-item' href='#' id='stock-remove' title='{% trans "Remove stock" %}'><span class='fas fa-minus-circle icon-red'></span> {% trans "Remove stock" %}</a></li>
{% endif %}
{% if item.in_stock and item.part.trackable %}
{% if item.part.trackable %}
<li><a class='dropdown-item' href='#' id='stock-serialize' title='{% trans "Serialize stock" %}'><span class='fas fa-hashtag'></span> {% trans "Serialize stock" %}</a> </li>
{% endif %}
{% endif %}
{% if item.in_stock and item.can_adjust_location %}
<li><a class='dropdown-item' href='#' id='stock-move' title='{% trans "Transfer stock" %}'><span class='fas fa-exchange-alt icon-blue'></span> {% trans "Transfer stock" %}</a></li>
{% endif %}
{% if item.in_stock and item.can_adjust_location and item.part.salable and not item.customer %}
{% if item.part.salable and not item.customer %}
<li><a class='dropdown-item' href='#' id='stock-assign-to-customer' title='{% trans "Assign to customer" %}'><span class='fas fa-user-tie'></span> {% trans "Assign to customer" %}</a></li>
{% endif %}
{% if item.customer %}
@@ -100,13 +96,12 @@
{% endif %}
{% if item.belongs_to %}
<li><a class='dropdown-item' href='#' id='stock-uninstall' title='{% trans "Uninstall stock item" %}'><span class='fas fa-unlink'></span> {% trans "Uninstall" %}</a></li>
{% else %}
{% endif %}
{% if item.part.get_used_in %}
<!--
<li><a class='dropdown-item' href='#' id='stock-install-in' title='{% trans "Install stock item" %}'><span class='fas fa-link'></span> {% trans "Install" %}</a></li>
-->
{% endif %}
{% endif %}
</ul>
</div>
{% endif %}
@@ -618,37 +613,7 @@ $("#stock-convert").click(function() {
});
{% endif %}
{% if item.in_stock %}
$("#stock-assign-to-customer").click(function() {
inventreeGet('{% url "api-stock-detail" item.pk %}', {}, {
success: function(response) {
assignStockToCustomer(
[response],
{
success: function() {
location.reload();
},
}
);
}
});
});
$("#stock-move").click(function() {
itemAdjust("move");
});
$("#stock-count").click(function() {
itemAdjust('count');
});
$('#stock-remove').click(function() {
itemAdjust('take');
});
{% else %}
{% if item.customer %}
$("#stock-return-from-customer").click(function() {
constructForm('{% url "api-stock-item-return" item.pk %}', {
@@ -666,6 +631,37 @@ $("#stock-return-from-customer").click(function() {
});
});
{% else %}
$("#stock-assign-to-customer").click(function() {
inventreeGet('{% url "api-stock-detail" item.pk %}', {}, {
success: function(response) {
assignStockToCustomer(
[response],
{
success: function() {
location.reload();
},
}
);
}
});
});
{% endif %}
{% if item.can_adjust_location %}
$("#stock-move").click(function() {
itemAdjust("move");
});
$("#stock-count").click(function() {
itemAdjust('count');
});
$('#stock-remove').click(function() {
itemAdjust('take');
});
{% endif %}

View File

@@ -31,6 +31,57 @@ class StockListTest(StockViewTestCase):
self.assertEqual(response.status_code, 200)
class StockDetailTest(StockViewTestCase):
"""Unit test for the 'stock detail' page"""
def test_basic_info(self):
"""Test that basic stock item info is rendered"""
url = reverse('stock-item-detail', kwargs={'pk': 1})
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
html = str(response.content)
# Part name
self.assertIn('Stock Item: M2x4 LPHS', html)
# Quantity
self.assertIn('<h5>Available Quantity</h5>', html)
self.assertIn('<h5>4000 </h5>', html)
# Batch code
self.assertIn('Batch', html)
self.assertIn('<td>B123</td>', html)
# Actions to check
actions = [
"id=\\\'stock-count\\\' title=\\\'Count stock\\\'",
"id=\\\'stock-add\\\' title=\\\'Add stock\\\'",
"id=\\\'stock-remove\\\' title=\\\'Remove stock\\\'",
"id=\\\'stock-move\\\' title=\\\'Transfer stock\\\'",
"id=\\\'stock-duplicate\\\'",
"id=\\\'stock-edit\\\'",
"id=\\\'stock-delete\\\'",
]
# Initially we should not have any of the required permissions
for act in actions:
self.assertNotIn(act, html)
# Give the user all the permissions
self.assignRole('stock.add')
self.assignRole('stock.change')
self.assignRole('stock.delete')
response = self.client.get(url)
html = str(response.content)
for act in actions:
self.assertIn(act, html)
class StockOwnershipTest(StockViewTestCase):
"""Tests for stock ownership views."""

View File

@@ -44,6 +44,45 @@ class StockTest(InvenTreeTestCase):
Part.objects.rebuild()
StockItem.objects.rebuild()
def test_link(self):
"""Test the link URL field validation"""
item = StockItem.objects.get(pk=1)
# Check that invalid URLs fail
for bad_url in [
'test.com',
'httpx://abc.xyz',
'https:google.com',
]:
with self.assertRaises(ValidationError):
item.link = bad_url
item.save()
item.full_clean()
# Check that valid URLs pass
for good_url in [
'https://test.com',
'https://digikey.com/datasheets?file=1010101010101.bin',
'ftp://download.com:8080/file.aspx',
]:
item.link = good_url
item.save()
item.full_clean()
# A long URL should fail
long_url = 'https://website.co.uk?query=' + 'a' * 173
with self.assertRaises(ValidationError):
item.link = long_url
item.full_clean()
# Shorten by a single character, will pass
long_url = long_url[:-1]
item.link = long_url
item.save()
def test_expiry(self):
"""Test expiry date functionality for StockItem model."""
today = datetime.datetime.now().date()

View File

@@ -87,8 +87,16 @@ if __name__ == '__main__':
# GITHUB_REF may be either 'refs/heads/<branch>' or 'refs/heads/<tag>'
GITHUB_REF = os.environ['GITHUB_REF']
GITHUB_REF_NAME = os.environ['GITHUB_REF_NAME']
GITHUB_BASE_REF = os.environ['GITHUB_BASE_REF']
# Print out version information, makes debugging actions *much* easier!
print(f"GITHUB_REF: {GITHUB_REF}")
print(f"GITHUB_REF_NAME: {GITHUB_REF_NAME}")
print(f"GITHUB_REF_TYPE: {GITHUB_REF_TYPE}")
print(f"GITHUB_BASE_REF: {GITHUB_BASE_REF}")
version_file = os.path.join(here, '..', 'InvenTree', 'InvenTree', 'version.py')
version = None
@@ -109,8 +117,19 @@ if __name__ == '__main__':
print(f"InvenTree Version: '{version}'")
# Check version number and look for existing versions
# Note that on a 'tag' (release) we *must* allow duplicate versions, as this *is* the version that has just been released
highest_release = check_version_number(version, allow_duplicate=GITHUB_REF_TYPE == 'tag')
# If a release is found which matches the current tag, throw an error
allow_duplicate = False
# Note: on a 'tag' (release) we *must* allow duplicate versions, as this *is* the version that has just been released
if GITHUB_REF_TYPE == 'tag':
allow_duplicate = True
# Note: on a push to 'stable' branch we also allow duplicates
if GITHUB_BASE_REF == 'stable':
allow_duplicate = True
highest_release = check_version_number(version, allow_duplicate=allow_duplicate)
# Determine which docker tag we are going to use
docker_tags = None

View File

@@ -10,6 +10,10 @@ asgiref==3.5.2 \
# via
# -c requirements.txt
# django
build==0.8.0 \
--hash=sha256:19b0ed489f92ace6947698c3ca8436cb0556a66e2aa2d34cd70e2a5d27cd0437 \
--hash=sha256:887a6d471c901b1a6e6574ebaeeebb45e5269a79d095fe9a8f88d6614ed2e5f0
# via pip-tools
certifi==2022.6.15 \
--hash=sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d \
--hash=sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412
@@ -20,9 +24,9 @@ cfgv==3.3.1 \
--hash=sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426 \
--hash=sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736
# via pre-commit
charset-normalizer==2.0.12 \
--hash=sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597 \
--hash=sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df
charset-normalizer==2.1.0 \
--hash=sha256:5189b6f22b01957427f35b6a08d9a0bc45b46d3788ef5a92e978433c7a35f8a5 \
--hash=sha256:575e708016ff3a5e3681541cb9d79312c416835686d054a23accb873b254f413
# via
# -c requirements.txt
# requests
@@ -90,13 +94,13 @@ coveralls==2.1.2 \
--hash=sha256:4430b862baabb3cf090d36d84d331966615e4288d8a8c5957e0fd456d0dd8bd6 \
--hash=sha256:b3b60c17b03a0dee61952a91aed6f131e0b2ac8bd5da909389c53137811409e1
# via -r requirements-dev.in
distlib==0.3.4 \
--hash=sha256:6564fe0a8f51e734df6333d08b8b94d4ea8ee6b99b5ed50613f731fd4089f34b \
--hash=sha256:e4b58818180336dc9c529bfb9a0b58728ffc09ad92027a3f30b7cd91e3458579
distlib==0.3.5 \
--hash=sha256:a7f75737c70be3b25e2bee06288cec4e4c221de18455b2dd037fe2a795cab2fe \
--hash=sha256:b710088c59f06338ca514800ad795a132da19fda270e3ce4affc74abf955a26c
# via virtualenv
django==3.2.14 \
--hash=sha256:677182ba8b5b285a4e072f3ac17ceee6aff1b5ce77fd173cc5b6a2d3dc022fcf \
--hash=sha256:a8681e098fa60f7c33a4b628d6fcd3fe983a0939ff1301ecacac21d0b38bad56
django==3.2.15 \
--hash=sha256:115baf5049d5cf4163e43492cdc7139c306ed6d451e7d3571fe9612903903713 \
--hash=sha256:f71934b1a822f14a86c9ac9634053689279cd04ae69cb6ade4a59471b886582b
# via
# -c requirements.txt
# django-debug-toolbar
@@ -115,9 +119,9 @@ filelock==3.7.1 \
--hash=sha256:37def7b658813cda163b56fc564cdc75e86d338246458c4c28ae84cabefa2404 \
--hash=sha256:3a0fd85166ad9dbab54c9aec96737b744106dc5f15c0b09a6744a445299fcf04
# via virtualenv
flake8==4.0.1 \
--hash=sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d \
--hash=sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d
flake8==5.0.4 \
--hash=sha256:6fbe320aad8d6b95cec8b8e47bc933004678dc63095be98528b7bdd2a9f510db \
--hash=sha256:7a1cf6b73744f5806ab95e526f6f0d8c01c66d7bbe349562d22dfca20610b248
# via
# -r requirements-dev.in
# flake8-docstrings
@@ -126,9 +130,9 @@ flake8-docstrings==1.6.0 \
--hash=sha256:99cac583d6c7e32dd28bbfbef120a7c0d1b6dde4adb5a9fd441c4227a6534bde \
--hash=sha256:9fe7c6a306064af8e62a055c2f61e9eb1da55f84bb39caef2b84ce53708ac34b
# via -r requirements-dev.in
identify==2.5.1 \
--hash=sha256:0dca2ea3e4381c435ef9c33ba100a78a9b40c0bab11189c7cf121f75815efeaa \
--hash=sha256:3d11b16f3fe19f52039fb7e39c9c884b21cb1b586988114fbe42671f03de3e82
identify==2.5.3 \
--hash=sha256:25851c8c1370effb22aaa3c987b30449e9ff0cece408f810ae6ce408fdd20893 \
--hash=sha256:887e7b91a1be152b0d46bbf072130235a8117392b9f1828446079a816a05ef44
# via pre-commit
idna==3.3 \
--hash=sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff \
@@ -140,46 +144,54 @@ isort==5.10.1 \
--hash=sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7 \
--hash=sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951
# via -r requirements-dev.in
mccabe==0.6.1 \
--hash=sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42 \
--hash=sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f
mccabe==0.7.0 \
--hash=sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325 \
--hash=sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e
# via flake8
nodeenv==1.7.0 \
--hash=sha256:27083a7b96a25f2f5e1d8cb4b6317ee8aeda3bdd121394e5ac54e498028a042e \
--hash=sha256:e0e7f7dfb85fc5394c6fe1e8fa98131a2473e04311a45afb6508f7cf1836fa2b
# via pre-commit
pep517==0.12.0 \
--hash=sha256:931378d93d11b298cf511dd634cf5ea4cb249a28ef84160b3247ee9afb4e8ab0 \
--hash=sha256:dd884c326898e2c6e11f9e0b64940606a93eb10ea022a2e067959f3a110cf161
# via pip-tools
pep8-naming==0.13.0 \
--hash=sha256:069ea20e97f073b3e6d4f789af2a57816f281ca64b86210c7d471117a4b6bfd0 \
--hash=sha256:9f38e6dcf867a1fb7ad47f5ff72c0ddae544a6cf64eb9f7600b7b3c0bb5980b5
packaging==21.3 \
--hash=sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb \
--hash=sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522
# via build
pep517==0.13.0 \
--hash=sha256:4ba4446d80aed5b5eac6509ade100bff3e7943a8489de249654a5ae9b33ee35b \
--hash=sha256:ae69927c5c172be1add9203726d4b84cf3ebad1edcd5f71fcdc746e66e829f59
# via build
pep8-naming==0.13.1 \
--hash=sha256:3af77cdaa9c7965f7c85a56cd579354553c9bbd3fdf3078a776f12db54dd6944 \
--hash=sha256:f7867c1a464fe769be4f972ef7b79d6df1d9aff1b1f04ecf738d471963d3ab9c
# via -r requirements-dev.in
pip-tools==6.6.2 \
--hash=sha256:6b486548e5a139e30e4c4a225b3b7c2d46942a9f6d1a91143c21b1de4d02fd9b \
--hash=sha256:f638503a9f77d98d9a7d72584b1508d3f82ed019b8fab24f4e5ad078c1b8c95e
pip-tools==6.8.0 \
--hash=sha256:39e8aee465446e02278d80dbebd4325d1dd8633248f43213c73a25f58e7d8a55 \
--hash=sha256:3e5cd4acbf383d19bdfdeab04738b6313ebf4ad22ce49bf529c729061eabfab8
# via -r requirements-dev.in
platformdirs==2.5.2 \
--hash=sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788 \
--hash=sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19
# via virtualenv
pre-commit==2.19.0 \
--hash=sha256:10c62741aa5704faea2ad69cb550ca78082efe5697d6f04e5710c3c229afdd10 \
--hash=sha256:4233a1e38621c87d9dda9808c6606d7e7ba0e087cd56d3fe03202a01d2919615
pre-commit==2.20.0 \
--hash=sha256:51a5ba7c480ae8072ecdb6933df22d2f812dc897d5fe848778116129a681aac7 \
--hash=sha256:a978dac7bc9ec0bcee55c18a277d553b0f419d259dadb4b9418ff2d00eb43959
# via -r requirements-dev.in
pycodestyle==2.8.0 \
--hash=sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20 \
--hash=sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f
pycodestyle==2.9.1 \
--hash=sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785 \
--hash=sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b
# via flake8
pydocstyle==6.1.1 \
--hash=sha256:1d41b7c459ba0ee6c345f2eb9ae827cab14a7533a88c5c6f7e94923f72df92dc \
--hash=sha256:6987826d6775056839940041beef5c08cc7e3d71d63149b48e36727f70144dc4
# via flake8-docstrings
pyflakes==2.4.0 \
--hash=sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c \
--hash=sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e
pyflakes==2.5.0 \
--hash=sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2 \
--hash=sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3
# via flake8
pyparsing==3.0.9 \
--hash=sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb \
--hash=sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc
# via packaging
pytz==2022.1 \
--hash=sha256:1e760e2fe6a8163bc0b3d9a19c4f84342afa0a2affebfaa84b01b978a02ecaa7 \
--hash=sha256:e68985985296d9a66a881eb3193b0906246245294a881e7c8afe623866ac6a5c
@@ -223,18 +235,12 @@ pyyaml==6.0 \
# via
# -c requirements.txt
# pre-commit
requests==2.28.0 \
--hash=sha256:bc7861137fbce630f17b03d3ad02ad0bf978c844f3536d0edda6499dafce2b6f \
--hash=sha256:d568723a7ebd25875d8d1eaf5dfa068cd2fc8194b2e483d7b1f7c81918dbec6b
requests==2.28.1 \
--hash=sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983 \
--hash=sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349
# via
# -c requirements.txt
# coveralls
six==1.16.0 \
--hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \
--hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254
# via
# -c requirements.txt
# virtualenv
snowballstemmer==2.2.0 \
--hash=sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1 \
--hash=sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a
@@ -253,20 +259,22 @@ toml==0.10.2 \
tomli==2.0.1 \
--hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \
--hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f
# via pep517
typing-extensions==4.2.0 \
--hash=sha256:6657594ee297170d19f67d55c05852a874e7eb634f4f753dbd667855e07c1708 \
--hash=sha256:f1c24655a0da0d1b67f07e17a5e6b2a105894e6824b92096378bb3668ef02376
# via
# build
# pep517
typing-extensions==4.3.0 \
--hash=sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02 \
--hash=sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6
# via django-test-migrations
urllib3==1.26.9 \
--hash=sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14 \
--hash=sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e
urllib3==1.26.11 \
--hash=sha256:c33ccba33c819596124764c23a97d25f32b28433ba0dedeb77d873a38722c9bc \
--hash=sha256:ea6e8fb210b19d950fab93b60c9009226c63a28808bc8386e05301e25883ac0a
# via
# -c requirements.txt
# requests
virtualenv==20.15.0 \
--hash=sha256:4c44b1d77ca81f8368e2d7414f9b20c428ad16b343ac6d226206c5b84e2b4fcc \
--hash=sha256:804cce4de5b8a322f099897e308eecc8f6e2951f1a8e7e2b3598dff865f01336
virtualenv==20.16.3 \
--hash=sha256:4193b7bc8a6cd23e4eb251ac64f29b4398ab2c233531e66e40b19a6b7b0d30c1 \
--hash=sha256:d86ea0bb50e06252d79e6c241507cb904fcd66090c3271381372d6221a3970f9
# via pre-commit
wheel==0.37.1 \
--hash=sha256:4bdcd7d840138086126cd09254dc6195fb4fc6f01c050a1d7236f2630db1d22a \

View File

@@ -33,6 +33,7 @@ rapidfuzz==0.7.6 # Fuzzy string matching
sentry-sdk # Error reporting (optional)
setuptools # Standard depenedency
tablib[xls,xlsx,yaml] # Support for XLS and XLSX formats
weasyprint==54.3 # PDF generation
# Fixed sub-dependencies
py-moneyed<2.0 # For django-money # FIXED 2022-06-18 as we need `moneyed.localization`

View File

@@ -10,7 +10,7 @@ asgiref==3.5.2
# via django
babel==2.10.3
# via py-moneyed
bleach[css]==5.0.0
bleach[css]==5.0.1
# via django-markdownify
blessed==1.19.1
# via django-q
@@ -20,11 +20,11 @@ certifi==2022.6.15
# via
# requests
# sentry-sdk
cffi==1.15.0
cffi==1.15.1
# via
# cryptography
# weasyprint
charset-normalizer==2.0.12
charset-normalizer==2.1.0
# via requests
coreapi==2.3.3
# via -r requirements.in
@@ -42,7 +42,7 @@ defusedxml==0.7.1
# python3-openid
diff-match-patch==20200713
# via django-import-export
django==3.2.14
django==3.2.15
# via
# -r requirements.in
# django-allauth
@@ -121,7 +121,7 @@ djangorestframework==3.13.1
# via -r requirements.in
et-xmlfile==1.1.0
# via openpyxl
fonttools[woff]==4.33.3
fonttools[woff]==4.34.4
# via weasyprint
gunicorn==20.1.0
# via -r requirements.in
@@ -135,7 +135,7 @@ itypes==1.2.0
# via coreapi
jinja2==3.1.2
# via coreschema
markdown==3.3.7
markdown==3.4.1
# via django-markdownify
markuppy==1.14
# via tablib
@@ -149,7 +149,7 @@ openpyxl==3.0.10
# via tablib
pdf2image==1.16.0
# via -r requirements.in
pillow==9.1.1
pillow==9.2.0
# via
# -r requirements.in
# django-stdimage
@@ -194,14 +194,14 @@ redis==3.5.3
# via
# django-q
# django-redis
requests==2.28.0
requests==2.28.1
# via
# coreapi
# django-allauth
# requests-oauthlib
requests-oauthlib==1.3.1
# via django-allauth
sentry-sdk==1.6.0
sentry-sdk==1.9.0
# via -r requirements.in
six==1.16.0
# via
@@ -224,14 +224,16 @@ tinycss2==1.1.1
# weasyprint
uritemplate==4.1.1
# via coreapi
urllib3==1.26.9
urllib3==1.26.11
# via
# requests
# sentry-sdk
wcwidth==0.2.5
# via blessed
weasyprint==55.0
# via django-weasyprint
weasyprint==54.3
# via
# -r requirements.in
# django-weasyprint
webencodings==0.5.1
# via
# bleach
@@ -242,7 +244,7 @@ xlrd==2.0.1
# via tablib
xlwt==1.3.0
# via tablib
zipp==3.8.0
zipp==3.8.1
# via importlib-metadata
zopfli==0.2.1
# via fonttools

View File

@@ -264,7 +264,7 @@ def export_records(c, filename='data.json', overwrite=False, include_permissions
print(f"Exporting database records to file '{filename}'")
if filename.exists() and overwrite is False:
if Path(filename).is_file() and overwrite is False:
response = input("Warning: file already exists. Do you want to overwrite? [y/N]: ")
response = str(response).strip().lower()