diff --git a/.eslintrc.yml b/.eslintrc.yml
new file mode 100644
index 0000000000..43363404b3
--- /dev/null
+++ b/.eslintrc.yml
@@ -0,0 +1,25 @@
+env:
+ commonjs: false
+ browser: true
+ es2021: true
+ jquery: true
+extends:
+ - google
+parserOptions:
+ ecmaVersion: 12
+rules:
+ no-var: off
+ guard-for-in: off
+ no-trailing-spaces: off
+ camelcase: off
+ padded-blocks: off
+ prefer-const: off
+ max-len: off
+ require-jsdoc: off
+ valid-jsdoc: off
+ no-multiple-empty-lines: off
+ comma-dangle: off
+ prefer-spread: off
+ indent:
+ - error
+ - 4
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 0000000000..1a75b97af0
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,31 @@
+---
+name: Bug report
+about: Create a bug report to help us improve InvenTree
+title: "[BUG] Enter bug description"
+labels: bug, question
+assignees: ''
+
+---
+
+**Describe the bug**
+A clear and concise description of what the bug is.
+
+**To Reproduce**
+Steps to reproduce the behavior:
+1. Go to '...'
+2. Click on '....'
+3. Scroll down to '....'
+4. See error
+
+**Expected behavior**
+A clear and concise description of what you expected to happen.
+
+**Screenshots**
+If applicable, add screenshots to help explain your problem.
+
+**Deployment Method**
+Docker
+Bare Metal
+
+**Version Information**
+You can get this by going to the "About InvenTree" section in the upper right corner and cicking on to the "copy version information"
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 0000000000..ca9ff88a58
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,26 @@
+---
+name: Feature request
+about: Suggest an idea for this project
+title: "[FR]"
+labels: enhancement
+assignees: ''
+
+---
+
+**Is your feature request the result of a bug?**
+Please link it here.
+
+**Problem**
+A clear and concise description of what the problem is. e.g. I'm always frustrated when [...]
+
+**Suggested solution**
+A clear and concise description of what you want to happen.
+
+**Describe alternatives you've considered**
+A clear and concise description of any alternative solutions or features you've considered.
+
+**Examples of other systems**
+Show how other software handles your FR if you have examples.
+
+**Do you want to develop this?**
+If so please describe briefly how you would like to implement it (so we can give advice) and if you have experience in the needed technology (you do not need to be a pro - this is just as a information for us).
diff --git a/.github/workflows/html.yaml b/.github/workflows/html.yaml
new file mode 100644
index 0000000000..069da7cbb4
--- /dev/null
+++ b/.github/workflows/html.yaml
@@ -0,0 +1,54 @@
+# Check javascript template files
+
+name: HTML Templates
+
+on:
+ push:
+ branches:
+ - master
+
+ pull_request:
+ branches-ignore:
+ - l10*
+
+jobs:
+
+ html:
+ runs-on: ubuntu-latest
+
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ INVENTREE_DB_ENGINE: sqlite3
+ INVENTREE_DB_NAME: inventree
+ INVENTREE_MEDIA_ROOT: ./media
+ INVENTREE_STATIC_ROOT: ./static
+ steps:
+ - name: Install node.js
+ uses: actions/setup-node@v2
+ - run: npm install
+ - name: Checkout Code
+ uses: actions/checkout@v2
+ - name: Setup Python
+ uses: actions/setup-python@v2
+ with:
+ python-version: 3.7
+ - name: Install Dependencies
+ run: |
+ sudo apt-get update
+ sudo apt-get install gettext
+ pip3 install invoke
+ invoke install
+ invoke static
+ - name: Check HTML Files
+ run: |
+ npm install markuplint
+ npx markuplint InvenTree/build/templates/build/*.html
+ npx markuplint InvenTree/common/templates/common/*.html
+ npx markuplint InvenTree/company/templates/company/*.html
+ npx markuplint InvenTree/order/templates/order/*.html
+ npx markuplint InvenTree/part/templates/part/*.html
+ npx markuplint InvenTree/stock/templates/stock/*.html
+ npx markuplint InvenTree/templates/*.html
+ npx markuplint InvenTree/templates/InvenTree/*.html
+ npx markuplint InvenTree/templates/InvenTree/settings/*.html
+
diff --git a/.github/workflows/javascript.yaml b/.github/workflows/javascript.yaml
index 908a87e31c..a07b516ac6 100644
--- a/.github/workflows/javascript.yaml
+++ b/.github/workflows/javascript.yaml
@@ -18,11 +18,33 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ INVENTREE_DB_ENGINE: sqlite3
+ INVENTREE_DB_NAME: inventree
+ INVENTREE_MEDIA_ROOT: ./media
+ INVENTREE_STATIC_ROOT: ./static
steps:
+ - name: Install node.js
+ uses: actions/setup-node@v2
+ - run: npm install
- name: Checkout Code
uses: actions/checkout@v2
- - name: Check Files
+ - name: Setup Python
+ uses: actions/setup-python@v2
+ with:
+ python-version: 3.7
+ - name: Install Dependencies
+ run: |
+ sudo apt-get update
+ sudo apt-get install gettext
+ pip3 install invoke
+ invoke install
+ invoke static
+ - name: Check Templated Files
run: |
cd ci
python check_js_templates.py
-
\ No newline at end of file
+ - name: Lint Javascript Files
+ run: |
+ npm install eslint eslint-config-google
+ invoke render-js-files
+ npx eslint js_tmp/*.js
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index f3fa0ac8c1..420524d06f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -67,8 +67,16 @@ secret_key.txt
.coverage
htmlcov/
+# Temporary javascript files (used for testing)
+js_tmp/
+
# Development files
dev/
# Locale stats file
locale_stats.json
+
+# node.js
+package-lock.json
+package.json
+node_modules/
\ No newline at end of file
diff --git a/InvenTree/InvenTree/ci_render_js.py b/InvenTree/InvenTree/ci_render_js.py
new file mode 100644
index 0000000000..62e3fc4667
--- /dev/null
+++ b/InvenTree/InvenTree/ci_render_js.py
@@ -0,0 +1,100 @@
+"""
+Pull rendered copies of the templated
+"""
+
+from django.http import response
+from django.test import TestCase, testcases
+from django.contrib.auth import get_user_model
+
+import os
+import pathlib
+
+
+class RenderJavascriptFiles(TestCase):
+ """
+ A unit test to "render" javascript files.
+
+ The server renders templated javascript files,
+ we need the fully-rendered files for linting and static tests.
+ """
+
+ def setUp(self):
+
+ user = get_user_model()
+
+ self.user = user.objects.create_user(
+ username='testuser',
+ password='testpassword',
+ email='user@gmail.com',
+ )
+
+ self.client.login(username='testuser', password='testpassword')
+
+ def download_file(self, filename, prefix):
+
+ url = os.path.join(prefix, filename)
+
+ response = self.client.get(url)
+
+ here = os.path.abspath(os.path.dirname(__file__))
+
+ output_dir = os.path.join(
+ here,
+ '..',
+ '..',
+ 'js_tmp',
+ )
+
+ output_dir = os.path.abspath(output_dir)
+
+ if not os.path.exists(output_dir):
+ os.mkdir(output_dir)
+
+ output_file = os.path.join(
+ output_dir,
+ filename,
+ )
+
+ with open(output_file, 'wb') as output:
+ output.write(response.content)
+
+ def download_files(self, subdir, prefix):
+ here = os.path.abspath(os.path.dirname(__file__))
+
+ js_template_dir = os.path.join(
+ here,
+ '..',
+ 'templates',
+ 'js',
+ )
+
+ directory = os.path.join(js_template_dir, subdir)
+
+ directory = os.path.abspath(directory)
+
+ js_files = pathlib.Path(directory).rglob('*.js')
+
+ n = 0
+
+ for f in js_files:
+ js = os.path.basename(f)
+
+ self.download_file(js, prefix)
+
+ n += 1
+
+ return n
+
+ def test_render_files(self):
+ """
+ Look for all javascript files
+ """
+
+ n = 0
+
+ print("Rendering javascript files...")
+
+ n += self.download_files('translated', '/js/i18n')
+ n += self.download_files('dynamic', '/js/dynamic')
+
+ print(f"Rendered {n} javascript files.")
diff --git a/InvenTree/InvenTree/filters.py b/InvenTree/InvenTree/filters.py
new file mode 100644
index 0000000000..cd1b769646
--- /dev/null
+++ b/InvenTree/InvenTree/filters.py
@@ -0,0 +1,51 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from rest_framework.filters import OrderingFilter
+
+
+class InvenTreeOrderingFilter(OrderingFilter):
+ """
+ Custom OrderingFilter class which allows aliased filtering of related fields.
+
+ To use, simply specify this filter in the "filter_backends" section.
+
+ filter_backends = [
+ InvenTreeOrderingFilter,
+ ]
+
+ Then, specify a ordering_field_aliases attribute:
+
+ ordering_field_alises = {
+ 'name': 'part__part__name',
+ 'SKU': 'part__SKU',
+ }
+ """
+
+ def get_ordering(self, request, queryset, view):
+
+ ordering = super().get_ordering(request, queryset, view)
+
+ aliases = getattr(view, 'ordering_field_aliases', None)
+
+ # Attempt to map ordering fields based on provided aliases
+ if ordering is not None and aliases is not None:
+ """
+ Ordering fields should be mapped to separate fields
+ """
+
+ for idx, field in enumerate(ordering):
+
+ reverse = False
+
+ if field.startswith('-'):
+ field = field[1:]
+ reverse = True
+
+ if field in aliases:
+ ordering[idx] = aliases[field]
+
+ if reverse:
+ ordering[idx] = '-' + ordering[idx]
+
+ return ordering
diff --git a/InvenTree/InvenTree/serializers.py b/InvenTree/InvenTree/serializers.py
index 4cea0a218c..0d21550f00 100644
--- a/InvenTree/InvenTree/serializers.py
+++ b/InvenTree/InvenTree/serializers.py
@@ -10,6 +10,8 @@ import os
from decimal import Decimal
+from collections import OrderedDict
+
from django.conf import settings
from django.contrib.auth.models import User
from django.core.exceptions import ValidationError as DjangoValidationError
@@ -94,9 +96,14 @@ class InvenTreeModelSerializer(serializers.ModelSerializer):
# If instance is None, we are creating a new instance
if instance is None and data is not empty:
-
- # Required to side-step immutability of a QueryDict
- data = data.copy()
+
+ if data is None:
+ data = OrderedDict()
+ else:
+ new_data = OrderedDict()
+ new_data.update(data)
+
+ data = new_data
# Add missing fields which have default values
ModelClass = self.Meta.model
diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py
index 71f6388c68..7d51c6a4cf 100644
--- a/InvenTree/InvenTree/urls.py
+++ b/InvenTree/InvenTree/urls.py
@@ -111,6 +111,7 @@ translated_javascript_urls = [
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'^helpers.js', DynamicJsView.as_view(template_name='js/translated/helpers.js'), name='helpers.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'),
diff --git a/InvenTree/InvenTree/version.py b/InvenTree/InvenTree/version.py
index 5e7453a80b..b53cacff79 100644
--- a/InvenTree/InvenTree/version.py
+++ b/InvenTree/InvenTree/version.py
@@ -10,11 +10,15 @@ import common.models
INVENTREE_SW_VERSION = "0.5.0 pre"
-INVENTREE_API_VERSION = 10
+INVENTREE_API_VERSION = 11
"""
Increment this API version number whenever there is a significant change to the API that any clients need to know about
+v11 -> 2021-08-26
+ - Adds "units" field to PartBriefSerializer
+ - This allows units to be introspected from the "part_detail" field in the StockItem serializer
+
v10 -> 2021-08-23
- Adds "purchase_price_currency" to StockItem serializer
- Adds "purchase_price_string" to StockItem serializer
diff --git a/InvenTree/build/templates/build/auto_allocate.html b/InvenTree/build/templates/build/auto_allocate.html
index 48d1837ae0..2f2c7bbca7 100644
--- a/InvenTree/build/templates/build/auto_allocate.html
+++ b/InvenTree/build/templates/build/auto_allocate.html
@@ -6,7 +6,7 @@
{{ block.super }}
- {% trans "Automatically Allocate Stock" %}
+ {% trans "Automatically Allocate Stock" %}
{% trans "The following stock items will be allocated to the specified build output" %}
- {% trans "Build Order is incomplete" %}
+ {% trans "Build Order is incomplete" %}
{% if build.incomplete_count > 0 %}
{% trans "Incompleted build outputs remain" %}
diff --git a/InvenTree/build/templates/build/create_build_item.html b/InvenTree/build/templates/build/create_build_item.html
index 9ebf5bb389..cc23bd49a9 100644
--- a/InvenTree/build/templates/build/create_build_item.html
+++ b/InvenTree/build/templates/build/create_build_item.html
@@ -8,7 +8,7 @@
{% if output %}
- {% blocktrans %}The allocated stock will be installed into the following build output: {{output}}{% endblocktrans %}
+ {% blocktrans %}The allocated stock will be installed into the following build output: {{output}}{% endblocktrans %}
{% endif %}
diff --git a/InvenTree/build/templates/build/detail.html b/InvenTree/build/templates/build/detail.html
index d6b59a060d..421cac059c 100644
--- a/InvenTree/build/templates/build/detail.html
+++ b/InvenTree/build/templates/build/detail.html
@@ -40,7 +40,7 @@
{% if build.take_from %}
{{ build.take_from }}{% include "clip.html"%}
{% else %}
- {% trans "Stock can be taken from any available location." %}
+ {% trans "Stock can be taken from any available location." %}
{% endif %}
@@ -53,7 +53,7 @@
{{ build.destination }}
{% include "clip.html"%}
{% else %}
- {% trans "Destination location not specified" %}
+ {% trans "Destination location not specified" %}
{% endif %}
@@ -127,7 +127,7 @@
{{ build.target_date }}{% if build.is_overdue %} {% endif %}
{% else %}
-
{% trans "No target date set" %}
+
{% trans "No target date set" %}
{% endif %}
@@ -136,7 +136,7 @@
{% if build.completion_date %}
{{ build.completion_date }}{% if build.completed_by %}{{ build.completed_by }}{% endif %}
{% else %}
-
{% trans "Build not complete" %}
+
{% trans "Build not complete" %}
{% endif %}
@@ -222,7 +222,7 @@
{% else %}
- {% trans "Create a new build output" %}
+ {% trans "Create a new build output" %}
{% trans "No incomplete build outputs remain." %}
{% trans "Create a new build output using the button above" %}
diff --git a/InvenTree/common/templates/common/edit_setting.html b/InvenTree/common/templates/common/edit_setting.html
index c74ed7d591..c479e268a5 100644
--- a/InvenTree/common/templates/common/edit_setting.html
+++ b/InvenTree/common/templates/common/edit_setting.html
@@ -6,9 +6,9 @@
{{ block.super }}
{% endblock %}
\ No newline at end of file
diff --git a/InvenTree/company/templates/company/company_base.html b/InvenTree/company/templates/company/company_base.html
index c50a9490f0..e4ca64b32e 100644
--- a/InvenTree/company/templates/company/company_base.html
+++ b/InvenTree/company/templates/company/company_base.html
@@ -78,7 +78,7 @@
{% if company.currency %}
{{ company.currency }}
{% else %}
- {% trans "Uses default currency" %}
+ {% trans "Uses default currency" %}
{% endif %}
diff --git a/InvenTree/company/templates/company/manufacturer_part.html b/InvenTree/company/templates/company/manufacturer_part.html
index cc2dd68840..13e81aaa90 100644
--- a/InvenTree/company/templates/company/manufacturer_part.html
+++ b/InvenTree/company/templates/company/manufacturer_part.html
@@ -225,7 +225,7 @@ $("#multi-parameter-delete").click(function() {
`;
selections.forEach(function(item) {
- text += `