Compare commits

..

30 Commits
0.4.0 ... 0.4.1

Author SHA1 Message Date
Oliver
0e59c15773 0.4.1 2021-07-30 11:26:53 +10:00
Oliver
eb883d7e70 Merge pull request #1884 from SchrodingersGat/typo-fix
logging.get -> logging.getLogger
2021-07-30 00:09:43 +10:00
Oliver Walters
d9f4c34a42 logging.get -> logging.getLogger 2021-07-29 23:44:52 +10:00
Oliver
3806e3ebeb Merge pull request #1880 from SchrodingersGat/settings-fix
Bug fix
2021-07-29 17:56:45 +10:00
Oliver
c39e3aaa82 Bug fix 2021-07-29 17:52:24 +10:00
Oliver
60e4022568 Merge pull request #1879 from SchrodingersGat/docker-improvements
Specify how many workers to use
2021-07-29 17:19:41 +10:00
Oliver
542c204ca0 PEP fixes 2021-07-29 16:39:51 +10:00
Oliver
dd12a593f4 Specify how many workers to use 2021-07-29 16:37:34 +10:00
Oliver
935ef968de Merge pull request #1878 from SchrodingersGat/js-template-stuff
Js template stuff
2021-07-29 13:42:47 +10:00
Oliver
7756c766c3 Fix stock.js 2021-07-29 12:35:21 +10:00
Oliver
4381a16b0e Template cleanup 2021-07-29 12:31:07 +10:00
Oliver
6fe5f0e0e6 Fixes for order.js 2021-07-29 11:58:32 +10:00
Oliver
ba5479090a Fix nav.js 2021-07-29 11:54:04 +10:00
Oliver
28bf5bfdbc Fix table_filters.js 2021-07-29 11:52:50 +10:00
Oliver
a222efda33 Fix rendering issues 2021-07-29 11:43:50 +10:00
Oliver
27ec65a002 Add 'settings.js' which provides all settings (global and user) as a dynamic javascript file
- Minimal database hits required
2021-07-29 11:28:04 +10:00
Oliver
915756eacf Improve test output 2021-07-29 09:28:08 +10:00
Oliver
8e97d14f1f Rename CI test 2021-07-29 09:26:56 +10:00
Oliver
14aebfdae1 Split dynamic javascript files into two separate directories
- One gets translated and is served statically
- One does not get translated and is served dynamically
- Add CI step
2021-07-29 09:23:24 +10:00
Oliver
bc3c3be751 force linux-style line endings for .sh files 2021-07-29 09:10:46 +10:00
Oliver
0a73032950 Merge pull request #1877 from eeintech/fix_search_js
Fixed missing comma propagating to translated JS files

(cherry picked from commit 2009773d9d)
2021-07-29 08:27:49 +10:00
Oliver
2009773d9d Merge pull request #1877 from eeintech/fix_search_js
Fixed missing comma propagating to translated JS files
2021-07-29 08:27:20 +10:00
eeintech
d43312d162 Missing comma propagating to translated JS files 2021-07-28 13:29:12 -04:00
Oliver
430f737953 Merge pull request #1876 from SchrodingersGat/mpn-api
Adds an API filter class for the ManufacturerPart list endpoint
2021-07-29 00:23:44 +10:00
Oliver
baa6283d20 Fixes 2021-07-28 23:47:50 +10:00
Oliver
5744796506 Adds an API filter class for the ManufacturerPart list endpoint 2021-07-28 23:32:49 +10:00
Oliver
a7229b5b0b Merge pull request #1874 from SchrodingersGat/docker-dev-fix
Copy static files when starting dev server

(cherry picked from commit 50eb70f538)
2021-07-28 22:50:31 +10:00
Oliver
50eb70f538 Merge pull request #1874 from SchrodingersGat/docker-dev-fix
Copy static files when starting dev server
2021-07-28 22:50:03 +10:00
Oliver
399e44fce7 Copy static files when starting dev server 2021-07-28 22:30:41 +10:00
Oliver
20b6e0fd1a Update version.py 2021-07-28 15:46:52 +10:00
43 changed files with 438 additions and 152 deletions

2
.gitattributes vendored
View File

@@ -7,5 +7,5 @@
*.yml text
*.yaml text
*.conf text
*.sh text
*.sh text eol=lf
*.js text

28
.github/workflows/javascript.yaml vendored Normal file
View File

@@ -0,0 +1,28 @@
# Check javascript template files
name: Javascript Templates
on:
push:
branches:
- master
pull_request:
branches-ignore:
- l10*
jobs:
javascript:
runs-on: ubuntu-latest
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- name: Checkout Code
uses: actions/checkout@v2
- name: Check Files
run: |
cd ci
python check_js_templates.py

View File

@@ -12,6 +12,7 @@ database setup in this file.
"""
import logging
import os
import random
import string
@@ -202,7 +203,7 @@ STATICFILES_DIRS = [
# Translated Template settings
STATICFILES_I18_PREFIX = 'i18n'
STATICFILES_I18_SRC = os.path.join(BASE_DIR, 'templates', 'js')
STATICFILES_I18_SRC = os.path.join(BASE_DIR, 'templates', 'js', 'translated')
STATICFILES_I18_TRG = STATICFILES_DIRS[0] + '_' + STATICFILES_I18_PREFIX
STATICFILES_DIRS.append(STATICFILES_I18_TRG)
STATICFILES_I18_TRG = os.path.join(STATICFILES_I18_TRG, STATICFILES_I18_PREFIX)
@@ -347,10 +348,22 @@ REST_FRAMEWORK = {
WSGI_APPLICATION = 'InvenTree.wsgi.application'
background_workers = os.environ.get('INVENTREE_BACKGROUND_WORKERS', None)
if background_workers is not None:
try:
background_workers = int(background_workers)
except ValueError:
background_workers = None
if background_workers is None:
# Sensible default?
background_workers = 4
# django-q configuration
Q_CLUSTER = {
'name': 'InvenTree',
'workers': 4,
'workers': background_workers,
'timeout': 90,
'retry': 120,
'queue_limit': 50,

View File

@@ -93,28 +93,33 @@ settings_urls = [
url(r'^.*$', SettingsView.as_view(template_name='InvenTree/settings/settings.html'), name='settings'),
]
# Some javascript files are served 'dynamically', allowing them to pass through the Django translation layer
# These javascript files are served "dynamically" - i.e. rendered on demand
dynamic_javascript_urls = [
url(r'^api.js', DynamicJsView.as_view(template_name='js/api.js'), name='api.js'),
url(r'^attachment.js', DynamicJsView.as_view(template_name='js/attachment.js'), name='attachment.js'),
url(r'^barcode.js', DynamicJsView.as_view(template_name='js/barcode.js'), name='barcode.js'),
url(r'^bom.js', DynamicJsView.as_view(template_name='js/bom.js'), name='bom.js'),
url(r'^build.js', DynamicJsView.as_view(template_name='js/build.js'), name='build.js'),
url(r'^calendar.js', DynamicJsView.as_view(template_name='js/calendar.js'), name='calendar.js'),
url(r'^company.js', DynamicJsView.as_view(template_name='js/company.js'), name='company.js'),
url(r'^filters.js', DynamicJsView.as_view(template_name='js/filters.js'), name='filters.js'),
url(r'^forms.js', DynamicJsView.as_view(template_name='js/forms.js'), name='forms.js'),
url(r'^inventree.js', DynamicJsView.as_view(template_name='js/inventree.js'), name='inventree.js'),
url(r'^label.js', DynamicJsView.as_view(template_name='js/label.js'), name='label.js'),
url(r'^model_renderers.js', DynamicJsView.as_view(template_name='js/model_renderers.js'), name='model_renderers.js'),
url(r'^modals.js', DynamicJsView.as_view(template_name='js/modals.js'), name='modals.js'),
url(r'^nav.js', DynamicJsView.as_view(template_name='js/nav.js'), name='nav.js'),
url(r'^order.js', DynamicJsView.as_view(template_name='js/order.js'), name='order.js'),
url(r'^part.js', DynamicJsView.as_view(template_name='js/part.js'), name='part.js'),
url(r'^report.js', DynamicJsView.as_view(template_name='js/report.js'), name='report.js'),
url(r'^stock.js', DynamicJsView.as_view(template_name='js/stock.js'), name='stock.js'),
url(r'^tables.js', DynamicJsView.as_view(template_name='js/tables.js'), name='tables.js'),
url(r'^table_filters.js', DynamicJsView.as_view(template_name='js/table_filters.js'), name='table_filters.js'),
url(r'^inventree.js', DynamicJsView.as_view(template_name='js/dynamic/inventree.js'), name='inventree.js'),
url(r'^calendar.js', DynamicJsView.as_view(template_name='js/dynamic/calendar.js'), name='calendar.js'),
url(r'^nav.js', DynamicJsView.as_view(template_name='js/dynamic/nav.js'), name='nav.js'),
url(r'^settings.js', DynamicJsView.as_view(template_name='js/dynamic/settings.js'), name='settings.js'),
]
# These javascript files are pased through the Django translation layer
translated_javascript_urls = [
url(r'^api.js', DynamicJsView.as_view(template_name='js/translated/api.js'), name='api.js'),
url(r'^attachment.js', DynamicJsView.as_view(template_name='js/translated/attachment.js'), name='attachment.js'),
url(r'^barcode.js', DynamicJsView.as_view(template_name='js/translated/barcode.js'), name='barcode.js'),
url(r'^bom.js', DynamicJsView.as_view(template_name='js/translated/bom.js'), name='bom.js'),
url(r'^build.js', DynamicJsView.as_view(template_name='js/translated/build.js'), name='build.js'),
url(r'^company.js', DynamicJsView.as_view(template_name='js/translated/company.js'), name='company.js'),
url(r'^filters.js', DynamicJsView.as_view(template_name='js/translated/filters.js'), name='filters.js'),
url(r'^forms.js', DynamicJsView.as_view(template_name='js/translated/forms.js'), name='forms.js'),
url(r'^label.js', DynamicJsView.as_view(template_name='js/translated/label.js'), name='label.js'),
url(r'^model_renderers.js', DynamicJsView.as_view(template_name='js/translated/model_renderers.js'), name='model_renderers.js'),
url(r'^modals.js', DynamicJsView.as_view(template_name='js/translated/modals.js'), name='modals.js'),
url(r'^order.js', DynamicJsView.as_view(template_name='js/translated/order.js'), name='order.js'),
url(r'^part.js', DynamicJsView.as_view(template_name='js/translated/part.js'), name='part.js'),
url(r'^report.js', DynamicJsView.as_view(template_name='js/translated/report.js'), name='report.js'),
url(r'^stock.js', DynamicJsView.as_view(template_name='js/translated/stock.js'), name='stock.js'),
url(r'^tables.js', DynamicJsView.as_view(template_name='js/translated/tables.js'), name='tables.js'),
url(r'^table_filters.js', DynamicJsView.as_view(template_name='js/translated/table_filters.js'), name='table_filters.js'),
]
urlpatterns = [
@@ -123,7 +128,8 @@ urlpatterns = [
url(r'^supplier-part/', include(supplier_part_urls)),
# "Dynamic" javascript files which are rendered using InvenTree templating.
url(r'^dynamic/', include(dynamic_javascript_urls)),
url(r'^js/dynamic/', include(dynamic_javascript_urls)),
url(r'^js/i18n/', include(translated_javascript_urls)),
url(r'^common/', include(common_urls)),

View File

@@ -8,7 +8,7 @@ import re
import common.models
INVENTREE_SW_VERSION = "0.4.0"
INVENTREE_SW_VERSION = "0.4.1"
INVENTREE_API_VERSION = 8

View File

@@ -82,7 +82,7 @@
},
{
success: function(response) {
var prefix = '{% settings_value "BUILDORDER_REFERENCE_PREFIX" %}';
var prefix = global_settings.BUILDORDER_REFERENCE_PREFIX;
for (var idx = 0; idx < response.length; idx++) {

View File

@@ -38,6 +38,67 @@ class BaseInvenTreeSetting(models.Model):
class Meta:
abstract = True
@classmethod
def allValues(cls, user=None):
"""
Return a dict of "all" defined global settings.
This performs a single database lookup,
and then any settings which are not *in* the database
are assigned their default values
"""
keys = set()
settings = []
results = cls.objects.all()
if user is not None:
results = results.filter(user=user)
# Query the database
for setting in results:
settings.append({
"key": setting.key.upper(),
"value": setting.value
})
keys.add(setting.key.upper())
# Specify any "default" values which are not in the database
for key in cls.GLOBAL_SETTINGS.keys():
if key.upper() not in keys:
settings.append({
"key": key.upper(),
"value": cls.get_setting_default(key)
})
# Enforce javascript formatting
for idx, setting in enumerate(settings):
key = setting['key']
value = setting['value']
validator = cls.get_setting_validator(key)
# Convert to javascript compatible booleans
if cls.validator_is_bool(validator):
value = str(value).lower()
# Numerical values remain the same
elif cls.validator_is_int(validator):
pass
# Wrap strings with quotes
else:
value = f"'{value}'"
setting["value"] = value
return settings
@classmethod
def get_setting_name(cls, key):
"""
@@ -368,13 +429,7 @@ class BaseInvenTreeSetting(models.Model):
validator = self.__class__.get_setting_validator(self.key)
if validator == bool:
return True
if type(validator) in [list, tuple]:
for v in validator:
if v == bool:
return True
return self.__class__.validator_is_bool(validator)
def as_bool(self):
"""
@@ -385,6 +440,19 @@ class BaseInvenTreeSetting(models.Model):
return InvenTree.helpers.str2bool(self.value)
@classmethod
def validator_is_bool(cls, validator):
if validator == bool:
return True
if type(validator) in [list, tuple]:
for v in validator:
if v == bool:
return True
return False
def is_int(self):
"""
Check if the setting is required to be an integer value:
@@ -392,6 +460,11 @@ class BaseInvenTreeSetting(models.Model):
validator = self.__class__.get_setting_validator(self.key)
return self.__class__.validator_is_int(validator)
@classmethod
def validator_is_int(cls, validator):
if validator == int:
return True

View File

@@ -6,6 +6,8 @@ Provides a JSON API for the Company app
from __future__ import unicode_literals
from django_filters.rest_framework import DjangoFilterBackend
from django_filters import rest_framework as rest_filters
from rest_framework import filters
from rest_framework import generics
@@ -84,6 +86,23 @@ class CompanyDetail(generics.RetrieveUpdateDestroyAPIView):
return queryset
class ManufacturerPartFilter(rest_filters.FilterSet):
"""
Custom API filters for the ManufacturerPart list endpoint.
"""
class Meta:
model = ManufacturerPart
fields = [
'manufacturer',
'MPN',
'part',
]
# Filter by 'active' status of linked part
active = rest_filters.BooleanFilter(field_name='part__active')
class ManufacturerPartList(generics.ListCreateAPIView):
""" API endpoint for list view of ManufacturerPart object
@@ -98,6 +117,7 @@ class ManufacturerPartList(generics.ListCreateAPIView):
)
serializer_class = ManufacturerPartSerializer
filterset_class = ManufacturerPartFilter
def get_serializer(self, *args, **kwargs):
@@ -115,45 +135,12 @@ class ManufacturerPartList(generics.ListCreateAPIView):
return self.serializer_class(*args, **kwargs)
def filter_queryset(self, queryset):
"""
Custom filtering for the queryset.
"""
queryset = super().filter_queryset(queryset)
params = self.request.query_params
# Filter by manufacturer
manufacturer = params.get('manufacturer', None)
if manufacturer is not None:
queryset = queryset.filter(manufacturer=manufacturer)
# Filter by parent part?
part = params.get('part', None)
if part is not None:
queryset = queryset.filter(part=part)
# Filter by 'active' status of the part?
active = params.get('active', None)
if active is not None:
active = str2bool(active)
queryset = queryset.filter(part__active=active)
return queryset
filter_backends = [
DjangoFilterBackend,
filters.SearchFilter,
filters.OrderingFilter,
]
filter_fields = [
]
search_fields = [
'manufacturer__name',
'description',

View File

@@ -198,17 +198,16 @@
);
});
{% settings_value "INVENTREE_DOWNLOAD_FROM_URL" as allow_download %}
if (global_settings.INVENTREE_DOWNLOAD_FROM_URL) {
{% if allow_download %}
$('#company-image-url').click(function() {
launchModalForm(
'{% url "company-image-download" company.id %}',
{
reload: true,
}
)
});
{% endif %}
$('#company-image-url').click(function() {
launchModalForm(
'{% url "company-image-download" company.id %}',
{
reload: true,
}
)
});
}
{% endblock %}

View File

@@ -164,7 +164,7 @@ $("#edit-order").click(function() {
constructForm('{% url "api-po-detail" order.pk %}', {
fields: {
reference: {
prefix: "{% settings_value 'PURCHASEORDER_REFERENCE_PREFIX' %}",
prefix: global_settings.PURCHASEORDER_REFERENCE_PREFIX,
},
{% if order.lines.count == 0 and order.status == PurchaseOrderStatus.PENDING %}
supplier: {

View File

@@ -66,7 +66,7 @@
},
{
success: function(response) {
var prefix = '{% settings_value "PURCHASEORDER_REFERENCE_PREFIX" %}';
var prefix = global_settings.PURCHASEORDER_REFERENCE_PREFIX;
for (var idx = 0; idx < response.length; idx++) {

View File

@@ -157,7 +157,7 @@ $("#edit-order").click(function() {
constructForm('{% url "api-so-detail" order.pk %}', {
fields: {
reference: {
prefix: "{% settings_value 'SALESORDER_REFERENCE_PREFIX' %}",
prefix: global_settings.SALESORDER_REFERENCE_PREFIX,
},
{% if order.lines.count == 0 and order.status == SalesOrderStatus.PENDING %}
customer: {

View File

@@ -67,7 +67,7 @@
{
success: function(response) {
var prefix = '{% settings_value "SALESORDER_REFERENCE_PREFIX" %}';
var prefix = global_settings.SALESORDER_REFERENCE_PREFIX;
for (var idx = 0; idx < response.length; idx++) {
var order = response[idx];

View File

@@ -394,17 +394,16 @@
{% if roles.part.change %}
{% settings_value "INVENTREE_DOWNLOAD_FROM_URL" as allow_download %}
{% if allow_download %}
$("#part-image-url").click(function() {
launchModalForm(
'{% url "part-image-download" part.id %}',
{
reload: true,
}
);
});
{% endif %}
if (global_settings.INVENTREE_DOWNLOAD_FROM_URL) {
$("#part-image-url").click(function() {
launchModalForm(
'{% url "part-image-download" part.id %}',
{
reload: true,
}
);
});
}
$("#part-image-select").click(function() {
launchModalForm("{% url 'part-image-select' part.id %}",

View File

@@ -207,6 +207,24 @@ def settings_value(key, *args, **kwargs):
return InvenTreeSetting.get_setting(key)
@register.simple_tag()
def user_settings(user, *args, **kwargs):
"""
Return all USER settings as a key:value dict
"""
return InvenTreeUserSetting.allValues(user=user)
@register.simple_tag()
def global_settings(*args, **kwargs):
"""
Return all GLOBAL InvenTree settings as a key:value dict
"""
return InvenTreeSetting.allValues()
@register.simple_tag()
def get_color_theme_css(username):
try:

View File

@@ -145,19 +145,22 @@
<script type='text/javascript' src="{% static 'script/inventree/notification.js' %}"></script>
<script type='text/javascript' src="{% static 'script/inventree/sidenav.js' %}"></script>
<!-- translated -->
<script type='text/javascript' src="{% i18n_static 'inventree.js' %}"></script>
<!-- dynamic javascript templates -->
<script type='text/javascript' src="{% url 'inventree.js' %}"></script>
<script type='text/javascript' src="{% url 'calendar.js' %}"></script>
<script type='text/javascript' src="{% url 'nav.js' %}"></script>
<script type='text/javascript' src="{% url 'settings.js' %}"></script>
<!-- translated javascript templates-->
<script type='text/javascript' src="{% i18n_static 'api.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'attachment.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'barcode.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'bom.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'build.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'calendar.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'company.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'filters.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'forms.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'label.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'nav.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'modals.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'model_renderers.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'order.js' %}"></script>

View File

@@ -91,11 +91,7 @@ function inventreeDocReady() {
url: '/api/part/',
data: {
search: request.term,
{% if request.user %}
limit: {% settings_value 'SEARCH_PREVIEW_RESULTS' user=request.user %},
{% else %}
limit: 25
{% endif %}
limit: user_settings.SEARCH_PREVIEW_RESULTS,
offset: 0
},
success: function (data) {

View File

@@ -0,0 +1,17 @@
{% load inventree_extras %}
// InvenTree settings
{% user_settings request.user as USER_SETTINGS %}
{% global_settings as GLOBAL_SETTINGS %}
var user_settings = {
{% for setting in USER_SETTINGS %}
{{ setting.key }}: {{ setting.value|safe }},
{% endfor %}
};
var global_settings = {
{% for setting in GLOBAL_SETTINGS %}
{{ setting.key }}: {{ setting.value|safe }},
{% endfor %}
};

View File

@@ -5,7 +5,7 @@
function buildFormFields() {
return {
reference: {
prefix: "{% settings_value 'BUILDORDER_REFERENCE_PREFIX' %}",
prefix: global_settings.BUILDORDER_REFERENCE_PREFIX,
},
title: {},
part: {},
@@ -232,7 +232,7 @@ function loadBuildOrderAllocationTable(table, options={}) {
switchable: false,
title: '{% trans "Build Order" %}',
formatter: function(value, row) {
var prefix = "{% settings_value 'BUILDORDER_REFERENCE_PREFIX' %}";
var prefix = global_settings.BUILDORDER_REFERENCE_PREFIX;
var ref = `${prefix}${row.build_detail.reference}`;
@@ -848,7 +848,7 @@ function loadBuildTable(table, options) {
switchable: true,
formatter: function(value, row, index, field) {
var prefix = "{% settings_value 'BUILDORDER_REFERENCE_PREFIX' %}";
var prefix = global_settings.BUILDORDER_REFERENCE_PREFIX;
if (prefix) {
value = `${prefix}${value}`;

View File

@@ -9,7 +9,7 @@ function createSalesOrder(options={}) {
method: 'POST',
fields: {
reference: {
prefix: '{% settings_value "SALESORDER_REFERENCE_PREFIX" %}',
prefix: global_settings.SALESORDER_REFERENCE_PREFIX,
},
customer: {
value: options.customer,
@@ -40,7 +40,7 @@ function createPurchaseOrder(options={}) {
method: 'POST',
fields: {
reference: {
prefix: "{% settings_value 'PURCHASEORDER_REFERENCE_PREFIX' %}",
prefix: global_settings.PURCHASEORDER_REFERENCE_PREFIX,
},
supplier: {
value: options.supplier,
@@ -214,7 +214,7 @@ function loadPurchaseOrderTable(table, options) {
switchable: false,
formatter: function(value, row, index, field) {
var prefix = "{% settings_value 'PURCHASEORDER_REFERENCE_PREFIX' %}";
var prefix = global_settings.PURCHASEORDER_REFERENCE_PREFIX;
if (prefix) {
value = `${prefix}${value}`;
@@ -309,7 +309,7 @@ function loadSalesOrderTable(table, options) {
title: '{% trans "Sales Order" %}',
formatter: function(value, row, index, field) {
var prefix = "{% settings_value 'SALESORDER_REFERENCE_PREFIX' %}";
var prefix = global_settings.SALESORDER_REFERENCE_PREFIX;
if (prefix) {
value = `${prefix}${value}`;
@@ -423,7 +423,7 @@ function loadSalesOrderAllocationTable(table, options={}) {
switchable: false,
formatter: function(value, row) {
var prefix = "{% settings_value 'SALESORDER_REFERENCE_PREFIX' %}";
var prefix = global_settings.SALESORDER_REFERENCE_PREFIX;
var ref = `${prefix}${row.order_detail.reference}`;

View File

@@ -6,8 +6,6 @@
* Requires api.js to be loaded first
*/
{% settings_value 'BARCODE_ENABLE' as barcodes %}
function stockStatusCodes() {
return [
{% for code in StockStatus.list %}
@@ -704,8 +702,7 @@ function loadStockTable(table, options) {
name: 'stock',
original: original,
showColumns: true,
{% settings_value 'STOCK_GROUP_BY_PART' as group_by_part %}
{% if group_by_part %}
{% if False %}
groupByField: options.groupByField || 'part',
groupBy: grouping,
groupByFormatter: function(field, id, data) {
@@ -1011,14 +1008,13 @@ function loadStockTable(table, options) {
title: '{% trans "Stocktake" %}',
sortable: true,
},
{% settings_value "STOCK_ENABLE_EXPIRY" as expiry %}
{% if expiry %}
{
field: 'expiry_date',
title: '{% trans "Expiry Date" %}',
sortable: true,
visible: global_settings.STOCK_ENABLE_EXPIRY,
switchable: global_settings.STOCK_ENABLE_EXPIRY,
},
{% endif %}
{
field: 'updated',
title: '{% trans "Last Updated" %}',
@@ -1037,7 +1033,7 @@ function loadStockTable(table, options) {
if (row.purchase_order_reference) {
var prefix = '{% settings_value "PURCHASEORDER_REFERENCE_PREFIX" %}';
var prefix = global_settings.PURCHASEORDER_REFERENCE_PREFIX;
text = prefix + row.purchase_order_reference;
}
@@ -1090,15 +1086,18 @@ function loadStockTable(table, options) {
}
*/
var buttons = [
'#stock-print-options',
'#stock-options',
];
if (global_settings.BARCODE_ENABLE) {
buttons.push('#stock-barcode-options');
}
linkButtonsToSelection(
table,
[
'#stock-print-options',
{% if barcodes %}
'#stock-barcode-options',
{% endif %}
'#stock-options',
]
buttons,
);
@@ -1138,19 +1137,19 @@ function loadStockTable(table, options) {
printTestReports(items);
})
{% if barcodes %}
$('#multi-item-barcode-scan-into-location').click(function() {
var selections = $('#stock-table').bootstrapTable('getSelections');
if (global_settings.BARCODE_ENABLE) {
$('#multi-item-barcode-scan-into-location').click(function() {
var selections = $('#stock-table').bootstrapTable('getSelections');
var items = [];
var items = [];
selections.forEach(function(item) {
items.push(item.pk);
})
selections.forEach(function(item) {
items.push(item.pk);
})
scanItemsIntoLocation(items);
});
{% endif %}
scanItemsIntoLocation(items);
});
}
$('#multi-item-stocktake').click(function() {
stockAdjustment('count');

View File

@@ -121,7 +121,8 @@ function getAvailableTableFilters(tableKey) {
// Filters for the "Stock" table
if (tableKey == 'stock') {
return {
var filters = {
active: {
type: 'bool',
title: '{% trans "Active parts" %}',
@@ -147,19 +148,6 @@ function getAvailableTableFilters(tableKey) {
title: '{% trans "Depleted" %}',
description: '{% trans "Show stock items which are depleted" %}',
},
{% settings_value "STOCK_ENABLE_EXPIRY" as expiry %}
{% if expiry %}
expired: {
type: 'bool',
title: '{% trans "Expired" %}',
description: '{% trans "Show stock items which have expired" %}',
},
stale: {
type: 'bool',
title: '{% trans "Stale" %}',
description: '{% trans "Show stock which is close to expiring" %}',
},
{% endif %}
in_stock: {
type: 'bool',
title: '{% trans "In Stock" %}',
@@ -216,6 +204,23 @@ function getAvailableTableFilters(tableKey) {
description: '{% trans "Show stock items which have a purchase price set" %}',
},
};
// Optional filters if stock expiry functionality is enabled
if (global_settings.STOCK_ENABLE_EXPIRY) {
filters.expired = {
type: 'bool',
title: '{% trans "Expired" %}',
description: '{% trans "Show stock items which have expired" %}',
};
filters.stale = {
type: 'bool',
title: '{% trans "Stale" %}',
description: '{% trans "Show stock which is close to expiring" %}',
};
}
return filters;
}
// Filters for the 'stock test' table

121
ci/check_js_templates.py Normal file
View File

@@ -0,0 +1,121 @@
"""
Test that the "translated" javascript files to not contain template tags
which need to be determined at "run time".
This is because the "translated" javascript files are compiled into the "static" directory.
They should only contain template tags that render static information.
"""
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import sys
import re
import os
import pathlib
here = os.path.abspath(os.path.dirname(__file__))
template_dir = os.path.abspath(os.path.join(here, '..', 'InvenTree', 'templates'))
# We only care about the 'translated' files
js_i18n_dir = os.path.join(template_dir, 'js', 'translated')
js_dynamic_dir = os.path.join(template_dir, 'js', 'dynamic')
errors = 0
print("=================================")
print("Checking static javascript files:")
print("=================================")
def check_invalid_tag(data):
pattern = r"{%(\w+)"
err_count = 0
for idx, line in enumerate(data):
results = re.findall(pattern, line)
for result in results:
err_count += 1
print(f" - Error on line {idx+1}: %{{{result[0]}")
return err_count
def check_prohibited_tags(data):
allowed_tags = [
'if',
'elif',
'else',
'endif',
'for',
'endfor',
'trans',
'load',
'include',
'url',
]
pattern = r"{% (\w+)\s"
err_count = 0
has_trans = False
for idx, line in enumerate(data):
for tag in re.findall(pattern, line):
if tag not in allowed_tags:
print(f" > Line {idx+1} contains prohibited template tag '{tag}'")
err_count += 1
if tag == 'trans':
has_trans = True
if not has_trans:
print(f" > file is missing 'trans' tags")
err_count += 1
return err_count
for filename in pathlib.Path(js_i18n_dir).rglob('*.js'):
print(f"Checking file 'translated/{os.path.basename(filename)}':")
with open(filename, 'r') as js_file:
data = js_file.readlines()
errors += check_invalid_tag(data)
errors += check_prohibited_tags(data)
for filename in pathlib.Path(js_dynamic_dir).rglob('*.js'):
print(f"Checking file 'dynamic/{os.path.basename(filename)}':")
# Check that the 'dynamic' files do not contains any translated strings
with open(filename, 'r') as js_file:
data = js_file.readlines()
pattern = r'{% trans '
err_count = 0
for idx, line in enumerate(data):
results = re.findall(pattern, line)
if len(results) > 0:
errors += 1
print(f" > prohibited {{% trans %}} tag found at line {idx + 1}")
if errors > 0:
print(f"Found {errors} incorrect template tags")
sys.exit(errors)

View File

@@ -27,6 +27,10 @@ ENV INVENTREE_MEDIA_ROOT="${INVENTREE_DATA_DIR}/media"
ENV INVENTREE_CONFIG_FILE="${INVENTREE_DATA_DIR}/config.yaml"
ENV INVENTREE_SECRET_KEY_FILE="${INVENTREE_DATA_DIR}/secret_key.txt"
# Worker configuration (can be altered by user)
ENV INVENTREE_GUNICORN_WORKERS="4"
ENV INVENTREE_BACKGROUND_WORKERS="4"
# Default web server port is 8000
ENV INVENTREE_WEB_PORT="8000"

View File

@@ -1,6 +1,22 @@
import multiprocessing
import os
import logging
workers = multiprocessing.cpu_count() * 2 + 1
logger = logging.getLogger('inventree')
workers = os.environ.get('INVENTREE_GUNICORN_WORKERS', None)
if workers is not None:
try:
workers = int(workers)
except ValueError:
workers = None
if workers is None:
workers = multiprocessing.cpu_count() * 2 + 1
logger.info(f"Starting gunicorn server with {workers} workers")
max_requests = 1000
max_requests_jitter = 50

View File

@@ -45,5 +45,7 @@ python3 manage.py migrate --noinput || exit 1
python3 manage.py migrate --run-syncdb || exit 1
python3 manage.py clearsessions || exit 1
invoke static
# Launch a development server
python3 manage.py runserver ${INVENTREE_WEB_ADDR}:${INVENTREE_WEB_PORT}

View File

@@ -39,4 +39,4 @@ python3 manage.py collectstatic --noinput || exit 1
python3 manage.py clearsessions || exit 1
# Now we can launch the server
gunicorn -c $INVENTREE_HOME/gunicorn.conf.py InvenTree.wsgi -b 0.0.0.0:$INVENTREE_WEB_PORT
gunicorn -c $INVENTREE_HOME/gunicorn.conf.py InvenTree.wsgi -b 0.0.0.0:$INVENTREE_WEB_PORT

View File

@@ -11,4 +11,4 @@ python3 manage.py wait_for_db
sleep 10
# Now we can launch the background worker process
python3 manage.py qcluster
python3 manage.py qcluster